Управлять списком в питоническом режиме, когда вывод зависит от других элементов
У меня есть задача, требующая операции над каждым элементом списка, причем результат операции зависит от других элементов в списке.
Например, мне может понадобиться конкатенация списка строк, условных на них, начиная с определенного символа:
Этот код решает проблему:
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, чем ваш код, но я думаю, что идея перехода в стандартную библиотеку, чтобы увидеть, подходит ли что-то почти к вашим потребностям, является полезной точкой.