Это скорее обратное Для чего вы можете использовать функции генератора Python?: генераторы python, выражения генераторов и модуль itertools - одни из моих любимых функций python в наши дни. Они особенно полезны при настройке цепочек операций для выполнения с большой грудой данных - я часто использую их при обработке файлов DSV.
Итак, когда нет - подходящее время для использования генератора, выражения генератора или функции itertools?
zip()itertools.izip(), илиrange() вместо xrange(), или[x for x in foo] вместо (x for x in foo)?Очевидно, что в конечном итоге нам нужно «преобразовать» генератор в фактические данные, обычно путем создания списка или итерации по нему с помощью цикла, не являющегося генератором. Иногда нам просто нужно знать длину. Я не об этом спрашиваю.
Мы используем генераторы, чтобы не назначать новые списки в память для промежуточных данных. Это особенно важно для больших наборов данных. Имеет ли это смысл и для небольших наборов данных? Есть ли заметный компромисс между памятью и процессором?
Мне особенно интересно, провел ли кто-нибудь профилирование по этому поводу, в свете открывающего глаза обсуждения производительность понимания списка по сравнению с map () и filter (). (альтернативная ссылка)
Отвечает ли это на ваш вопрос? Выражения генератора против понимания списков






Вы никогда не должны отдавать предпочтение zip перед izip, range перед xrange или составления списка перед пониманием генератора. В Python 3.0 range имеет семантику, подобную xrange, а zip имеет семантику, подобную izip.
Понимание списков на самом деле более ясное, как list(frob(x) for x in foo), для тех случаев, когда вам нужен реальный список.
@ Стивен: Я не возражаю, но мне интересно, какова причина вашего ответа. Почему понимание zip, range и list никогда не должно быть предпочтительнее соответствующей "ленивой" версии ??
потому что, как он сказал, старое поведение zip и range скоро исчезнет.
@ Стивен: Хороший момент. Я забыл об этих изменениях в версии 3.0, а это, вероятно, означает, что кто-то наверху убежден в своем общем превосходстве. Re: Составление списков, они часто более понятны (и быстрее, чем расширенные циклы for!), Но можно легко написать непонятные понимания списков.
Я имел в виду, что список (frob (x) for x in foo) более описательный, чем [frob (x) for x in foo] - то есть понимание списка [] «сахар» бесполезно.
Я понимаю, что вы имеете в виду, но считаю, что форма [] достаточно описательна (и в целом более лаконична и менее загромождена). Но это дело вкуса.
И похоже, что это будет официальный ответ, в основном по поводу того, что генераторы стали нормальными формами в 3.0. Никто не привел к серьезному ущербу для осторожного использования генераторов, даже на коротких наборах данных, поэтому я буду продолжать использовать их с энтузиазмом.
Пожалуйста, проверьте мой ответ с цифрами производительности ниже. Понимание списков может быть значительно быстрее, чем выражения генератора при использовании psyco.
Операции со списками быстрее для небольших размеров данных, но все работает быстро, когда размер данных небольшой, поэтому вы всегда должны предпочесть генераторы, если у вас нет особой причины использовать списки (по таким причинам см. Ответ Райана Гинстрома).
Это довольно слабое место, я не могу вспомнить случай, когда вы не можете получить ленивую версию, играя с try/except NameError или ImportError.
Использование timeit в Python 3.8 дает [frob(x) for x in foo] на 50% быстрее, чем list(frob(x) for x in foo). Последнее не является пониманием списка, как говорится в сообщении, - это генератор выражения.
В общем, не используйте генератор, когда вам нужны операции со списком, такие как len (), reversed () и т. д.
Также могут быть случаи, когда вам не нужна ленивая оценка (например, чтобы выполнить все вычисления заранее, чтобы вы могли освободить ресурс). В этом случае может быть лучше выражение списка.
Кроме того, предварительное выполнение всех вычислений гарантирует, что если вычисление элементов списка вызовет исключение, оно будет сгенерировано в точке, где список равен созданный, а не в цикле, который впоследствии проходит через него. Если вам нужно обеспечить безошибочную обработку всего списка перед продолжением, генераторы бесполезны.
Неплохо подмечено. Очень неприятно пройти половину обработки генератора, только чтобы все взорвалось. Это может быть потенциально опасно.
Как вы упомянули: «Это особенно важно для больших наборов данных», я думаю, это отвечает на ваш вопрос.
Если вы не сталкиваетесь с какими-либо стенами, с точки зрения производительности вы все равно можете придерживаться списков и стандартных функций. Затем, когда вы столкнетесь с проблемами с производительностью, переключитесь.
Однако, как упоминал @ u0b34a0f6ae в комментариях, использование генераторов в начале может облегчить вам масштабирование до более крупных наборов данных.
Генераторы +1 делают ваш код более готовым к работе с большими наборами данных, даже если вы этого не ожидаете.
Профиль, Профиль, Профиль.
Профилирование кода - единственный способ узнать, имеет ли то, что вы делаете, какой-либо эффект.
Большинство случаев использования xrange, генераторов и т. д. Превышают статический размер, небольшие наборы данных. Только когда вы добираетесь до больших наборов данных, это действительно имеет значение. range () vs. xrange () в основном заключается в том, чтобы сделать код немного более уродливым, и ничего не потерять, а, возможно, что-то получить.
Профиль, Профиль, Профиль.
Профиль, действительно. На днях я попробую провести эмпирическое сравнение. До этого я просто надеялся, что кто-то другой уже это сделал. :)
Профиль, Профиль, Профиль. Я полностью согласен. Профиль, Профиль, Профиль.
Что касается производительности, я не могу вспомнить ни одного случая, когда вы хотели бы использовать список вместо генератора.
all(True for _ in range(10 ** 8)) медленнее, чем all([True for _ in range(10 ** 8)]) в Python 3.8. Я бы предпочел здесь список, а не генератор
Я никогда не встречал ситуации, когда генераторы мешали бы тому, что вы пытаетесь сделать. Однако есть множество случаев, когда использование генераторов не поможет вам больше, чем их неиспользование.
Например:
sorted(xrange(5))
Не предлагает никаких улучшений по сравнению с:
sorted(range(5))
Ни один из них не предлагает никаких улучшений по сравнению с range(5), поскольку результирующий список уже отсортирован.
Что касается производительности: при использовании psyco списки могут быть немного быстрее, чем генераторы. В приведенном ниже примере списки почти на 50% быстрее при использовании psyco.full ().
import psyco
import time
import cStringIO
def time_func(func):
"""The amount of time it requires func to run"""
start = time.clock()
func()
return time.clock() - start
def fizzbuzz(num):
"""That algorithm we all know and love"""
if not num % 3 and not num % 5:
return "%d fizz buzz" % num
elif not num % 3:
return "%d fizz" % num
elif not num % 5:
return "%d buzz" % num
return None
def with_list(num):
"""Try getting fizzbuzz with a list comprehension and range"""
out = cStringIO.StringIO()
for fibby in [fizzbuzz(x) for x in range(1, num) if fizzbuzz(x)]:
print >> out, fibby
return out.getvalue()
def with_genx(num):
"""Try getting fizzbuzz with generator expression and xrange"""
out = cStringIO.StringIO()
for fibby in (fizzbuzz(x) for x in xrange(1, num) if fizzbuzz(x)):
print >> out, fibby
return out.getvalue()
def main():
"""
Test speed of generator expressions versus list comprehensions,
with and without psyco.
"""
#our variables
nums = [10000, 100000]
funcs = [with_list, with_genx]
# try without psyco 1st
print "without psyco"
for num in nums:
print " number:", num
for func in funcs:
print func.__name__, time_func(lambda : func(num)), "seconds"
print
# now with psyco
print "with psyco"
psyco.full()
for num in nums:
print " number:", num
for func in funcs:
print func.__name__, time_func(lambda : func(num)), "seconds"
print
if __name__ == "__main__":
main()
Полученные результаты:
without psyco
number: 10000
with_list 0.0519102208309 seconds
with_genx 0.0535933367509 seconds
number: 100000
with_list 0.542204280744 seconds
with_genx 0.557837353115 seconds
with psyco
number: 10000
with_list 0.0286369007033 seconds
with_genx 0.0513424889137 seconds
number: 100000
with_list 0.335414877839 seconds
with_genx 0.580363490491 seconds
Это потому, что psyco вообще не ускоряет генераторы, поэтому это скорее недостаток psyco, чем генераторов. Но хороший ответ.
Кроме того, psyco сейчас практически не обслуживается. Все разработчики тратят время на JIT PyPy, который, насколько мне известно, оптимизирует генераторы.
Вы должны предпочесть понимание списков, если вам нужно сохранить значения для чего-то еще позже, и размер вашего набора не слишком велик.
Например: вы создаете список, который вы будете повторять несколько раз в своей программе.
В некоторой степени вы можете думать о генераторах как о замене итераций (циклов), а не понимания списков как о типе инициализации структуры данных. Если вы хотите сохранить структуру данных, используйте списки.
Если вам нужен только ограниченный просмотр вперед / назад в потоке, возможно, itertools.tee() может вам помочь. Но, как правило, если вам нужно более одного прохода или произвольный доступ к некоторым промежуточным данным, сделайте их список / набор / определение.
Используйте список вместо генератора, когда:
1) Вам необходимо получить доступ к данным несколько раз (т.е. кешировать результаты, а не пересчитывать их):
for i in outer: # used once, okay to be a generator or return a list
for j in inner: # used multiple times, reusing a list is better
...
2) Вам нужен произвольный доступ (или любой доступ, кроме прямого последовательного порядка):
for i in reversed(data): ... # generators aren't reversible
s[i], s[j] = s[j], s[i] # generators aren't indexable
3) Вам нужны строки присоединиться (для чего требуется два прохода по данным):
s = ''.join(data) # lists are faster than generators in this use case
4) Вы используете PyPy, который иногда не может оптимизировать код генератора настолько, насколько это возможно, с помощью обычных вызовов функций и манипуляций со списком.
Что касается пункта 3, нельзя ли избежать двух проходов, используя ireduce для репликации соединения?
Спасибо! Я не знал о поведении соединения строк. Можете ли вы предоставить или дать ссылку на объяснение, почему для этого требуется два прохода?
@DavidEyk str.join делает один проход для суммирования длин всех фрагментов строки, чтобы он знал много памяти, которую нужно выделить для комбинированного конечного результата. Второй проход копирует фрагменты строки в новый буфер для создания одной новой строки. См. hg.python.org/cpython/file/82fd95c2851b/Objects/stringlib/…
Интересно, что я очень часто использую генераторы для объединения строк. Но, интересно, как это работает, если нужно два прохода? например ''.join('%s' % i for i in xrange(10))
@ ikaros45 Если вход в присоединиться не является списком, он должен проделать дополнительную работу для создания временного списка для двух проходов. Примерно это `` данные = данные if isinstance (data, list) else list (data); n = сумма (карта (len, данные)); буфер = массив байтов (n); ... <копировать фрагменты в буфер> ``.
Я поставил аналогичный вопрос здесь и провел некоторый анализ, чтобы найти, что в моем конкретном примересписки быстрее для итераций длины
<5.