Python: generator-expressie vs. opbrengst

Is er in Python een verschil tussen het maken van een generatorobject via een generatorexpressieen het gebruik van de yield-instructie?

rendementgebruiken:

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

Gebruik generatoruitdrukking:

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

Beide functies retourneren generatorobjecten, die tuples produceren, b.v. (0,0), (0,1) enz.

Enige voordelen van het een of het ander? Gedachten?


Antwoord 1, autoriteit 100%

Er zijn slechts kleine verschillen tussen de twee. U kunt de module disgebruiken om dit soort dingen zelf te onderzoeken.

Bewerken:mijn eerste versie decompileerde de generator-expressie die was gemaakt op module-scope in de interactieve prompt. Dat is iets anders dan de OP-versie waarin het in een functie wordt gebruikt. Ik heb dit aangepast zodat het overeenkomt met het werkelijke geval in de vraag.

Zoals je hieronder kunt zien, heeft de “yield”-generator (eerste geval) drie extra instructies in de setup, maar van de eerste FOR_ITERverschillen ze in slechts één opzicht: de “yield”-benadering gebruikt een LOAD_FASTin plaats van een LOAD_DEREFin de lus. De LOAD_DEREFis “vrij langzamer”dan LOAD_FAST, dus het maakt de “yield”-versie iets sneller dan de generatorexpressie voor waarden die groot genoeg zijn voor x(de buitenste lus) omdat de waarde van ywordt bij elke pas iets sneller geladen. Voor kleinere waarden van xzou het iets langzamer zijn vanwege de extra overhead van de setup-code.

Het is misschien ook de moeite waard om erop te wijzen dat de generator-expressie meestal inline in de code wordt gebruikt, in plaats van deze zo met de functie in te pakken. Dat zou een beetje van de setup-overhead wegnemen en de generator-expressie iets sneller houden voor kleinere luswaarden, zelfs als LOAD_FASTde “yield”-versie anders een voordeel zou geven.

In geen van beide gevallen zou het prestatieverschil voldoende zijn om een ​​keuze tussen het een of het ander te rechtvaardigen. De leesbaarheid telt veel meer, dus gebruik wat het meest leesbaar is voor de betreffende situatie.

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

Antwoord 2, autoriteit 48%

In dit voorbeeld niet echt. Maar yieldkan worden gebruikt voor complexere constructies – bijvoorbeeldhet kan ook waarden van de beller accepteren en de stroom als resultaat wijzigen. Lees PEP 342voor meer details (het is een interessante techniek die het waard is om te weten).

Hoe dan ook, het beste advies is gebruik wat voor u duidelijker is.

P.S. Hier is een eenvoudig coroutinevoorbeeld van 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!")

Antwoord 3, autoriteit 25%

Er is geen verschil voor het soort eenvoudige lussen dat u in een generator-expressie kunt passen. Opbrengst kan echter worden gebruikt om generatoren te maken die veel complexere bewerkingen uitvoeren. Hier is een eenvoudig voorbeeld voor het genereren van de fibonacci-reeks:

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

Antwoord 4, autoriteit 13%

Let tijdens gebruik op een onderscheid tussen een generatorobject en een generatorfunctie.

Een generatorobject is eenmalig te gebruiken, in tegenstelling tot een generatorfunctie, die elke keer dat u het opnieuw aanroept opnieuw kan worden gebruikt, omdat het een nieuw generatorobject retourneert.

Generatorexpressies worden in de praktijk meestal “onbewerkt” gebruikt, zonder ze in een functie te stoppen, en ze retourneren een generatorobject.

Bijvoorbeeld:

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

die uitvoer:

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

Vergelijk met een iets ander gebruik:

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

die uitvoer:

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

En vergelijk met een generatoruitdrukking:

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

die ook uitvoert:

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

Antwoord 5, autoriteit 11%

Het gebruik van yieldis fijn als de expressie ingewikkelder is dan alleen geneste lussen. U kunt onder andere een bijzondere eerste of bijzondere laatste waarde teruggeven. Overweeg:

def Generator(x):
  for i in xrange(x):
    yield(i)
  yield(None)

Antwoord 6, autoriteit 9%

Ja, er is een verschil.

Voor de generatoruitdrukking (x for var in expr), wordt iter(expr)aangeroepen wanneer de uitdrukking gemaaktis.

Bij gebruik van defen yieldom een ​​generator te maken, zoals in:

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

iter(expr)is nog niet aangeroepen. Het wordt alleen aangeroepen bij iteratie op g(en wordt mogelijk helemaal niet aangeroepen).

Deze iterator als voorbeeld nemen:

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

Deze code:

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

terwijl:

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)

Omdat de meeste iterators niet veel dingen doen in __iter__, is het gemakkelijk om dit gedrag over het hoofd te zien. Een voorbeeld uit de praktijk is Django’s QuerySet, die het ophalen van gegevens in __iter__en data = (f(x) for x in qs)kan veel tijd kosten, terwijl def g(): for x in qs: yield f(x)gevolgd door data=g()onmiddellijk zou terugkeren.

Voor meer info en de formele definitie verwijzen we naar PEP 289 — Generator Expressions.


Antwoord 7, autoriteit 7%

Als je aan iterators denkt, de module itertools:

… standaardiseert een kernset van snelle, geheugenefficiënte tools die op zichzelf of in combinatie nuttig zijn. Samen vormen ze een “iteratoralgebra” die het mogelijk maakt om gespecialiseerde tools beknopt en efficiënt te construeren in pure Python.

Overweeg voor prestaties itertools.product(*iterables[, repeat])

Cartesiaans product van invoer-iterables.

Equivalent aan geneste for-lussen in een generator-expressie. product(A, B)geeft bijvoorbeeld hetzelfde terug als ((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)]
>>> 

Other episodes