Python: wyrażenie generatora a wydajność

W Pythonie, czy istnieje jakakolwiek różnica między tworzeniem obiektu generatora poprzez wyrażenie generatora a używaniem instrukcji yield?

Użycie wydajność :

def Generator(x, y):
    for i in xrange(x):
        for j in xrange(y):
            yield(i, j)

Użycie wyrażenia generatora :

def Generator(x, y):
    return ((i, j) for i in xrange(x) for j in xrange(y))

Obie funkcje zwracają obiekty generatora, które wytwarzają krotki, np. (0,0), (0,1) itd.

Jakieś zalety jednego lub drugiego? Myśli?


Dzięki wszystkim! Jest wiele świetnych informacji i kolejne odniesienia w tych odpowiedziach!
Author: cschol, 2010-01-03

8 answers

Istnieją tylko niewielkie różnice w tych dwóch. Możesz użyć modułu dis do samodzielnego zbadania tego typu rzeczy.

Edit: moja pierwsza wersja dekompilowała wyrażenie generatora utworzone w module-scope w interaktywnym monicie. To nieco różni się od wersji OP z używanym wewnątrz funkcji. Zmodyfikowałem to tak, aby pasowało do konkretnej sprawy w pytaniu.

Jak widać poniżej, generator "plon" (pierwszy przypadek) ma trzy dodatkowe instrukcje w konfiguracji, ale od pierwszej FOR_ITER różnią się tylko pod jednym względem: podejście "yield" używa LOAD_FAST zamiast LOAD_DEREF wewnątrz pętli. LOAD_DEREF jest "raczej wolniejszym" niż LOAD_FAST, więc sprawia, że wersja "yield" jest nieco szybsza niż wyrażenie generatora dla wystarczająco dużych wartości x (pętli zewnętrznej), ponieważ wartość y jest ładowana nieco szybciej na każdym przejściu. Dla mniejszych wartości x byłoby to nieco wolniejsze ze względu na dodatkowe obciążenie kod konfiguracji.

Warto również zwrócić uwagę, że wyrażenie generatora zwykle będzie używane w kodzie w linii prostej, zamiast owijać je taką funkcją. To usunęłoby trochę z góry konfiguracji i utrzymałoby wyrażenie generatora nieco szybciej dla mniejszych wartości pętli, nawet jeśli LOAD_FAST dało wersję "yield" przewagę w przeciwnym razie.

W żadnym przypadku różnica w wydajności nie byłaby wystarczająca, aby uzasadnić podjęcie decyzji między jednym a drugim. Czytelność liczy się znacznie więcej, więc używaj tego, co jest najbardziej czytelne w danej sytuacji.

>>> 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
 68
Author: Peter Hansen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-03 18:22:10

W tym przykładzie nie do końca. Ale yield może być używany dla bardziej złożonych konstrukcji - na przykład może również przyjmować wartości z wywołującego i modyfikować przepływ w wyniku. Przeczytaj PEP 342 Po Więcej Szczegółów (to ciekawa technika, którą warto znać).

W każdym razie, najlepszą radą jest użyj tego, co jest jaśniejsze dla Twoich potrzeb.

P. S. Oto prosty przykład z Dave Beazley :

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!")
 35
Author: Eli Bendersky,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-03 16:18:36

Nie ma różnicy w rodzaju prostych pętli, które można zmieścić w wyrażeniu generatora. Jednak wydajność może być wykorzystana do tworzenia generatorów, które wykonują znacznie bardziej złożone przetwarzanie. Oto prosty przykład generowania ciągu Fibonacciego:

>>> 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]
 17
Author: Dave Kirby,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-03 16:30:39

Użycie yield jest przyjemne, jeśli wyrażenie jest bardziej skomplikowane niż tylko zagnieżdżone pętle. Między innymi możesz zwrócić specjalną pierwszą lub specjalną ostatnią wartość. Consider:

def Generator(x):
  for i in xrange(x):
    yield(i)
  yield(None)
 8
Author: Tor Valamo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-03 16:13:13

W użyciu należy zauważyć rozróżnienie między obiektem generatora a funkcją generatora.

Obiekt generatora jest use-once-only, W przeciwieństwie do funkcji generatora, która może być ponownie użyta za każdym razem, gdy zostanie wywołana ponownie, ponieważ zwraca nowy obiekt generatora.

Wyrażenia generatora są w praktyce zwykle używane "raw", bez zawijania ich w funkcję i zwracają obiekt generatora.

Np.:

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()))

Które wyjście:

[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]

Porównaj z nieco inne zastosowanie:

range_10_gen = range_10_gen_func()
print(list(range_10_gen))
print(list(range_10_gen))
print(list(range_10_gen))

Które wyjście:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[]
[]

I porównaj z wyrażeniem generatora:

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))

, które również wyprowadza:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[]
[]
 8
Author: Craig McQueen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-12 06:45:23

Myśląc o iteratorach, moduł itertools:

... standaryzuje podstawowy zestaw szybkich, wydajnych pamięci narzędzi, które są przydatne same lub w połączeniu. Razem tworzą "algebrę iteratora", dzięki czemu możliwe jest zwięzłe i wydajne konstruowanie specjalistycznych narzędzi w czystym Pythonie.

Dla wydajności, rozważ itertools.product(*iterables[, repeat])

Iloczyn kartezjański iterabli wejściowych.

Odpowiednik zagnieżdżonych pętli for-loops w generatorze ekspresja. Na przykład, product(A, B) zwraca to samo co ((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)]
>>> 
 5
Author: gimel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-03 16:21:44

Tak, jest różnica.

Dla wyrażenia generatora(x for var in expr), iter(expr) jest wywoływane, gdy wyrażenie jest utworzone .

Przy użyciu def i yield do utworzenia generatora, jak w:

def my_generator():
    for var in expr:
        yield x

g = my_generator()

iter(expr) nie został jeszcze wezwany. Zostanie wywołana tylko podczas iteracji na g (i może nie być wywołana w ogóle).

Na przykład ten iterator:
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

Ten kod:

g1 = (i ** 2 for i in CountDown(3))  # immediately prints "ITER"
print("Go!")
for x in g1:
    print(x)

While:

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)

Ponieważ większość iteratorów nie wiele rzeczy w __iter__, łatwo jest przegapić takie zachowanie. Prawdziwym przykładem może być QuerySet Django, które pobiera dane w __iter__ i data = (f(x) for x in qs) może zająć dużo czasu, podczas gdy def g(): for x in qs: yield f(x) a następnie data=g() powróci natychmiast.

Więcej informacji i formalna definicja znajdują się w PEP 289 -- wyrażenia generatora.

 2
Author: Udi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-17 23:10:21

Istnieje różnica, która może być ważna w niektórych kontekstach, które jeszcze nie zostały wskazane. Używanie yield zapobiega używaniu return do czegoś innego niż w domyśle podnoszenie Stopiteracji (i rzeczy związanych z koroutinami).

Oznacza to, że ten kod jest źle uformowany (a podanie go interpreterowi da ci 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)

Z drugiej strony, ten kod działa jak urok:

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)
 0
Author: Adrien,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:47:19