python generator - Cosa fa la parola chiave "rendimento"?




15 Answers

Per capire che cosa yield , devi capire cosa sono i generatori . E prima che i generatori arrivino iterabili .

iterabili

Quando crei un elenco, puoi leggere i suoi elementi uno per uno. La lettura dei suoi articoli uno per uno è chiamata iterazione:

>>> mylist = [1, 2, 3]
>>> for i in mylist:
...    print(i)
1
2
3

mylist è un iterabile . Quando si utilizza una comprensione di lista, si crea una lista, e quindi un iterabile:

>>> mylist = [x*x for x in range(3)]
>>> for i in mylist:
...    print(i)
0
1
4

Tutto ciò che puoi usare " for... in... " è un iterable; lists , strings , file ...

Questi iterabili sono utili perché puoi leggerli quanto vuoi, ma memorizzi tutti i valori in memoria e questo non è sempre quello che vuoi quando hai molti valori.

generatori

I generatori sono iteratori, una sorta di iterabile che puoi solo scorrere una volta sola . I generatori non memorizzano tutti i valori in memoria, generano i valori al volo :

>>> mygenerator = (x*x for x in range(3))
>>> for i in mygenerator:
...    print(i)
0
1
4

È lo stesso eccetto che hai usato () invece di [] . MA, non è possibile eseguire for i in mygenerator una seconda volta poiché i generatori possono essere utilizzati solo una volta: calcolano 0, quindi dimenticano e calcolano 1 e terminano il calcolo 4, uno per uno.

dare la precedenza

yield è una parola chiave che viene utilizzata come return , tranne che la funzione restituirà un generatore.

>>> def createGenerator():
...    mylist = range(3)
...    for i in mylist:
...        yield i*i
...
>>> mygenerator = createGenerator() # create a generator
>>> print(mygenerator) # mygenerator is an object!
<generator object createGenerator at 0xb7555c34>
>>> for i in mygenerator:
...     print(i)
0
1
4

Qui è un esempio inutile, ma è utile quando si sa che la funzione restituirà un enorme set di valori che sarà necessario leggere solo una volta.

Per padroneggiare la yield , è necessario comprendere che quando si chiama la funzione, il codice che si è scritto nel corpo della funzione non viene eseguito. La funzione restituisce solo l'oggetto generatore, questo è un po 'complicato :-)

Quindi, il codice verrà eseguito ogni volta che for utilizza il generatore.

Ora la parte difficile:

La prima volta che for chiama l'oggetto generatore creato dalla funzione, eseguirà il codice nella funzione dall'inizio fino a quando non raggiunge il yield , quindi restituirà il primo valore del ciclo. Quindi, ogni altra chiamata eseguirà il ciclo che hai scritto nella funzione ancora una volta e restituirà il valore successivo, fino a quando non ci sarà alcun valore da restituire.

Il generatore è considerato vuoto una volta che la funzione è stata eseguita, ma non ha più raggiunto il yield . Può essere perché il ciclo era giunto al termine, o perché non si soddisfa più un "if/else" .

Il tuo codice spiegato

Generatore:

# Here you create the method of the node object that will return the generator
def _get_child_candidates(self, distance, min_dist, max_dist):

    # Here is the code that will be called each time you use the generator object:

    # If there is still a child of the node object on its left
    # AND if distance is ok, return the next child
    if self._leftchild and distance - max_dist < self._median:
        yield self._leftchild

    # If there is still a child of the node object on its right
    # AND if distance is ok, return the next child
    if self._rightchild and distance + max_dist >= self._median:
        yield self._rightchild

    # If the function arrives here, the generator will be considered empty
    # there is no more than two values: the left and the right children

Caller:

# Create an empty list and a list with the current object reference
result, candidates = list(), [self]

# Loop on candidates (they contain only one element at the beginning)
while candidates:

    # Get the last candidate and remove it from the list
    node = candidates.pop()

    # Get the distance between obj and the candidate
    distance = node._get_dist(obj)

    # If distance is ok, then you can fill the result
    if distance <= max_dist and distance >= min_dist:
        result.extend(node._values)

    # Add the children of the candidate in the candidates list
    # so the loop will keep running until it will have looked
    # at all the children of the children of the children, etc. of the candidate
    candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))

return result

Questo codice contiene diverse parti intelligenti:

  • Il ciclo scorre su una lista, ma l'elenco si espande mentre il ciclo viene iterato :-) È un modo conciso per passare attraverso tutti questi dati annidati anche se è un po 'pericoloso poiché si può finire con un ciclo infinito. In questo caso, candidates.extend(node._get_child_candidates(distance, min_dist, max_dist)) esaurisce tutti i valori del generatore, ma continua a creare nuovi oggetti generatore che produrranno valori diversi dai precedenti poiché non è applicato sullo stesso nodo.

  • Il metodo extend() è un metodo elenco oggetti che si aspetta un iterabile e aggiunge i suoi valori alla lista.

Di solito passiamo una lista ad esso:

>>> a = [1, 2]
>>> b = [3, 4]
>>> a.extend(b)
>>> print(a)
[1, 2, 3, 4]

Ma nel tuo codice viene generato un generatore, il che è positivo perché:

  1. Non è necessario leggere i valori due volte.
  2. Potresti avere molti bambini e non li vuoi tutti salvati in memoria.

E funziona perché a Python non interessa se l'argomento di un metodo è un elenco o meno. Python prevede iterabili, quindi funzionerà con stringhe, elenchi, tuple e generatori! Si chiama duck typing ed è una delle ragioni per cui Python è così cool. Ma questa è un'altra storia, per un'altra domanda ...

Puoi fermarti qui o leggere un po 'per vedere un uso avanzato di un generatore:

Controllo dell'esaurimento del generatore

>>> class Bank(): # Let's create a bank, building ATMs
...    crisis = False
...    def create_atm(self):
...        while not self.crisis:
...            yield "$100"
>>> hsbc = Bank() # When everything's ok the ATM gives you as much as you want
>>> corner_street_atm = hsbc.create_atm()
>>> print(corner_street_atm.next())
$100
>>> print(corner_street_atm.next())
$100
>>> print([corner_street_atm.next() for cash in range(5)])
['$100', '$100', '$100', '$100', '$100']
>>> hsbc.crisis = True # Crisis is coming, no more money!
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> wall_street_atm = hsbc.create_atm() # It's even true for new ATMs
>>> print(wall_street_atm.next())
<type 'exceptions.StopIteration'>
>>> hsbc.crisis = False # The trouble is, even post-crisis the ATM remains empty
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> brand_new_atm = hsbc.create_atm() # Build a new one to get back in business
>>> for cash in brand_new_atm:
...    print cash
$100
$100
$100
$100
$100
$100
$100
$100
$100
...

Nota: per Python 3, utilizzare print(corner_street_atm.__next__()) o print(next(corner_street_atm))

Può essere utile per varie cose come controllare l'accesso a una risorsa.

Itertools, il tuo migliore amico

Il modulo itertools contiene funzioni speciali per manipolare i iterabili. Hai mai desiderato duplicare un generatore? Catena due generatori? Raggruppa i valori in un elenco annidato con un unico elemento? Map / Zip senza creare un altro elenco?

Quindi import itertools solo import itertools .

Un esempio? Vediamo i possibili ordini di arrivo per una corsa di quattro cavalli:

>>> horses = [1, 2, 3, 4]
>>> races = itertools.permutations(horses)
>>> print(races)
<itertools.permutations object at 0xb754f1dc>
>>> print(list(itertools.permutations(horses)))
[(1, 2, 3, 4),
 (1, 2, 4, 3),
 (1, 3, 2, 4),
 (1, 3, 4, 2),
 (1, 4, 2, 3),
 (1, 4, 3, 2),
 (2, 1, 3, 4),
 (2, 1, 4, 3),
 (2, 3, 1, 4),
 (2, 3, 4, 1),
 (2, 4, 1, 3),
 (2, 4, 3, 1),
 (3, 1, 2, 4),
 (3, 1, 4, 2),
 (3, 2, 1, 4),
 (3, 2, 4, 1),
 (3, 4, 1, 2),
 (3, 4, 2, 1),
 (4, 1, 2, 3),
 (4, 1, 3, 2),
 (4, 2, 1, 3),
 (4, 2, 3, 1),
 (4, 3, 1, 2),
 (4, 3, 2, 1)]

Comprensione dei meccanismi interni dell'iterazione

L'iterazione è un processo che implica iterabili (implementando il __iter__() ) e iteratori (implementando il __next__() ). Iterables sono tutti gli oggetti da cui è possibile ottenere un iteratore. Iterator sono oggetti che ti permettono di scorrere su iterabili.

C'è dell'altro in questo articolo su come funzionano i loop .

yield list

Qual è l'uso della parola chiave yield in Python? Che cosa fa?

Ad esempio, sto cercando di capire questo codice 1 :

def _get_child_candidates(self, distance, min_dist, max_dist):
    if self._leftchild and distance - max_dist < self._median:
        yield self._leftchild
    if self._rightchild and distance + max_dist >= self._median:
        yield self._rightchild  

E questo è il chiamante:

result, candidates = [], [self]
while candidates:
    node = candidates.pop()
    distance = node._get_dist(obj)
    if distance <= max_dist and distance >= min_dist:
        result.extend(node._values)
    candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
return result

Cosa succede quando viene chiamato il metodo _get_child_candidates ? È stata restituita una lista? Un singolo elemento? Si chiama di nuovo? Quando si fermeranno le chiamate successive?

1. Il codice proviene da Jochen Schulz (jrschulz), che ha creato una grande libreria Python per gli spazi metrici. Questo è il link alla fonte completa: Modulo mspace .




Pensare in questo modo:

Un iteratore è solo un termine dal suono elaborato per un oggetto che ha un metodo next (). Quindi una funzione resa diventa qualcosa di simile a questo:

Versione originale:

def some_function():
    for i in xrange(4):
        yield i

for i in some_function():
    print i

Questo è fondamentalmente ciò che l'interprete Python fa con il codice di cui sopra:

class it:
    def __init__(self):
        # Start at -1 so that we get 0 when we add 1 below.
        self.count = -1

    # The __iter__ method will be called once by the 'for' loop.
    # The rest of the magic happens on the object returned by this method.
    # In this case it is the object itself.
    def __iter__(self):
        return self

    # The next method will be called repeatedly by the 'for' loop
    # until it raises StopIteration.
    def next(self):
        self.count += 1
        if self.count < 4:
            return self.count
        else:
            # A StopIteration exception is raised
            # to signal that the iterator is done.
            # This is caught implicitly by the 'for' loop.
            raise StopIteration

def some_func():
    return it()

for i in some_func():
    print i

Per ulteriori informazioni su cosa succede dietro le quinte, il ciclo for può essere riscritto a questo:

iterator = some_func()
try:
    while 1:
        print iterator.next()
except StopIteration:
    pass

Ha più senso o ti confonde di più? :)

Devo notare che questa è una semplificazione eccessiva a scopi illustrativi. :)




Cosa fa la parola chiave yield in Python?

Risposta Struttura / Riepilogo

  • Una funzione con yield , quando chiamata, restituisce un Generator .
  • I generatori sono iteratori perché implementano il protocollo iteratore , quindi puoi eseguirne l'iterazione.
  • Un generatore può anche essere inviato informazioni , rendendolo concettualmente una coroutine .
  • In Python 3, è possibile delegare da un generatore all'altro in entrambe le direzioni con yield from .
  • (L'Appendice critica un paio di risposte, inclusa quella superiore, e discute l'uso del return in un generatore).

generatori:

yield è legale solo all'interno di una definizione di funzione e l'inclusione del yield in una definizione di funzione fa sì che restituisca un generatore.

L'idea per i generatori viene da altre lingue (vedi nota 1) con diverse implementazioni. In Python's Generators, l'esecuzione del codice è frozen al punto della resa. Quando viene chiamato il generatore (i metodi sono discussi di seguito) l'esecuzione riprende e quindi si blocca alla resa successiva.

yield fornisce un modo semplice per implementare il protocollo iteratore , definito dai seguenti due metodi: __iter__ e next (Python 2) o __next__ (Python 3). Entrambi questi metodi rendono un oggetto un iteratore che è possibile digitare-verificare con la classe base astratta Iterator dal modulo delle collections .

>>> def func():
...     yield 'I am'
...     yield 'a generator!'
... 
>>> type(func)                 # A function with yield is still a function
<type 'function'>
>>> gen = func()
>>> type(gen)                  # but it returns a generator
<type 'generator'>
>>> hasattr(gen, '__iter__')   # that's an iterable
True
>>> hasattr(gen, 'next')       # and with .next (.__next__ in Python 3)
True                           # implements the iterator protocol.

Il tipo di generatore è un sottotipo di iteratore:

>>> import collections, types
>>> issubclass(types.GeneratorType, collections.Iterator)
True

E se necessario, possiamo digitare check-in in questo modo:

>>> isinstance(gen, types.GeneratorType)
True
>>> isinstance(gen, collections.Iterator)
True

Una caratteristica di un Iterator è che una volta esaurito , non è possibile riutilizzarlo o resettarlo:

>>> list(gen)
['I am', 'a generator!']
>>> list(gen)
[]

Dovrai fare un altro se vuoi usare di nuovo la sua funzionalità (vedi nota 2):

>>> list(func())
['I am', 'a generator!']

Uno può fornire dati a livello di codice, ad esempio:

def func(an_iterable):
    for item in an_iterable:
        yield item

Il generatore semplice di cui sopra è anche equivalente al seguente - a partire da Python 3.3 (e non disponibile in Python 2), puoi usare yield from :

def func(an_iterable):
    yield from an_iterable

Tuttavia, il yield from consente anche la delega ai subgeneratori, che verrà spiegato nella sezione seguente sulla delega cooperativa con sub-coroutine.

coroutine:

yield rappresenta un'espressione che consente di inviare dati al generatore (vedi nota 3)

Ecco un esempio, prendi nota della variabile received , che punterà ai dati che vengono inviati al generatore:

def bank_account(deposited, interest_rate):
    while True:
        calculated_interest = interest_rate * deposited 
        received = yield calculated_interest
        if received:
            deposited += received


>>> my_account = bank_account(1000, .05)

Innanzitutto, dobbiamo accodare il generatore con la funzione incorporata, next . __next__ metodo next o __next__ appropriato, a seconda della versione di Python che si sta utilizzando:

>>> first_year_interest = next(my_account)
>>> first_year_interest
50.0

E ora possiamo inviare dati al generatore. (L' invio di None equivale a chiamare in next .):

>>> next_year_interest = my_account.send(first_year_interest + 1000)
>>> next_year_interest
102.5

Delegazione cooperativa a sub-coroutine con yield from

Ora, ricorda che yield from è disponibile in Python 3. Ciò ci consente di delegare le coroutine a un sottoprogramma:

def money_manager(expected_rate):
    under_management = yield     # must receive deposited value
    while True:
        try:
            additional_investment = yield expected_rate * under_management 
            if additional_investment:
                under_management += additional_investment
        except GeneratorExit:
            '''TODO: write function to send unclaimed funds to state'''
        finally:
            '''TODO: write function to mail tax info to client'''


def investment_account(deposited, manager):
    '''very simple model of an investment account that delegates to a manager'''
    next(manager) # must queue up manager
    manager.send(deposited)
    while True:
        try:
            yield from manager
        except GeneratorExit:
            return manager.close()

E ora possiamo delegare funzionalità a un sub-generatore e può essere utilizzato da un generatore come sopra:

>>> my_manager = money_manager(.06)
>>> my_account = investment_account(1000, my_manager)
>>> first_year_return = next(my_account)
>>> first_year_return
60.0
>>> next_year_return = my_account.send(first_year_return + 1000)
>>> next_year_return
123.6

Puoi leggere di più sulla precisa semantica della yield from in PEP 380.

Altri metodi: chiudere e lanciare

Il metodo close genera GeneratorExit nel momento in cui l'esecuzione della funzione è stata congelata. Questo sarà anche chiamato da __del__ modo da poter inserire qualsiasi codice di pulizia in cui __del__ GeneratorExit :

>>> my_account.close()

È inoltre possibile generare un'eccezione che può essere gestita nel generatore o propagata all'utente:

>>> import sys
>>> try:
...     raise ValueError
... except:
...     my_manager.throw(*sys.exc_info())
... 
Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
  File "<stdin>", line 2, in <module>
ValueError

Conclusione

Credo di aver coperto tutti gli aspetti della seguente domanda:

Cosa fa la parola chiave yield in Python?

Si scopre che la yield fa molto. Sono sicuro che potrei aggiungere esempi ancora più approfonditi a questo. Se vuoi di più o avere qualche critica costruttiva, fammi sapere commentando qui sotto.

Appendice:

Critica della risposta in alto / accettata **

  • È confuso su ciò che rende un iterable , usando solo un elenco come esempio. Vedi i miei riferimenti sopra, ma in sintesi: un iterabile ha un metodo __iter__ che restituisce un iteratore . Un iteratore fornisce un .next (Python 2 o .__next__ (Python 3), che viene chiamato implicitamente da cicli for fino a quando non solleva StopIteration e, una volta fatto, continuerà a farlo.
  • Quindi utilizza un'espressione di generatore per descrivere cosa è un generatore. Dato che un generatore è semplicemente un modo conveniente per creare un iteratore , confonde solo la questione, e non abbiamo ancora ottenuto la parte di yield .
  • Nel controllo dell'esaurimento di un generatore , chiama il metodo .next , quando invece dovrebbe usare la funzione integrata, di next . Sarebbe uno strato appropriato di riferimento indiretto, perché il suo codice non funziona in Python 3.
  • Itertools? Questo non era rilevante per ciò che la yield ha a tutti.
  • Nessuna discussione sui metodi che fornisce fornisce insieme alla nuova yield from in Python 3. La risposta superiore / accettata è una risposta molto incompleta.

Critica di risposta che suggerisce la yield in un'espressione o comprensione generatore.

La grammatica attualmente consente qualsiasi espressione in una comprensione di lista.

expr_stmt: testlist_star_expr (annassign | augassign (yield_expr|testlist) |
                     ('=' (yield_expr|testlist_star_expr))*)
...
yield_expr: 'yield' [yield_arg]
yield_arg: 'from' test | testlist

Dal momento che la resa è un'espressione, è stata propagandata da alcuni come interessante utilizzarla nelle comprensioni o nell'espressione di generatore - nonostante non citi un caso d'uso particolarmente valido.

Gli sviluppatori principali di CPython stanno discutendo di deprecare la sua indennità . Ecco un post pertinente dalla mailing list:

Il 30 gennaio 2017 alle 19:05, Brett Cannon ha scritto:

Il 29 gennaio 2017 alle 16:39 Craig Rodrigues ha scritto:

Sto bene con entrambi gli approcci. Lasciare le cose come sono in Python 3 non va bene, IMHO.

Il mio voto è un errore Syntax poiché non ottieni ciò che ti aspetti dalla sintassi.

Sono d'accordo che per noi è un posto sensato finire, poiché qualsiasi codice basato sul comportamento attuale è davvero troppo intelligente per essere mantenibile.

In termini di arrivarci, probabilmente vorremmo:

  • SyntaxWarning o DeprecationWarning in 3.7
  • Avvertimento Py3k in 2.7.x
  • SyntaxError in 3.8

Saluti, Nick.

- Nick Coghlan | ncoghlan su gmail.com | Brisbane, Australia

Inoltre, c'è un problema in sospeso (10544) che sembra indicare la direzione di questa idea non è mai una buona idea (PyPy, un'implementazione Python scritta in Python, sta già sollevando avvisi di sintassi).

In conclusione, fino a quando gli sviluppatori di CPython non ci diranno diversamente: non mettere il yield in un'espressione o comprensione di generatore.

La dichiarazione di return in un generatore

In Python 2 :

In una funzione generatore, l'istruzione return non è autorizzata a includere un expression_list . In quel contesto, un semplice return indica che il generatore è stato fatto e causerà l' StopIteration di StopIteration .

An expression_listè praticamente un numero qualsiasi di espressioni separate da virgole - in sostanza, in Python 2, puoi fermare il generatore con return, ma non puoi restituire un valore.

In Python 3 :

In una funzione di generatore, la returndichiarazione indica che il generatore è fatto e farà StopIterationsorgere. Il valore restituito (se presente) viene utilizzato come argomento da costruire StopIteratione diventa l' StopIteration.valueattributo.

Le note

  1. Le lingue CLU, Sather e Icon sono state referenziate nella proposta per introdurre il concetto di generatori in Python. L'idea generale è che una funzione può mantenere lo stato interno e fornire dati intermedi su richiesta dall'utente. Questo ha promesso di essere superiore nelle prestazioni ad altri approcci, incluso il threading Python , che non è nemmeno disponibile su alcuni sistemi.

  2. Ciò significa, ad esempio, che gli xrangeoggetti ( rangein Python 3) non sono Iterators, anche se sono iterabili, perché possono essere riutilizzati. Come gli elenchi, i loro __iter__metodi restituiscono oggetti iteratore.

  3. yieldè stato originariamente introdotto come una dichiarazione, il che significa che poteva apparire solo all'inizio di una riga in un blocco di codice. Ora yieldcrea un'espressione di resa. https://docs.python.org/2/reference/simple_stmts.html#grammar-token-yield_stmt Questa modifica è stata proposta per consentire a un utente di inviare dati al generatore proprio come uno potrebbe riceverlo. Per inviare dati, bisogna essere in grado di assegnarlo a qualcosa, e per quello, una dichiarazione non funzionerà.




C'è una cosa in più da citare: una funzione che produce non deve necessariamente terminare. Ho scritto un codice come questo:

def fib():
    last, cur = 0, 1
    while True: 
        yield cur
        last, cur = cur, last + cur

Quindi posso usarlo in un altro codice come questo:

for f in fib():
    if some_condition: break
    coolfuncs(f);

Aiuta davvero a semplificare alcuni problemi e facilita il lavoro con alcune cose.




Resa ti dà un generatore.

def get_odd_numbers(i):
    return range(1, i, 2)
def yield_odd_numbers(i):
    for x in range(1, i, 2):
       yield x
foo = get_odd_numbers(10)
bar = yield_odd_numbers(10)
foo
[1, 3, 5, 7, 9]
bar
<generator object yield_odd_numbers at 0x1029c6f50>
bar.next()
1
bar.next()
3
bar.next()
5

Come puoi vedere, nel primo caso foo mantiene in memoria l'intera lista in una sola volta. Non è un grosso problema per una lista con 5 elementi, ma cosa succede se vuoi una lista di 5 milioni? Non solo è un enorme divoratore di memoria, ma richiede anche molto tempo per essere costruito nel momento in cui viene chiamata la funzione. Nel secondo caso, la barra ti dà solo un generatore. Un generatore è un iterabile, il che significa che è possibile utilizzarlo in un ciclo for, ecc., Ma è possibile accedere a ciascun valore solo una volta. Tutti i valori non vengono anche memorizzati in memoria nello stesso momento; l'oggetto generatore "ricorda" dove si trovava nel loop l'ultima volta che l'hai chiamato - in questo modo, se usi un iterabile per (dire) conta fino a 50 miliardi, non devi contare fino a 50 miliardi tutti subito e memorizzare i 50 miliardi di numeri per contare. Ancora una volta, questo è un esempio abbastanza forzato,probabilmente useresti itertools se volessi davvero contare fino a 50 miliardi. :)

Questo è il caso d'uso più semplice dei generatori. Come hai detto, può essere usato per scrivere permutazioni efficienti, usando yield per spingere le cose attraverso lo stack di chiamate invece di usare una sorta di variabile stack. I generatori possono essere utilizzati anche per attraversamenti di alberi specializzati e ogni sorta di altre cose.




Sta restituendo un generatore. Non ho molta familiarità con Python, ma credo che sia lo stesso tipo di blocco iteratore di C # se hai familiarità con quelli.

L'idea chiave è che il compilatore / interprete / qualsiasi cosa faccia qualche trucco in modo tale che per quanto riguarda il chiamante, possono continuare a chiamare next () e manterrà i valori di ritorno - come se il metodo del generatore fosse sospeso . Ora ovviamente non si può realmente "mettere in pausa" un metodo, quindi il compilatore costruisce una macchina a stati per ricordare dove si è attualmente e quali sono le variabili locali ecc. Questo è molto più facile che scrivere un iteratore tu stesso.




Ecco un esempio in linguaggio semplice. Fornirò una corrispondenza tra concetti umani di alto livello e concetti di basso livello di Python.

Voglio operare su una sequenza di numeri, ma non voglio disturbarmi con la creazione di quella sequenza, voglio solo concentrarmi sull'operazione che voglio fare. Quindi, faccio quanto segue:

  • Ti chiamo e ti dico che voglio una sequenza di numeri che viene prodotta in un modo specifico, e ti faccio sapere qual è l'algoritmo.
    Questo passaggio corrisponde all'integrazione defdella funzione generatore, ovvero la funzione contenente a yield.
  • Qualche tempo dopo, ti dico, "Ok, preparati a dirmi la sequenza dei numeri".
    Questo passaggio corrisponde a chiamare la funzione generatore che restituisce un oggetto generatore. Nota che non mi dici ancora nessun numero; ti basta prendere la carta e la matita.
  • Ti chiedo, "dimmi il prossimo numero", e tu mi dici il primo numero; dopo di ciò, mi aspetti di chiederti il ​​numero successivo. È compito tuo ricordare dove ti trovavi, quali numeri hai già detto e qual è il prossimo numero. Non mi importa dei dettagli.
    Questo passaggio corrisponde alla chiamata .next()sull'oggetto generatore.
  • ... ripetere il passaggio precedente, fino a ...
  • alla fine, potresti finire. Non mi dici un numero; tu solo gridi, "tieni i tuoi cavalli! Ho finito! Basta numeri!"
    Questo passaggio corrisponde all'oggetto generatore che termina il suo lavoro e genera StopIterationun'eccezione La funzione generatore non ha bisogno di aumentare l'eccezione. Viene sollevato automaticamente quando la funzione termina o genera a return.

Questo è ciò che fa un generatore (una funzione che contiene a yield); inizia l'esecuzione, si ferma ogni volta che fa un yield, e quando viene richiesto un .next()valore continua dal punto in cui è stato l'ultimo. Si adatta perfettamente alla progettazione con il protocollo iteratore di Python, che descrive come richiedere i valori sequenzialmente.

L'utente più famoso del protocollo iteratore è il forcomando in Python. Quindi, ogni volta che fai un:

for item in sequence:

non importa se sequenceè una lista, una stringa, un dizionario o un oggetto generatore come descritto sopra; il risultato è lo stesso: leggi gli elementi da una sequenza uno per uno.

Nota che defentrare in una funzione che contiene una yieldparola chiave non è l'unico modo per creare un generatore; è solo il modo più semplice per crearne uno.

Per informazioni più accurate, leggere i tipi di iteratore , la dichiarazione di rendimento e i generators nella documentazione di Python.




C'è un altro yielduso e significato (da Python 3.3):

yield from <expr>

Da PEP 380 - Sintassi per la delega a un sottogeneratore :

Una sintassi viene proposta per un generatore per delegare parte delle sue operazioni a un altro generatore. Ciò consente di scomporre una parte di codice contenente "yield" e di inserirla in un altro generatore. Inoltre, il subgeneratore può tornare con un valore e il valore viene reso disponibile al generatore delegante.

La nuova sintassi apre anche alcune opportunità di ottimizzazione quando un generatore restituisce valori prodotti da un altro.

Inoltre this introdurrà (dal momento che Python 3.5):

async def new_coroutine(data):
   ...
   await blocking_action()

per evitare che le coroutine vengano confuse con un generatore regolare (oggi yieldè usato in entrambi).




Ecco alcuni esempi di Python su come implementare realmente i generatori come se Python non fornisse loro zucchero sintattico:

Come generatore Python:

from itertools import islice

def fib_gen():
    a, b = 1, 1
    while True:
        yield a
        a, b = b, a + b

assert [1, 1, 2, 3, 5] == list(islice(fib_gen(), 5))

Usando chiusure lessicali invece di generatori

def ftake(fnext, last):
    return [fnext() for _ in xrange(last)]

def fib_gen2():
    #funky scope due to python2.x workaround
    #for python 3.x use nonlocal
    def _():
        _.a, _.b = _.b, _.a + _.b
        return _.a
    _.a, _.b = 0, 1
    return _

assert [1,1,2,3,5] == ftake(fib_gen2(), 5)

Utilizzo di chiusure di oggetti anziché generatori (perché ClosuresAndObjectsAreEquivalent )

class fib_gen3:
    def __init__(self):
        self.a, self.b = 1, 1

    def __call__(self):
        r = self.a
        self.a, self.b = self.b, self.a + self.b
        return r

assert [1,1,2,3,5] == ftake(fib_gen3(), 5)



Tutte ottime risposte, comunque un po 'difficili per i neofiti.

Presumo che tu abbia appreso la returndichiarazione.

Come un'analogia returne yieldsono gemelli. returnsignifica 'ritorno e stop' mentre 'rendimento' significa 'ritorno, ma continua'

  1. Cerca di ottenere un num_list con return.
def num_list(n):
    for i in range(n):
        return i

Eseguirlo:

In [5]: num_list(3)
Out[5]: 0

Vedi, ottieni solo un singolo numero piuttosto che un elenco di essi. returnnon ti consente mai di prevalere felicemente, implementa solo una volta e smetti.

  1. Arriva yield

Sostituisci returncon yield:

In [10]: def num_list(n):
    ...:     for i in range(n):
    ...:         yield i
    ...:

In [11]: num_list(3)
Out[11]: <generator object num_list at 0x10327c990>

In [12]: list(num_list(3))
Out[12]: [0, 1, 2]

Ora vinci per ottenere tutti i numeri.

Confrontando con returnquali esecuzioni una volta e fermandosi, yieldesegui i tempi che hai pianificato. Puoi interpretare returncome return one of them, e yieldcome return all of them. Questo è chiamato iterable.

  1. Un altro passo possiamo riscrivere la yielddichiarazione conreturn
In [15]: def num_list(n):
    ...:     result = []
    ...:     for i in range(n):
    ...:         result.append(i)
    ...:     return result

In [16]: num_list(3)
Out[16]: [0, 1, 2]

È il nucleo di yield.

La differenza tra un returnoutput di lista e l' yieldoutput dell'oggetto è:

Otterrai sempre [0, 1, 2] da un oggetto elenco, ma potrai recuperarli da " yieldoutput dell'oggetto " solo una volta. Quindi, ha un nuovo generatoroggetto nome come visualizzato in Out[11]: <generator object num_list at 0x10327c990>.

In conclusione, come metafora per ingannarlo:

  • returne yieldsono gemelli
  • liste generatorsono gemelli



Ecco un'immagine mentale di ciò che yieldfa.

Mi piace pensare a un thread come a uno stack (anche se non è implementato in questo modo).

Quando viene chiamata una funzione normale, mette le sue variabili locali nello stack, esegue alcuni calcoli, quindi cancella lo stack e ritorna. I valori delle sue variabili locali non vengono mai più visti.

Con una yieldfunzione, quando il suo codice inizia a funzionare (cioè dopo che la funzione è stata chiamata, restituendo un oggetto generatore, il cui next()metodo è quindi invocato), mette le sue variabili locali nello stack e calcola per un po '. Ma poi, quando colpisce la yielddichiarazione, prima di cancellare la sua parte dello stack e tornare, prende uno snapshot delle sue variabili locali e le memorizza nell'oggetto generatore. Scrive anche il punto in cui è attualmente nel suo codice (cioè la yielddichiarazione particolare ).

Quindi è una specie di funzione congelata su cui è appeso il generatore.

Quando next()viene chiamato successivamente, recupera gli oggetti della funzione sullo stack e lo anima nuovamente. La funzione continua a calcolare da dove era stata interrotta, ignaro del fatto che aveva appena passato un'eternità in una cella frigorifera.

Confronta i seguenti esempi:

def normalFunction():
    return
    if False:
        pass

def yielderFunction():
    return
    if False:
        yield 12

Quando chiamiamo la seconda funzione, si comporta in modo molto diverso dal primo. L' yieldaffermazione potrebbe essere irraggiungibile, ma se è presente ovunque, cambia la natura di ciò con cui abbiamo a che fare.

>>> yielderFunction()
<generator object yielderFunction at 0x07742D28>

La chiamata yielderFunction()non esegue il suo codice, ma crea un generatore fuori dal codice. (Forse è una buona idea denominare tali cose con il yielderprefisso per la leggibilità.)

>>> gen = yielderFunction()
>>> dir(gen)
['__class__',
 ...
 '__iter__',    #Returns gen itself, to make it work uniformly with containers
 ...            #when given to a for loop. (Containers return an iterator instead.)
 'close',
 'gi_code',
 'gi_frame',
 'gi_running',
 'next',        #The method that runs the function's body.
 'send',
 'throw']

I campi gi_codee gi_framesono dove viene memorizzato lo stato congelato. Esplorandoli con dir(..), possiamo confermare che il nostro modello mentale sopra è credibile.




Il rendimento è un oggetto

A returnin una funzione restituirà un singolo valore.

Se vuoi che una funzione restituisca un enorme set di valori , usa yield.

Ancora più importante, yieldè una barriera .

come barriera nel linguaggio CUDA, non trasferirà il controllo fino a quando non viene completato.

Cioè, eseguirà il codice nella tua funzione dall'inizio fino a quando non colpisce yield. Quindi, restituirà il primo valore del ciclo.

Quindi, ogni altra chiamata eseguirà il ciclo che hai scritto nella funzione ancora una volta, restituendo il valore successivo fino a quando non c'è alcun valore da restituire.




yieldè come un elemento di ritorno per una funzione. La differenza è che l' yieldelemento trasforma una funzione in un generatore. Un generatore si comporta come una funzione finché qualcosa non viene "ceduto". Il generatore si arresta finché non viene richiamato e continua esattamente dallo stesso punto in cui è stato avviato. Puoi ottenere una sequenza di tutti i valori "ottenuti" in uno, chiamando list(generator()).




Molte persone usano returnpiuttosto che yield, ma in alcuni casi yieldpossono essere più efficienti e più facili da lavorare.

Ecco un esempio che yieldè sicuramente il migliore per:

ritorno (in funzione)

import random

def return_dates():
    dates = [] # With 'return' you need to create a list then return it
    for i in range(5):
        date = random.choice(["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th"])
        dates.append(date)
    return dates

rendimento (in funzione)

def yield_dates():
    for i in range(5):
        date = random.choice(["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th"])
        yield date # 'yield' makes a generator automatically which works
                   # in a similar way. This is much more efficient.

Funzioni di chiamata

dates_list = return_dates()
print(dates_list)
for i in dates_list:
    print(i)

dates_generator = yield_dates()
print(dates_generator)
for i in dates_generator:
    print(i)

Entrambe le funzioni fanno la stessa cosa, ma yieldusano tre linee anziché cinque e ha una variabile in meno di cui preoccuparsi.

Questo è il risultato del codice:

Come puoi vedere entrambe le funzioni fanno la stessa cosa. L'unica differenza è return_dates()dare una lista e yield_dates()dare un generatore.

Un esempio di vita reale potrebbe essere qualcosa come leggere un file riga per riga o se si desidera creare un generatore.




Ecco un yieldapproccio semplice basato, per calcolare la serie di fibonacci, ha spiegato:

def fib(limit=50):
    a, b = 0, 1
    for i in range(limit):
       yield b
       a, b = b, a+b

Quando inserisci questo nel tuo REPL e poi prova a chiamarlo, otterrai un risultato mistificante:

>>> fib()
<generator object fib at 0x7fa38394e3b8>

Questo perché la presenza di yieldsegnali a Python che si desidera creare un generatore , vale a dire un oggetto che genera valori su richiesta.

Quindi, come si generano questi valori? Questo può essere fatto direttamente usando la funzione built-in next, o, indirettamente, alimentandolo con un costrutto che consuma valori.

Usando la funzione built-in next(), invochi direttamente .next/ __next__, forzando il generatore a produrre un valore:

>>> g = fib()
>>> next(g)
1
>>> next(g)
1
>>> next(g)
2
>>> next(g)
3
>>> next(g)
5

Indirettamente, se fornisci fibun forciclo, un listinizializzatore, un tupleinizializzatore o qualsiasi altra cosa che si aspetta un oggetto che genera / produce valori, "consumerai" il generatore fino a quando non sarà più prodotto da esso (e restituisce) :

results = []
for i in fib(30):       # consumes fib
    results.append(i) 
# can also be accomplished with
results = list(fib(30)) # consumes fib

Allo stesso modo, con un tupleinizializzatore:

>>> tuple(fib(5))       # consumes fib
(1, 1, 2, 3, 5)

Un generatore differisce da una funzione nel senso che è pigro. Ciò si ottiene mantenendo il suo stato locale e consentendoti di riprenderti quando necessario.

Quando invochi fibper la prima volta chiamandolo:

f = fib()

Python compila la funzione, incontra la yieldparola chiave e restituisce semplicemente un oggetto generatore a te. Non molto utile sembra.

Quando lo richiedi, genera il primo valore, direttamente o indirettamente, esegue tutte le istruzioni che trova, finché non incontra un yield, quindi restituisce il valore fornito yielde sospeso. Per un esempio che dimostra meglio questo, usiamo alcune printchiamate (sostituisci con print "text"se su Python 2):

def yielder(value):
    """ This is an infinite generator. Only use next on it """ 
    while 1:
        print("I'm going to generate the value for you")
        print("Then I'll pause for a while")
        yield value
        print("Let's go through it again.")

Ora, inserisci nella REPL:

>>> gen = yielder("Hello, yield!")

hai un oggetto generatore ora in attesa di un comando per generare un valore. Usa nexte vedi cosa viene stampato:

>>> next(gen) # runs until it finds a yield
I'm going to generate the value for you
Then I'll pause for a while
'Hello, yield!'

I risultati non quotati sono ciò che viene stampato. Il risultato indicato è ciò che viene restituito yield. Chiama di nextnuovo ora:

>>> next(gen) # continues from yield and runs again
Let's go through it again.
I'm going to generate the value for you
Then I'll pause for a while
'Hello, yield!'

Il generatore ricorda che è stato messo in pausa yield valuee riprende da lì. Il prossimo messaggio viene stampato e la ricerca yielddell'istruzione da sospendere viene eseguita di nuovo (a causa del whileciclo).




Related

python iterator generator yield coroutine