Выражения генератора и понимание списка
Когда вы должны использовать выражения генератора и когда вы должны использовать списочные выражения в Python?
# Generator expression
(x*2 for x in range(256))
# List comprehension
[x*2 for x in range(256)]
14 ответов
Хороший ответ Джона (эти списки лучше, если вы хотите повторять что-то несколько раз). Однако также стоит отметить, что вы должны использовать список, если вы хотите использовать любой из методов списка. Например, следующий код не будет работать:
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
В основном, используйте выражение генератора, если все, что вы делаете, это итерация один раз. Если вы хотите сохранить и использовать сгенерированные результаты, то вам, вероятно, лучше понять список.
Поскольку производительность является наиболее распространенной причиной выбора одного над другим, я советую не беспокоиться об этом и просто выбрать один; если вы обнаружите, что ваша программа работает слишком медленно, тогда и только тогда вам следует вернуться и заняться настройкой своего кода.
Итерация по выражению генератора или списку будет делать то же самое. Однако понимание списка сначала создаст весь список в памяти, в то время как выражение генератора будет создавать элементы на лету, поэтому вы можете использовать его для очень больших (а также бесконечных!) Последовательностей.
Используйте списки, когда результат должен повторяться несколько раз или когда скорость имеет первостепенное значение. Используйте выражения генератора, где диапазон большой или бесконечный.
Важным моментом является то, что понимание списка создает новый список. Генератор создает итеративный объект, который будет "фильтровать" исходный материал на лету, когда вы будете использовать биты.
Представьте, что у вас есть файл журнала размером 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
итератор, который в свою очередь читает файл.
Таким образом, вместо того, чтобы "выталкивать" данные в вашу функцию вывода в виде полностью заполненного списка, вы даете функции вывода способ "извлекать" данные только тогда, когда это необходимо. В нашем случае это гораздо эффективнее, но не так гибко. Генераторы один путь, один проход; данные из файла журнала, который мы прочитали, немедленно удаляются, поэтому мы не можем вернуться к предыдущей строке. С другой стороны, нам не нужно беспокоиться о сохранении данных, как только мы закончим с ними.
Преимущество выражения генератора заключается в том, что оно использует меньше памяти, поскольку не создает весь список сразу. Выражения генератора лучше всего использовать, когда список является посредником, например, суммируя результаты или создавая из результатов выборку.
Например:
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)] )
При создании генератора из изменяемого объекта (например, списка) следует помнить, что генератор будет оцениваться по состоянию списка во время использования генератора, а не во время создания генератора:
>>> mylist = ["a", "b", "c"]
>>> gen = (elem + "1" for elem in mylist)
>>> mylist.clear()
>>> for x in gen: print (x)
# nothing
Если есть вероятность, что ваш список будет изменен (или изменяемый объект внутри этого списка), но вам нужно состояние при создании генератора, вам нужно вместо этого использовать понимание списка.
Python 3.7:
Понимание списков происходит быстрее.
Генераторы более эффективны с точки зрения памяти.
Как уже говорили все остальные, если вы хотите масштабировать бесконечные данные, в конечном итоге вам понадобится генератор. Для относительно статичных небольших и средних заданий, где необходима скорость, лучше всего будет составить список.
Я использую модуль 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 - отличный пример использования всех преимуществ Генераторов.
Некоторые примечания для встроенных функций Python:
Используйте выражение генератора, если вам нужно использовать короткое замыкание или . Эти функции предназначены для остановки итерации, когда ответ известен, но понимание списка должно оценивать каждый элемент, прежде чем функция может быть вызвана.
Например, если у нас есть
from time import sleep
def long_calculation(value):
sleep(1) # for simulation purposes
return value == 1
затемany([long_calculation(x) for x in range(10)])
занимает около десяти секунд, так как будет вызываться для каждогоx
.any(long_calculation(x) for x in range(10))
занимает всего около двух секунд, так какlong_calculation
будет вызываться только с0
и1
входы.
Когда и перебирают понимание списка, они все равно перестанут проверять элементы на истинность , как только ответ станет известен (как толькоany
находит истинный результат, илиall
находит ложный); однако это обычно тривиально по сравнению с фактической работой, проделанной пониманием.
Выражения-генераторы, конечно, более эффективно используют память, когда их можно использовать. Понимание списков будет немного быстрее с некоротким замыканиемmin
, иsum
(время дляmax
показано здесь):
$ python -m timeit "max(_ for _ in range(1))"
500000 loops, best of 5: 476 nsec per loop
$ python -m timeit "max([_ for _ in range(1)])"
500000 loops, best of 5: 425 nsec per loop
$ python -m timeit "max(_ for _ in range(100))"
50000 loops, best of 5: 4.42 usec per loop
$ python -m timeit "max([_ for _ in range(100)])"
100000 loops, best of 5: 3.79 usec per loop
$ python -m timeit "max(_ for _ in range(10000))"
500 loops, best of 5: 468 usec per loop
$ python -m timeit "max([_ for _ in range(10000)])"
500 loops, best of 5: 442 usec per loop
Иногда вы можете избежать использования функции tee из itertools, она возвращает несколько итераторов для одного и того же генератора, который можно использовать независимо.
Составление списков нетерпеливо, но генераторы ленивы.
В составлении списков все объекты создаются сразу, создание и возврат списка занимает больше времени. В выражениях генератора создание объекта откладывается до запроса. При генерации объект создается и немедленно возвращается.
Итерация выполняется быстрее при понимании списков, поскольку объекты уже созданы.
Если вы перебираете все элементы в понимании списка и выражении генератора, временная производительность будет примерно такой же. Несмотря на то, что выражение генератора сразу возвращает объект-генератор, оно не создает все элементы. Каждый раз, когда вы перебираете новый элемент, он будет создавать и возвращать его.
Но если вы не перебираете все элементы, генератор будет более эффективным. Допустим, вам нужно создать список, содержащий миллионы элементов, но вы используете только 10 из них. Вам все еще нужно создать миллионы предметов. Вы просто тратите время на миллионы вычислений, чтобы создать миллионы элементов для использования только 10. Или, если вы делаете миллионы запросов api, но в итоге используете только 10 из них. Поскольку выражения генератора ленивы, он не выполняет все вычисления или вызовы API, если это не требуется. В этом случае использование выражений генератора будет более эффективным.
При просмотре списков в память загружается вся коллекция. Но выражения генератора, как только он вернет вам значение после вашего
next()
вызов, это сделано с ним, и ему больше не нужно хранить его в памяти. В память загружается только один элемент. Если вы выполняете итерацию по огромному файлу на диске, если файл слишком большой, у вас могут возникнуть проблемы с памятью. В этом случае использование выражения генератора более эффективно.
Есть что-то, что я думаю, что большинство ответов пропустили. Понимание списков в основном создает список и добавляет его в стек. В случаях, когда объект списка очень велик, ваш скриптовый процесс будет остановлен. В этом случае генератор был бы более предпочтительным, так как его значения не хранятся в памяти, а сохраняются как функция с отслеживанием состояния. Также скорость создания; понимание списка медленнее, чем понимание генератора
Короче говоря; используйте понимание списка, когда размер объекта не слишком велик, иначе используйте понимание генератора
Для функционального программирования мы хотим использовать как можно меньше индексации. По этой причине, если мы хотим продолжать использовать элементы после того, как мы возьмем первый фрагмент элементов, islice() является лучшим выбором, поскольку состояние итератора сохраняется.
from itertools import islice
def slice_and_continue(sequence):
ret = []
seq_i = iter(sequence) #create an iterator from the list
seq_slice = islice(seq_i,3) #take first 3 elements and print
for x in seq_slice: print(x),
for x in seq_i: print(x**2), #square the rest of the numbers
slice_and_continue([1,2,3,4,5])
вывод: 1 2 3 16 25
Как насчет использования [(exp для x в iter)], чтобы получить пользу от обоих. Производительность от понимания генератора, а также методы списка