Per capire cosa yield
fa, devi capire cosa sono i generatori . E prima che tu possa capire i generatori, devi capire gli iterabili .
iterabili
Quando si crea un elenco, è possibile leggere i suoi elementi uno per uno. Leggere i suoi elementi uno per uno si chiama iterazione:
>>> mylist = [1, 2, 3]
>>> for i in mylist:
... print(i)
1
2
3
mylist
è un iterabile . Quando si utilizza la comprensione di un elenco, si crea un elenco 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...
" su è iterabile; 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, un tipo di iterabile che puoi ripetere una sola volta . 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, tranne che hai usato al ()
posto di []
. MA non puoi esibirtifor i in mygenerator
una seconda volta poiché i generatori possono essere usati solo una volta: calcolano 0, poi si dimenticano di esso e calcolano 1, e terminano il calcolo 4, uno per uno.
dare la precedenza
yield
è una parola chiave che viene utilizzata come return
, tranne per il fatto 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 sai che la tua funzione restituirà un enorme set di valori che dovrai leggere solo una volta.
Per padroneggiare yield
, devi capire che quando chiami la funzione, il codice che hai scritto nel corpo della funzione non viene eseguito. La funzione restituisce solo l'oggetto generatore, questo è un po 'complicato :-)
Quindi, il codice continuerà da dove era stato interrotto ogni volta che for
utilizza il generatore.
Ora la parte difficile:
La prima volta che for
chiama l'oggetto generatore creato dalla tua funzione, eseguirà il codice nella tua funzione dall'inizio fino a quando non colpisce yield
, quindi restituirà il primo valore del ciclo. Quindi, ogni chiamata successiva eseguirà un'altra iterazione del ciclo che hai scritto nella funzione e restituirà il valore successivo. Ciò continuerà fino a quando il generatore viene considerato vuoto, cosa che accade quando la funzione viene eseguita senza colpire yield
. Ciò può essere dovuto al fatto che il ciclo è terminato o perché non si soddisfa più un "if/else"
.
Il tuo codice ha 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 the 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 the 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 candidate's 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 un elenco, ma l'elenco si espande mentre il ciclo viene ripetuto :-) È un modo conciso di esaminare tutti questi dati nidificati anche se è un po 'pericoloso poiché puoi finire con un ciclo infinito. In questo caso, candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
esaurisci tutti i valori del generatore, ma while
continua a creare nuovi oggetti generatore che produrranno valori diversi dai precedenti poiché non sono applicati sullo stesso nodo.
Il extend()
metodo è un metodo di oggetto elenco che prevede un iterabile e aggiunge i suoi valori all'elenco.
Di solito passiamo un elenco ad esso:
>>> a = [1, 2]
>>> b = [3, 4]
>>> a.extend(b)
>>> print(a)
[1, 2, 3, 4]
Ma nel tuo codice, ottiene un generatore, che è buono perché:
- Non è necessario leggere i valori due volte.
- Potresti avere molti figli e non li vuoi tutti memorizzati.
E funziona perché a Python non importa se l'argomento di un metodo è un elenco o meno. Python si aspetta iterabili, quindi funzionerà con stringhe, elenchi, tuple e generatori! Questo si chiama Duck Typing ed è uno dei motivi per cui Python è così bello. 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__())
oprint(next(corner_street_atm))
Può essere utile per varie cose come il controllo dell'accesso a una risorsa.
Itertools, il tuo migliore amico
Il modulo itertools contiene funzioni speciali per manipolare gli iterabili. Hai mai desiderato duplicare un generatore? Catena di due generatori? Raggruppare i valori in un elenco nidificato con una riga? Map / Zip
senza creare un altro elenco?
Quindi basta import itertools
.
Un esempio? Vediamo i possibili ordini di arrivo per una corsa a 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 (implementazione del __iter__()
metodo) e iteratori (implementazione del __next__()
metodo). Gli Iterabili sono tutti gli oggetti da cui è possibile ottenere un iteratore. Gli iteratori sono oggetti che ti consentono di iterare su iterabili.
C'è di più in questo articolo su come for
funzionano i loop .
yield
non è magico come suggerisce questa risposta. Quando si chiama una funzione che contieneyield
un'istruzione ovunque, si ottiene un oggetto generatore, ma non viene eseguito alcun codice. Quindi ogni volta che si estrae un oggetto dal generatore, Python esegue il codice nella funzione fino a quando non arriva ayield
un'istruzione, quindi mette in pausa e consegna l'oggetto. Quando si estrae un altro oggetto, Python riprende subito dopoyield
e continua fino a quando non raggiunge un altroyield
(spesso lo stesso, ma una iterazione in seguito). Questo continua fino a quando la funzione termina alla fine, a quel punto il generatore viene considerato esaurito.