Выражения генератора и понимание списка

413

Когда вы должны использовать выражения генератора и когда вы должны использовать списочные выражения в Python?

# Generator expression
(x*2 for x in range(256))

# List comprehension
[x*2 for x in range(256)]
Readonly
источник
28
может [exp for x in iter]быть просто сахар для list((exp for x in iter))? или есть разница в исполнении?
2013 года в 0:32
1
мне кажется, у меня был соответствующий вопрос, поэтому при использовании yield мы можем использовать только выражение генератора из функции или мы должны использовать yield для функции, чтобы вернуть объект генератора?
28
@ b0fh Очень поздний ответ на ваш комментарий: в Python2 есть небольшая разница: переменная цикла будет вытекать из списка, а выражение генератора не будет. Сравните X = [x**2 for x in range(5)]; print xс Y = list(y**2 for y in range(5)); print y, второе выдаст ошибку. В Python3 понимание списка действительно является синтаксическим сахаром для выражения генератора, переданного, list()как вы ожидали, поэтому переменная цикла больше не будет просачиваться .
Бас Суинкельс
13
Я бы предложил прочитать PEP 0289 . Подводя итог: «Этот PEP представляет выражения генератора как высокопроизводительное обобщение списков и генераторов с эффективным использованием памяти» . У этого также есть полезные примеры того, когда их использовать.
icc97
5
@ icc97 Я также опоздал на вечеринку на восемь лет, и связь с ПКП была идеальной. Спасибо, что так легко найти!
eenblam

Ответы:

284

Хороший ответ Джона (этот список лучше, если вы хотите повторять что-то несколько раз). Однако также стоит отметить, что вы должны использовать список, если вы хотите использовать любой из методов списка. Например, следующий код не будет работать:

def gen():
    return (something for something in get_some_stuff())

print gen()[:2]     # generators don't support indexing or slicing
print [5,6] + gen() # generators can't be added to lists

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

Поскольку производительность является наиболее распространенной причиной выбора одного над другим, я советую не беспокоиться об этом и просто выбрать один; если вы обнаружите, что ваша программа работает слишком медленно, тогда и только тогда вам следует вернуться и заняться настройкой своего кода.

Эли Кортрайт
источник
70
Иногда вам нужно использовать генераторы - например, если вы пишете сопрограммы с кооперативным планированием с использованием yield. Но если вы делаете это, вы, вероятно, не задаете этот вопрос;)
ephemient
12
Я знаю, что это старо, но я думаю, что стоит отметить, что генераторы (и любые итерируемые) могут быть добавлены в списки с расширением: a = [1, 2, 3] b = [4, 5, 6] a.extend(b)- теперь будет a [1, 2, 3, 4, 5, 6]. (Можете ли вы добавить новые строки в комментариях ??)
jarvisteve
12
@jarvisteve ваш пример противоречит словам, которые вы говорите. Здесь также есть тонкость. Списки могут быть расширены с помощью генераторов, но тогда не было смысла делать его генератором. Генераторы не могут быть расширены списками, и генераторы не совсем итерируемы. a = (x for x in range(0,10)), b = [1,2,3]например. a.extend(b)бросает исключение. b.extend(a)оценит все, в этом случае нет смысла делать его генератором.
Слейтер Викторофф
4
@SlaterTyranus, вы на 100% правы, и я проголосовал за вас за точность. тем не менее, я думаю, что его комментарий является полезным не ответом на вопрос ОП, потому что он поможет тем, кто оказался здесь, потому что они ввели что-то вроде «объединить генератор с пониманием списка» в поисковую систему.
ОПБ
1
Разве причина для использования генератора для итераций один раз (например, мое беспокойство по поводу нехватки памяти переопределяет мою заботу о «извлечении» значений по одному ), вероятно, все еще применима при многократном повторении? Я бы сказал, что это может сделать список более полезным, но достаточно ли этого, чтобы перевесить проблемы с памятью, это нечто другое.
Роб Грант
181

Итерация по выражению генератора или пониманию списка будет делать то же самое. Однако понимание списка сначала создаст весь список в памяти, в то время как выражение генератора будет создавать элементы на лету, так что вы сможете использовать его для очень больших (а также бесконечных!) Последовательностей.

дР.
источник
39
+1 за бесконечность. Вы не можете сделать это со списком, независимо от того, насколько мало вы заботитесь о производительности.
Пол Дрейпер
Можете ли вы создать бесконечные генераторы, используя метод понимания?
AnnanFay
5
@Annan Только если у вас уже есть доступ к другому бесконечному генератору. Например, itertools.count(n)это бесконечная последовательность целых чисел, начиная с n, поэтому (2 ** item for item in itertools.count(n))будет бесконечной последовательностью степеней, 2начиная с 2 ** n.
Кевин
2
Генератор удаляет элементы из памяти после их повторения. Так что это быстро, если у вас есть большие данные, вы просто хотите их отобразить, например. Это не боров памяти. с генераторами элементы обрабатываются «по мере необходимости». если вы хотите повесить список или повторить его (сохраните элементы), используйте понимание списка.
j2emanue
102

Используйте списки, когда результат должен повторяться несколько раз или когда скорость имеет первостепенное значение. Используйте выражения генератора, где диапазон большой или бесконечный.

Посмотрите выражения Генератора и список понятий для получения дополнительной информации.

Джон Милликин
источник
2
Это будет, вероятно, немного не по теме, но, к сожалению, «не гуглится» ... Что будет означать «первостепенное» в этом контексте? Я не являюсь носителем английского языка ... :)
Гильермо Арес
6
@GuillermoAres это прямой результат «поиска в Google» значения первостепенной важности: важнее всего остального; высший.
Sнаđошƒаӽ
1
Так listsбыстрее, чем generatorвыражения? Прочитав ответ Д.Ф., выяснилось, что все наоборот.
Хасан Бэйг
1
Вероятно, лучше сказать, что составление списков происходит быстрее, когда диапазон небольшой, но по мере увеличения масштаба становится более ценным вычислять значения на лету - как раз вовремя для их использования. Это то, что делает выражение генератора.
Кайл
59

Важным моментом является то, что понимание списка создает новый список. Генератор создает итеративный объект, который будет «фильтровать» исходный материал на лету, когда вы используете биты.

Представьте, что у вас есть файл журнала объемом 2 ТБ, называемый "принц", и вам нужно содержимое и длина для всех строк, начинающихся со слова "ВХОД".

Итак, попробуйте начать с написания списка:

logfile = open("hugefile.txt","r")
entry_lines = [(line,len(line)) for line in logfile if line.startswith("ENTRY")]

Это затирает весь файл, обрабатывает каждую строку и сохраняет совпадающие строки в вашем массиве. Следовательно, этот массив может содержать до 2 ТБ контента. Это много оперативной памяти, и, вероятно, не практично для ваших целей.

Поэтому вместо этого мы можем использовать генератор, чтобы применить «фильтр» к нашему контенту. На самом деле данные не читаются, пока мы не начнем итерацию по результату.

logfile = open("hugefile.txt","r")
entry_lines = ((line,len(line)) for line in logfile if line.startswith("ENTRY"))

Из нашего файла еще не было прочитано ни одной строки. На самом деле, скажем, мы хотим отфильтровать наш результат еще дальше:

long_entries = ((line,length) for (line,length) in entry_lines if length > 80)

Пока ничего не прочитано, но мы указали два генератора, которые будут работать с нашими данными так, как мы хотим.

Давайте запишем наши отфильтрованные строки в другой файл:

outfile = open("filtered.txt","a")
for entry,length in long_entries:
    outfile.write(entry)

Теперь мы читаем входной файл. Поскольку наш forцикл продолжает запрашивать дополнительные строки, long_entriesгенератор запрашивает строки из entry_linesгенератора, возвращая только те, длина которых превышает 80 символов. И, в свою очередь, entry_linesгенератор запрашивает строки (отфильтрованные как указано) отlogfile итератора, который, в свою очередь, читает файл.

Таким образом, вместо того, чтобы «выталкивать» данные в вашу функцию вывода в виде полностью заполненного списка, вы даете функции вывода способ «извлекать» данные только тогда, когда это необходимо. В нашем случае это гораздо эффективнее, но не так гибко. Генераторы один путь, один проход; данные из файла журнала, который мы прочитали, немедленно удаляются, поэтому мы не можем вернуться к предыдущей строке. С другой стороны, нам не нужно беспокоиться о сохранении данных, как только мы закончим с ними.

tylerl
источник
46

Преимущество выражения генератора заключается в том, что оно использует меньше памяти, поскольку не создает весь список сразу. Выражения генератора лучше всего использовать, когда список является посредником, например, суммируя результаты или создавая из результатов выборку.

Например:

sum(x*2 for x in xrange(256))

dict( (k, some_func(k)) for k in some_list_of_keys )

Преимущество состоит в том, что список генерируется не полностью, и поэтому используется мало памяти (и также должно быть быстрее)

Тем не менее, вы должны использовать списки, когда желаемый конечный продукт является списком. Вы не собираетесь сохранять какую-либо память, используя выражения генератора, так как вам нужен сгенерированный список. Вы также получаете возможность использовать любые функции списка, например отсортированные или обратные.

Например:

reversed( [x*2 for x in xrange(256)] )
цыпленок
источник
9
Правильно намекает на то, что выражения генератора должны использоваться таким образом. Потерять скобки! sum(x*2 for x in xrange(256))
u0b34a0f6ae
8
sortedи reversedотлично работает с любыми повторяемыми выражениями-генераторами.
marr75
1
Если вы можете использовать 2.7 и выше, этот пример dict () будет выглядеть лучше как понимание dict (PEP для этого старше, чем генератор выражений PEP, но занимает больше времени для приземления)
Юрген А. Эрхард
14

При создании генератора из изменяемого объекта (например, списка) следует помнить, что генератор будет оцениваться по состоянию списка во время использования генератора, а не во время создания генератора:

>>> mylist = ["a", "b", "c"]
>>> gen = (elem + "1" for elem in mylist)
>>> mylist.clear()
>>> for x in gen: print (x)
# nothing

Если есть вероятность, что ваш список будет изменен (или изменяемый объект внутри этого списка), но вам нужно состояние при создании генератора, вам нужно вместо этого использовать понимание списка.

Freaker
источник
1
И это должен быть принятый ответ. Если ваши данные больше доступной памяти, вы всегда должны использовать генераторы, хотя циклический список в памяти может быть быстрее (но у вас недостаточно памяти для этого).
Марек Марчак
4

Иногда вы можете избежать использования функции tee из itertools , она возвращает несколько итераторов для одного и того же генератора, который можно использовать независимо.

Джейкоб Ригби
источник
4

Я использую модуль Hadoop Mincemeat . Я думаю, что это отличный пример, чтобы принять к сведению:

import mincemeat

def mapfn(k,v):
    for w in v:
        yield 'sum',w
        #yield 'count',1


def reducefn(k,v): 
    r1=sum(v)
    r2=len(v)
    print r2
    m=r1/r2
    std=0
    for i in range(r2):
       std+=pow(abs(v[i]-m),2)  
    res=pow((std/r2),0.5)
    return r1,r2,res

Здесь генератор извлекает числа из текстового файла (размером до 15 ГБ) и применяет к этим числам простую математику, используя Hadoop map-Reduce. Если бы я не использовал функцию yield, а вместо понимания списка, вычисление сумм и среднего потребовало бы намного больше времени (не говоря уже о сложности пространства).

Hadoop - отличный пример использования всех преимуществ Генераторов.

Мерфи
источник