Python: Generatorausdruck vs. Ertrag


90

Gibt es in Python einen Unterschied zwischen dem Erstellen eines Generatorobjekts über einen Generatorausdruck und der Verwendung der Yield- Anweisung?

Mit Ausbeute :

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

Mit Generator Ausdruck :

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

Beide Funktionen geben Generatorobjekte zurück, die Tupel erzeugen, z. B. (0,0), (0,1) usw.

Irgendwelche Vorteile des einen oder anderen? Gedanken?


Danke an alle! Diese Antworten enthalten viele großartige Informationen und weitere Referenzen!


2
Wählen Sie diejenige aus, die Sie am besten lesen können.
user238424

Antworten:


74

Es gibt nur geringfügige Unterschiede zwischen den beiden. Sie können das disModul verwenden, um solche Dinge selbst zu untersuchen.

Bearbeiten: Meine erste Version hat den im Modulbereich in der interaktiven Eingabeaufforderung erstellten Generatorausdruck dekompiliert. Das unterscheidet sich geringfügig von der OP-Version, die in einer Funktion verwendet wird. Ich habe dies geändert, um es dem tatsächlichen Fall in der Frage anzupassen.

Wie Sie unten sehen können, enthält der "Yield" -Generator (erster Fall) drei zusätzliche Anweisungen im Setup, die sich jedoch von der ersten FOR_ITERnur in einer Hinsicht unterscheiden: Der "Yield" -Ansatz verwendet ein LOAD_FASTanstelle eines LOAD_DEREFinnerhalb der Schleife. Das LOAD_DEREFist "eher langsamer" als LOAD_FAST, daher ist die "Yield" -Version etwas schneller als der Generatorausdruck für ausreichend große Werte von x(der äußeren Schleife), da der Wert von ybei jedem Durchgang etwas schneller geladen wird. Bei kleineren Werten xwäre dies aufgrund des zusätzlichen Overheads des Setup-Codes etwas langsamer.

Es kann auch erwähnenswert sein, dass der Generatorausdruck normalerweise inline im Code verwendet wird, anstatt ihn mit dieser Funktion zu umschließen. Dies würde den Setup-Aufwand etwas verringern und den Generatorausdruck für kleinere Schleifenwerte etwas schneller halten, selbst wenn LOAD_FASTdie "Yield" -Version ansonsten einen Vorteil hätte.

In keinem Fall würde der Leistungsunterschied ausreichen, um eine Entscheidung zwischen dem einen oder anderen zu rechtfertigen. Die Lesbarkeit zählt weitaus mehr. Verwenden Sie daher diejenige, die sich für die jeweilige Situation am besten lesbar anfühlt.

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

Akzeptiert - zur detaillierten Erklärung des Unterschieds mit dis. Vielen Dank!
Cschol

Ich habe aktualisiert, um einen Link zu einer Quelle aufzunehmen, die behauptet, dass sie LOAD_DEREF"eher langsamer" ist. Wenn also die Leistung wirklich wichtig timeitwäre, wäre ein echtes Timing mit gut. Eine theoretische Analyse geht nur so weit.
Peter Hansen

36

In diesem Beispiel nicht wirklich. Aber yieldkann für komplexere Konstrukte verwendet werden - zum Beispiel als auch Werte aus den Anrufern annehmen kann und die Strömung als Ergebnis ändern. Lesen Sie PEP 342 für weitere Details (es ist eine interessante Technik, die Sie kennen sollten).

Wie auch immer, der beste Rat ist , alles zu verwenden, was für Ihre Bedürfnisse klarer ist .

PS Hier ist ein einfaches Coroutine-Beispiel von 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!")

8
+1 für die Verknüpfung mit David Beazley. Sein Vortrag über Coroutinen ist das umwerfendste, was ich seit langer Zeit gelesen habe. Vielleicht nicht so nützlich wie seine Präsentation über Generatoren, aber dennoch erstaunlich.
Robert Rossney

18

Es gibt keinen Unterschied für die Art der einfachen Schleifen, die Sie in einen Generatorausdruck einpassen können. Der Ertrag kann jedoch verwendet werden, um Generatoren zu erstellen, die eine viel komplexere Verarbeitung durchführen. Hier ist ein einfaches Beispiel für die Erzeugung der Fibonacci-Sequenz:

>>> 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 das ist super cool ... kann nicht sagen, dass ich jemals eine so kurze und süße Fib-Implementierung ohne Rekursion gesehen habe.
JudoWill

Täuschend einfaches Code-Snippet - ich denke, Fibonacci wird sich freuen, es zu sehen !!
User-Asterix

10

Beachten Sie bei der Verwendung einen Unterschied zwischen einem Generatorobjekt und einer Generatorfunktion.

Ein Generatorobjekt kann nur einmal verwendet werden, im Gegensatz zu einer Generatorfunktion, die bei jedem erneuten Aufruf wiederverwendet werden kann, da ein neues Generatorobjekt zurückgegeben wird.

Generatorausdrücke werden in der Praxis normalerweise "roh" verwendet, ohne sie in eine Funktion einzuschließen, und sie geben ein Generatorobjekt zurück.

Z.B:

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

welche Ausgänge:

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

Vergleichen Sie mit einer etwas anderen Verwendung:

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

welche Ausgänge:

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

Und vergleiche mit einem Generatorausdruck:

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

welches auch ausgibt:

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

8

Die Verwendung yieldist hilfreich, wenn der Ausdruck komplizierter ist als nur verschachtelte Schleifen. Unter anderem können Sie einen speziellen ersten oder speziellen letzten Wert zurückgeben. Erwägen:

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

5

Wenn Sie an Iteratoren denken, ist das itertoolsModul:

... standardisiert einen Kernsatz schneller, speichereffizienter Tools, die für sich oder in Kombination nützlich sind. Zusammen bilden sie eine „Iteratoralgebra“, die es ermöglicht, spezielle Tools in reinem Python kurz und effizient zu erstellen.

Berücksichtigen Sie für die Leistung itertools.product(*iterables[, repeat])

Kartesisches Produkt von iterativen Eingaben.

Entspricht verschachtelten for-Schleifen in einem Generatorausdruck. Gibt beispielsweise product(A, B)dasselbe zurück wie ((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

Ja, da gibt es einen Unterschied.

Für den Generator Ausdruck (x for var in expr), iter(expr)aufgerufen wird , wenn der Ausdruck erstellt .

Bei Verwendung defund yieldzum Erstellen eines Generators wie in:

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

g = my_generator()

iter(expr)wird noch nicht aufgerufen. Es wird nur beim Iterieren aufgerufen g(und möglicherweise überhaupt nicht aufgerufen).

Am Beispiel dieses Iterators:

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

Dieser Code:

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

während:

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)

Da die meisten Iteratoren nicht viel tun __iter__, ist es leicht, dieses Verhalten zu übersehen. Ein Beispiel aus der realen Welt wäre Django QuerySet, das Daten abruft__iter__ und data = (f(x) for x in qs)viel Zeit in Anspruch nehmen kann, def g(): for x in qs: yield f(x)gefolgt von data=g()einer sofortigen Rückkehr.

Weitere Informationen und die formale Definition finden Sie in PEP 289 - Generatorausdrücke .


0

Es gibt einen Unterschied, der in einigen Kontexten wichtig sein könnte, auf die noch nicht hingewiesen wurde. Die Verwendung yieldverhindert, dass Sie returnfür etwas anderes als das implizite Erhöhen von StopIteration (und Coroutinen-bezogenen Dingen) verwenden .

Dies bedeutet, dass dieser Code falsch geformt ist (und wenn Sie ihn einem Dolmetscher zuführen, erhalten Sie einen 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)

Auf der anderen Seite funktioniert dieser Code wie ein Zauber:

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)
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.