Python: вираз генератора проти прибутковості


90

У 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) тощо.

Якісь переваги того чи іншого? Думки?


Дякую всім! У цих відповідях є багато чудової інформації та подальших посилань!


2
Виберіть той, який вам здається найбільш читабельним.
user238424

Відповіді:


74

У цих двох лише незначні відмінності. Ви можете використовувати disмодуль, щоб перевірити подібні речі на власні очі.

Редагувати: Моя перша версія декомпілювала вираз генератора, створений у модулі-області дії в інтерактивному запиті. Це трохи відрізняється від версії OP, оскільки вона використовується всередині функції. Я змінив це відповідно до фактичного випадку у питанні.

Як ви можете бачити нижче, генератор "yield" (перший випадок) має три додаткові інструкції в налаштуванні, але від першого FOR_ITERвони відрізняються лише в одному відношенні: "yield" підхід використовує 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

Прийнято - для детального пояснення різниці за допомогою dis. Дякую!
cschol

Я оновив, включивши посилання на джерело, яке стверджує, що LOAD_DEREF"досить повільніше", тому, якби продуктивність дійсно мала значення, деякі реальні терміни timeitбули б хорошими. Теоретичний аналіз йде лише дотепер.
Peter Hansen

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 за посилання на Девіда Бізлі. Його презентація про спільні програми - це найдивовижніше, що я читав за довгий час. Можливо, не такий корисний, як його презентація про генератори, але тим не менш дивовижний.
Роберт Росней,

18

Немає різниці у типі простих циклів, які можна вмістити у вираз генератора. Однак вихід можна використовувати для створення генераторів, які роблять набагато складнішу обробку. Ось простий приклад для генерації послідовності Фібоначчі:

>>> 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, це супер круто ... не можу сказати, що я коли-небудь бачив таку коротку і солодку реалізацію fib без рекурсії.
JudoWill

Обманливо простий фрагмент коду - я думаю, що Фібоначчі буде радий бачити його !!
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)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.