Управлять списком в питоническом режиме, когда вывод зависит от других элементов

У меня есть задача, требующая операции над каждым элементом списка, причем результат операции зависит от других элементов в списке.

Например, мне может понадобиться конкатенация списка строк, условных на них, начиная с определенного символа:

Этот код решает проблему:

x = ['*a', 'b', 'c', '*d', 'e', '*f', '*g']
concat = []
for element in x:
    if element.startswith('*'):
        concat.append(element)
    else:
        concat[len(concat) - 1] += element

в результате:

concat
Out[16]: ['*abc', '*de', '*f', '*g']

Но это кажется ужасно не-питоническим. Как работать с элементами a list, когда результат операции зависит от предыдущих результатов?

Ответ 1

Несколько соответствующих выдержек из import this (арбитр того, что такое Pythonic):

  • Простой лучше, чем сложный
  • Показатели удобочитаемости
  • Явный лучше, чем неявный.

Я бы просто использовал такой код, а не беспокоился о замене цикла for на что-то "плоское".

x = ['*a', 'b', 'c', '*d', 'e', '*f', '*g']
partials = []
for element in x:
    if element.startswith('*'):
        partials.append([])
    partials[-1].append(element)
concat = map("".join, partials)

Ответ 2

Вы можете использовать регулярное выражение, чтобы выполнить это кратко. Однако это, как правило, обходит ваш вопрос относительно того, как работать с зависимыми элементами списка. Кредиты mbomb007 для улучшения разрешенных функций персонажа.

import re
z = re.findall('\*[^*]+',"".join(x))

Выходы:

['*abc', '*de', '*f', '*g']

Малый бенчмаркинг:

Donkey Kong answer:

import timeit
setup = '''
import re
x = ['*a', 'b', 'c', '*d', 'e', '*f', '*g']
y = ['*a', 'b', 'c', '*d', 'e', '*f', '*g'] * 100
'''
print (min(timeit.Timer('re.findall("\*[^\*]+","".join(x))', setup=setup).repeat(7, 1000)))
print (min(timeit.Timer('re.findall("\*[^\*]+","".join(y))', setup=setup).repeat(7, 1000)))

Возвращает 0.00226416693456 и 0.06827958075 соответственно.

Ответ Chepner:

setup = '''
x = ['*a', 'b', 'c', '*d', 'e', '*f', '*g']
y = ['*a', 'b', 'c', '*d', 'e', '*f', '*g'] * 100
def chepner(x):
    partials = []
    for element in x:
        if element.startswith('*'):
            partials.append([])
        partials[-1].append(element)
    concat = map("".join, partials)  
    return concat
'''
print (min(timeit.Timer('chepner(x)', setup=setup).repeat(7, 1000)))
print (min(timeit.Timer('chepner(y)', setup=setup).repeat(7, 1000)))

Возвращает 0.00456210269896 и 0.364635824689 соответственно.

Ответ Сакшама

setup = '''
x = ['*a', 'b', 'c', '*d', 'e', '*f', '*g'] 
y = ['*a', 'b', 'c', '*d', 'e', '*f', '*g'] * 100

'''
print (min(timeit.Timer("['*'+item for item in ''.join(x).split('*') if item]", setup=setup).repeat(7, 1000)))
print (min(timeit.Timer("['*'+item for item in ''.join(y).split('*') if item]", setup=setup).repeat(7, 1000))))

Возвращает 0.00104848906006 и 0.0556093171512 соответственно.

tl; dr Сакшам немного быстрее моего, затем Хепнер следует за нашими.

Ответ 3

Как насчет этого:

>>> x = ['*a', 'b', 'c', '*d', 'e', '*f', '*g']
>>> print ['*'+item for item in ''.join(x).split('*') if item]
['*abc', '*de', '*f', '*g']

Ответ 4

"".join(x).split("*")

может быть достаточно, грубо это может быть надуманным примером в вашем OP, который упрощен и, как таковой, не будет работать

Ответ 5

Я чувствую, что это очень Pythonic:

# assumes no empty strings, or no spaces in strings
"".join(x).replace('*', ' *').split()

Вот функциональный подход к нему:

from functools import reduce

# assumes no empty strings
def reduction(l, it):
    if it[0] == '*':
        return l + [it]
    else:
        new_l, last = l[:-1], l[-1]
        return new_l + [last + it]

x = ['*a', 'b', 'c', '*d', 'e', '*f', '*g']

print reduce(reduction, x, [])
>>> ['*abc', '*de', '*f', '*g']

Если вы поклонник лямбда (не очень Pythonic), вы можете уйти от этого:

# Don't do this, it ugly and unreadable.
reduce(lambda l, it: l + [it] if it.startswith('*') else l[:-1] + [l[-1]+it], x, [])

Ответ 6

Это ужасно близко к тому, что делает itertools.groupby, и на самом деле с небольшой помощью карри я могу заставить его продолжать группировку до тех пор, пока возникает условие "break", например startswith('*').

from itertools import groupby

def new_group_when_true(pred):
    group_num = 0
    def group_for_elem(elem):
        nonlocal group_num
        if pred(elem):
            group_num +=1
        return group_num
    return group_for_elem

l = ['*a', 'b', 'c', '*d', 'e', '*f', '*g']

test = new_group_when_true(lambda elem: elem.startswith('*'))

grouped = [list(v) for k,v in groupby(l, test)]

Результат:

>>> print(grouped)
[['*a', 'b', 'c'], ['*d', 'e'], ['*f'], ['*g']]

Для ключевого слова nonlocal требуется, конечно, Python 3. Другая возможность заключалась бы в том, чтобы сделать класс в соответствии с groupby "эквивалентным кодом" из документации itertools.

Я не знаю, что это больше Pythonic, чем ваш код, но я думаю, что идея перехода в стандартную библиотеку, чтобы увидеть, подходит ли что-то почти к вашим потребностям, является полезной точкой.