Python: выражение генератора против yield

90

В Python есть ли разница между созданием объекта- генератора с помощью выражения генератора и использованием оператора yield ?

Использование 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) и т. Д.

Какие преимущества того или другого? Мысли?


Спасибо всем! В этих ответах много полезной информации и дополнительных ссылок!

cschol
источник
2
Выберите тот, который вы считаете наиболее читаемым.
user238424 03

Ответы:

74

Между ними есть лишь небольшие различия. Вы можете использовать этот disмодуль, чтобы самостоятельно изучить подобные вещи.

Изменить: Моя первая версия декомпилировала выражение генератора, созданное в области модуля в интерактивной подсказке. Это немного отличается от версии OP, поскольку она используется внутри функции. Я изменил это, чтобы соответствовать фактическому случаю в вопросе.

Как вы можете видеть ниже, генератор «yield» (первый случай) имеет три дополнительных инструкции в настройке, но с первого FOR_ITERраза они отличаются только в одном отношении: подход «yield» использует внутри цикла LOAD_FASTвместо a 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
Питер Хансен
источник
Принято - для подробного объяснения разницы с помощью дис. Благодаря!
cschol 03
Я обновил, чтобы включить ссылку на источник, который утверждает, что LOAD_DEREFон «довольно медленнее», поэтому, если производительность действительно имеет значение, timeitбыло бы неплохо использовать реальное время . Теоретический анализ идет только до этого момента.
Питер Хансен
36

В этом примере нет. Но yieldможет использоваться для более сложных конструкций - например, он также может принимать значения от вызывающего объекта и в результате изменять поток. Прочтите PEP 342 для получения более подробной информации (это интересный метод, который стоит знать).

В любом случае, лучший совет - используйте то, что более четко соответствует вашим потребностям .

PS Вот простой пример сопрограммы от Дэйва Бизли :

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!")
Эли Бендерский
источник
8
+1 за ссылку на Дэвида Бизли. Его презентация о сопрограммах - самая умопомрачительная вещь, которую я читал за долгое время. Возможно, не так полезно, как его презентация о генераторах, но тем не менее потрясающе.
Роберт Россни 03
18

Нет никакой разницы в том, какие простые циклы можно вписать в выражение генератора. Однако yield можно использовать для создания генераторов, которые выполняют гораздо более сложную обработку. Вот простой пример создания последовательности Фибоначчи:

>>> def fibgen():
...    a = b = 1
...    while True:
...        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]
Дэйв Кирби
источник
5
+1 это супер круто ... не могу сказать, что когда-либо видел такую ​​короткую и милую реализацию выдумки без рекурсии.
JudoWill 03
Обманчиво простой фрагмент кода - я думаю, Фибоначчи будет счастлив его увидеть !!
user-asterix
10

Обратите внимание на различие между объектом-генератором и функцией-генератором.

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

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

Например:

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]
[]
[]
Крейг МакКуин
источник
8

Использование yieldудобно, если выражение сложнее, чем просто вложенные циклы. Помимо прочего, вы можете вернуть особое первое или особое последнее значение. Рассматривать:

def Generator(x):
  for i in xrange(x):
    yield(i)
  yield(None)
Тор Валамо
источник
5

Размышляя об итераторах, itertoolsмодуль:

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

Для производительности рассмотрите itertools.product(*iterables[, repeat])

Декартово произведение входных итераций.

Эквивалентен вложенным циклам for в выражении генератора. Например, 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)]
>>> 
гимель
источник
4

Да, разница есть.

Для выражения генератора (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 - Выражения генератора .

Уди
источник
0

Есть разница, которая может быть важной в некоторых контекстах, на которую еще не указывалось. Использование не 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)
Адриан
источник