Python: espressione del generatore e rendimento


90

In Python, c'è qualche differenza tra la creazione di un oggetto generatore tramite un'espressione generatore e l'utilizzo dell'istruzione yield ?

Utilizzando la resa :

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

Utilizzando l' espressione del generatore :

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

Entrambe le funzioni restituiscono oggetti generatori, che producono tuple, ad esempio (0,0), (0,1) ecc.

Qualche vantaggio dell'uno o dell'altro? Pensieri?


Grazie a tutti! Ci sono molte ottime informazioni e ulteriori riferimenti in queste risposte!


2
Scegli quello che trovi più leggibile.
user238424

Risposte:


74

Ci sono solo lievi differenze tra i due. Puoi usare il dismodulo per esaminare questo genere di cose da solo.

Modifica: la mia prima versione ha decompilato l'espressione del generatore creata nell'ambito del modulo nel prompt interattivo. È leggermente diverso dalla versione dell'OP con esso utilizzato all'interno di una funzione. L'ho modificato per adattarlo al caso reale nella domanda.

Come puoi vedere sotto, il generatore "yield" (primo caso) ha tre istruzioni extra nel setup, ma dal primo FOR_ITERdifferiscono solo per un aspetto: l'approccio "yield" utilizza a LOAD_FASTal posto di a LOAD_DEREFall'interno del ciclo. Il LOAD_DEREFè "più lenta" che LOAD_FAST, in modo che rende leggermente più veloce rispetto al generatore di espressione per grandi valori abbastanza della versione "yield" x(l'anello esterno) perché il valore yviene caricato leggermente più veloce ad ogni passaggio. Per valori più piccoli xsarebbe leggermente più lento a causa dell'overhead aggiuntivo del codice di installazione.

Potrebbe anche valere la pena sottolineare che l'espressione del generatore viene solitamente utilizzata inline nel codice, piuttosto che racchiuderla con la funzione in questo modo. Ciò rimuoverà un po 'dell'overhead di configurazione e manterrebbe l'espressione del generatore leggermente più veloce per valori di loop più piccoli, anche se LOAD_FASTaltrimenti avrebbe dato alla versione "yield" un vantaggio.

In nessuno dei due casi la differenza di prestazioni sarebbe sufficiente a giustificare la decisione tra l'uno o l'altro. La leggibilità conta molto di più, quindi usa quello che ti sembra più leggibile per la situazione in questione.

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

Accettato - per la spiegazione dettagliata della differenza utilizzando dis. Grazie!
cschol

Ho aggiornato per includere un collegamento a una fonte che afferma che LOAD_DEREFè "piuttosto più lenta", quindi se le prestazioni fossero davvero importanti, un tempismo reale timeitsarebbe buono. Un'analisi teorica va solo fin qui.
Peter Hansen

36

In questo esempio, non proprio. Ma yieldpuò essere utilizzato per costrutti più complessi, ad esempio può accettare valori anche dal chiamante e modificare il flusso di conseguenza. Leggi PEP 342 per maggiori dettagli (è una tecnica interessante che vale la pena conoscere).

Ad ogni modo, il miglior consiglio è usare tutto ciò che è più chiaro per le tue esigenze .

PS Ecco un semplice esempio di coroutine di 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 per il collegamento a David Beazley. La sua presentazione sulle coroutine è la cosa più strabiliante che abbia letto da molto tempo. Non così utile, forse, come la sua presentazione sui generatori, ma comunque sorprendente.
Robert Rossney

18

Non c'è differenza per il tipo di loop semplici che puoi inserire in un'espressione del generatore. Tuttavia, la resa può essere utilizzata per creare generatori che eseguono elaborazioni molto più complesse. Ecco un semplice esempio per generare la sequenza di Fibonacci:

>>> 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 è fantastico ... non posso dire di aver mai visto un'implementazione di bug così breve e dolce senza ricorsione.
JudoWill

Snippet di codice ingannevolmente semplice - penso che Fibonacci sarà felice di vederlo !!
user-asterix

10

Nell'uso, notare una distinzione tra un oggetto generatore e una funzione generatore.

Un oggetto generatore è utilizzabile una sola volta, a differenza di una funzione generatore, che può essere riutilizzata ogni volta che lo si richiama, perché restituisce un nuovo oggetto generatore.

Le espressioni del generatore sono in pratica usate solitamente "grezze", senza avvolgerle in una funzione, e restituiscono un oggetto generatore.

Per esempio:

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

quali uscite:

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

Confronta con un utilizzo leggermente diverso:

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

quali uscite:

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

E confronta con un'espressione del generatore:

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

che produce anche:

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

8

L'utilizzo yieldè utile se l'espressione è più complicata dei semplici cicli annidati. Tra le altre cose puoi restituire un primo o un ultimo valore speciale. Tener conto di:

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

5

Quando si pensa agli iteratori, il itertoolsmodulo:

... standardizza un set di base di strumenti veloci ed efficienti in termini di memoria che sono utili da soli o in combinazione. Insieme, formano un '"algebra iteratore" che rende possibile costruire strumenti specializzati in modo succinto ed efficiente in puro Python.

Per le prestazioni, considera itertools.product(*iterables[, repeat])

Prodotto cartesiano di iterabili di input.

Equivalente a cicli for annidati in un'espressione del generatore. Ad esempio, product(A, B)restituisce lo stesso di ((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

Sì, c'è differenza.

Per l'espressione generatore (x for var in expr), iter(expr)viene chiamato quando viene creata l'espressione .

Quando si utilizza defe yieldper creare un generatore, come in:

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

g = my_generator()

iter(expr)non è ancora stato chiamato. Verrà chiamato solo durante l'iterazione g(e potrebbe non essere chiamato affatto).

Prendendo questo iteratore come esempio:

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

Questo codice:

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

mentre:

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)

Poiché la maggior parte degli iteratori non fa molte cose __iter__, è facile perdere questo comportamento. Un esempio del mondo reale potrebbe essere Django QuerySet, che recupera i dati__iter__ e data = (f(x) for x in qs)potrebbe richiedere molto tempo, mentre def g(): for x in qs: yield f(x)seguito da data=g()tornerebbe immediatamente.

Per maggiori informazioni e la definizione formale fare riferimento a PEP 289 - Generator Expressions .


0

C'è una differenza che potrebbe essere importante in alcuni contesti che non è stata ancora evidenziata. L'utilizzo yieldti impedisce di utilizzare returnper qualcos'altro che sollevare implicitamente StopIteration (e cose correlate alle coroutine) .

Ciò significa che questo codice è mal formato (e fornirlo a un interprete ti darà un 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)

D'altra parte, questo codice funziona come un fascino:

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)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.