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


413

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

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

# List comprehension
[x*2 for x in range(256)]

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 итератора, который, в свою очередь, читает файл.

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


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

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


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 - отличный пример использования всех преимуществ Генераторов.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.