Python: выражение генератора против урожая
В Python существует ли разница между созданием объекта-генератора с помощью выражения генератора по сравнению с использованием инструкции yield?
Используя выход:
def Generator(x, y):
for i in xrange(x):
for j in xrange(y):
yield(i, j)
Использование выражения генератора:
def Generator(x, y):
return ((i, j) for i in xrange(x) for j in xrange(y))
Обе функции возвращают объекты-генераторы, которые производят кортежи, например. (0,0), (0,1) и т.д.
Любые преимущества того или другого? Мысли?
Спасибо всем! В этих ответах есть много отличной информации и дальнейших ссылок!
Ответы
Ответ 1
Есть только незначительные различия в двух. Вы можете использовать модуль dis
для изучения такого рода вещей для себя.
Изменить: Моя первая версия декомпилировала выражение генератора, созданное в области модулей в интерактивной подсказке. Это немного отличается от версии OP, используемой внутри функции. Я изменил это, чтобы соответствовать фактическому случаю в вопросе.
Как вы можете видеть ниже, генератор "yield" (первый случай) имеет три дополнительные инструкции в настройке, но из первого FOR_ITER
они отличаются только одним уважением: подход "yield" использует a LOAD_FAST
вместо LOAD_DEREF
внутри цикла. LOAD_DEREF
"более медленный" , чем LOAD_FAST
, поэтому он делает версию "выход" немного быстрее, чем выражение генератора достаточно велико значения x
(внешний цикл), потому что значение y
загружается немного быстрее на каждом проходе. Для меньших значений x
он будет немного медленнее из-за дополнительных накладных расходов на установочный код.
Также может быть целесообразно указать, что выражение генератора обычно используется внутри кода, а не обертывает его такой функцией. Это позволило бы немного снизить накладные расходы на установку и немного ускорить выражение генератора для меньших значений цикла, даже если LOAD_FAST
предоставило преимущество "yield" в противном случае.
В любом случае разница в производительности была бы достаточной, чтобы оправдать решение между тем или иным. Читаемость рассчитывается гораздо больше, поэтому используйте то, что кажется наиболее читаемым для ситуации.
>>> def Generator(x, y):
... for i in xrange(x):
... for j in xrange(y):
... yield(i, j)
...
>>> dis.dis(Generator)
2 0 SETUP_LOOP 54 (to 57)
3 LOAD_GLOBAL 0 (xrange)
6 LOAD_FAST 0 (x)
9 CALL_FUNCTION 1
12 GET_ITER
>> 13 FOR_ITER 40 (to 56)
16 STORE_FAST 2 (i)
3 19 SETUP_LOOP 31 (to 53)
22 LOAD_GLOBAL 0 (xrange)
25 LOAD_FAST 1 (y)
28 CALL_FUNCTION 1
31 GET_ITER
>> 32 FOR_ITER 17 (to 52)
35 STORE_FAST 3 (j)
4 38 LOAD_FAST 2 (i)
41 LOAD_FAST 3 (j)
44 BUILD_TUPLE 2
47 YIELD_VALUE
48 POP_TOP
49 JUMP_ABSOLUTE 32
>> 52 POP_BLOCK
>> 53 JUMP_ABSOLUTE 13
>> 56 POP_BLOCK
>> 57 LOAD_CONST 0 (None)
60 RETURN_VALUE
>>> def Generator_expr(x, y):
... return ((i, j) for i in xrange(x) for j in xrange(y))
...
>>> dis.dis(Generator_expr.func_code.co_consts[1])
2 0 SETUP_LOOP 47 (to 50)
3 LOAD_FAST 0 (.0)
>> 6 FOR_ITER 40 (to 49)
9 STORE_FAST 1 (i)
12 SETUP_LOOP 31 (to 46)
15 LOAD_GLOBAL 0 (xrange)
18 LOAD_DEREF 0 (y)
21 CALL_FUNCTION 1
24 GET_ITER
>> 25 FOR_ITER 17 (to 45)
28 STORE_FAST 2 (j)
31 LOAD_FAST 1 (i)
34 LOAD_FAST 2 (j)
37 BUILD_TUPLE 2
40 YIELD_VALUE
41 POP_TOP
42 JUMP_ABSOLUTE 25
>> 45 POP_BLOCK
>> 46 JUMP_ABSOLUTE 6
>> 49 POP_BLOCK
>> 50 LOAD_CONST 0 (None)
53 RETURN_VALUE
Ответ 2
В этом примере это не так. Но yield
может использоваться для более сложных конструкций - например, он также может принимать значения от вызывающего и изменять поток в результате. Подробнее читайте PEP 342 (это интересная техника, которая стоит знать).
В любом случае, лучший совет использовать все, что более понятно для ваших нужд.
P.S. Вот простой пример coroutine из Дэйв Безли:
def grep(pattern):
print "Looking for %s" % pattern
while True:
line = (yield)
if pattern in line:
print line,
# Example use
if __name__ == '__main__':
g = grep("python")
g.next()
g.send("Yeah, but no, but yeah, but no")
g.send("A series of tubes")
g.send("python generators rock!")
Ответ 3
Нет никакой разницы для типа простых циклов, которые вы можете вставить в выражение генератора. Однако выход можно использовать для создания генераторов, которые выполняют гораздо более сложную обработку. Вот простой пример для генерации последовательности фибоначчи:
>>> def fibgen():
... a = b = 1
... while 1:
... yield a
... a, b = b, a+b
>>> list(itertools.takewhile((lambda x: x<100), fibgen()))
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
Ответ 4
Использование yield
приятно, если выражение более сложное, чем просто вложенные циклы. Среди прочего вы можете вернуть специальное первое или специальное последнее значение. Рассмотрим:
def Generator(x):
for i in xrange(x):
yield(i)
yield(None)
Ответ 5
В использовании обратите внимание на различие между объектом генератора и функцией генератора.
Объект-генератор используется только один раз, в отличие от функции-генератора, который может быть повторно использован каждый раз, когда вы вызываете его снова, потому что он возвращает новый объект-генератор.
Обычно выражения генератора обычно используются "raw", не обертывая их функцией, и возвращают объект-генератор.
например:.
def range_10_gen_func():
x = 0
while x < 10:
yield x
x = x + 1
print(list(range_10_gen_func()))
print(list(range_10_gen_func()))
print(list(range_10_gen_func()))
который выводит:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Сравните со слегка другим использованием:
range_10_gen = range_10_gen_func()
print(list(range_10_gen))
print(list(range_10_gen))
print(list(range_10_gen))
который выводит:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[]
[]
И сравните с выражением генератора:
range_10_gen_expr = (x for x in range(10))
print(list(range_10_gen_expr))
print(list(range_10_gen_expr))
print(list(range_10_gen_expr))
который также выводит:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[]
[]
Ответ 6
Когда мы думаем об итераторах, модуль itertools
:
... стандартизирует базовый набор быстрых, эффективных по памяти инструментов, которые полезны сами по себе или в сочетании. Вместе они образуют "итерационную алгебру", что позволяет создавать специализированные инструменты лаконично и эффективно в чистом Python.
Для производительности рассмотрим itertools.product(*iterables[, repeat])
Декартово произведение входных итераций.
Эквивалентен вложенным циклам в выражении генератора. Например, product(A, B)
возвращает то же самое, что ((x,y) for x in A for y in B)
.
>>> import itertools
>>> def gen(x,y):
... return itertools.product(xrange(x),xrange(y))
...
>>> [t for t in gen(3,2)]
[(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1)]
>>>
Ответ 7
Да, есть разница.
Для выражения генератора (x for var in expr)
, iter(expr)
вызывается, когда выражение создано.
При использовании def
и yield
для создания генератора, как в:
def my_generator():
for var in expr:
yield x
g = my_generator()
iter(expr)
еще не вызван. Он будет вызываться только при повторении на g
(и вообще не может быть вызван).
Взятие этого итератора в качестве примера:
from __future__ import print_function
class CountDown(object):
def __init__(self, n):
self.n = n
def __iter__(self):
print("ITER")
return self
def __next__(self):
if self.n == 0:
raise StopIteration()
self.n -= 1
return self.n
next = __next__ # for python2
Этот код:
g1 = (i ** 2 for i in CountDown(3)) # immediately prints "ITER"
print("Go!")
for x in g1:
print(x)
а
def my_generator():
for i in CountDown(3):
yield i ** 2
g2 = my_generator()
print("Go!")
for x in g2: # "ITER" is only printed here
print(x)
Поскольку большинство итераторов не делают много всего в __iter__
, легко пропустить это поведение. Пример реального мира - Django QuerySet
, который извлекает данные в __iter__
и data = (f(x) for x in qs)
может занять много времени, в то время как def g(): for x in qs: yield f(x)
, за которым следует data=g()
, немедленно вернется.
Для получения дополнительной информации и формального определения см. PEP 289 - Выражения генератора.
Ответ 8
Есть разница, которая может быть важна в некоторых контекстах, которые еще не были отмечены. Использование yield
не позволяет использовать return
для чего-то еще, чем неявно поднимая StopIteration (и связанные с сопрограммой вещи).
Это означает, что этот код плохо сформирован (и подача его интерпретатору даст вам AttributeError
):
class Tea:
"""With a cloud of milk, please"""
def __init__(self, temperature):
self.temperature = temperature
def mary_poppins_purse(tea_time=False):
"""I would like to make one thing clear: I never explain anything."""
if tea_time:
return Tea(355)
else:
for item in ['lamp', 'mirror', 'coat rack', 'tape measure', 'ficus']:
yield item
print(mary_poppins_purse(True).temperature)
С другой стороны, этот код работает как шарм:
class Tea:
"""With a cloud of milk, please"""
def __init__(self, temperature):
self.temperature = temperature
def mary_poppins_purse(tea_time=False):
"""I would like to make one thing clear: I never explain anything."""
if tea_time:
return Tea(355)
else:
return (item for item in ['lamp', 'mirror', 'coat rack',
'tape measure', 'ficus'])
print(mary_poppins_purse(True).temperature)