Care este utilizarea cuvântului yield
în Python și ce face acesta?
De exemplu, încerc să înțeleg acest cod1:
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
Și acesta este apelantul:
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
Ce se întâmplă atunci când metoda _get_child_candidates
este apelată?Este returnată o listă? Un singur element? Este apelată din nou? Când se vor opri apelurile ulterioare?
1. Această bucată de cod a fost scrisă de Jochen Schulz (jrschulz), care a creat o bibliotecă Python excelentă pentru spații metrice. Acesta este linkul către sursa completă: Modul mspace.
- @Booboo – ai dreptate – sistemul este stricat – votul nu le va elimina – > Por NeilG.
- @booboo: StackOverflow are adesea răspunsuri mult mai bune decât manualele. „Reputația” și „medaliile” aici sunt doar o grămadă de numere. Nu e ceva care să te deranjeze. Ceea ce este surprinzător, totuși, este cât de multă muncă neplătită fac moderatorii. Atât de mult timp personal pus la dispoziție. – > Por Nav.
Pentru a înțelege ce yield
face, trebuie să înțelegeți ce generatoare sunt. Și înainte de a înțelege generatoarele, trebuie să înțelegeți iterabilele.
Iterabile
Atunci când creați o listă, puteți citi elementele sale unul câte unul. Citirea elementelor sale unul câte unul se numește iterație:
>>> mylist = [1, 2, 3]
>>> for i in mylist:
... print(i)
1
2
3
mylist
este un iterabilă. Atunci când utilizați o înțelegere de listă, creați o listă, deci un iterabil:
>>> mylist = [x*x for x in range(3)]
>>> for i in mylist:
... print(i)
0
1
4
Tot ceea ce puteți utiliza „for... in...
” este un iterabil; lists
, , strings
, , fișiere…
Aceste iterabile sunt la îndemână pentru că le puteți citi oricât de mult doriți, dar stocați toate valorile în memorie și acest lucru nu este întotdeauna ceea ce doriți atunci când aveți multe valori.
Generatoare
Generatorii sunt iteratori, un fel de iterabile pe care se poate itera doar o singură dată. Generatorii nu stochează toate valorile în memorie, ele generează valorile din mers.:
>>> mygenerator = (x*x for x in range(3))
>>> for i in mygenerator:
... print(i)
0
1
4
Este exact același lucru, cu excepția faptului că ați folosit ()
în loc de []
. DAR, tu nu se poate efectua for i in mygenerator
a doua oară, deoarece generatoarele nu pot fi utilizate decât o singură dată: ele calculează 0, apoi uită de el și calculează 1, iar la final calculează 4, unul câte unul.
Randament
yield
este un cuvânt cheie care se utilizează ca și return
, , cu excepția faptului că funcția va returna un generator.
>>> def create_generator():
... mylist = range(3)
... for i in mylist:
... yield i*i
...
>>> mygenerator = create_generator() # create a generator
>>> print(mygenerator) # mygenerator is an object!
<generator object create_generator at 0xb7555c34>
>>> for i in mygenerator:
... print(i)
0
1
4
Aici este un exemplu inutil, dar este la îndemână atunci când știți că funcția dvs. va returna un set uriaș de valori pe care nu va trebui să le citiți decât o singură dată.
Pentru a stăpâni yield
, , trebuie să înțelegeți că atunci când apelați funcția, codul pe care l-ați scris în corpul funcției nu se execută. Funcția returnează doar obiectul generator, acest lucru este un pic mai complicat.
Apoi, codul dvs. va continua de unde a rămas de fiecare dată când for
utilizează generatorul.
Acum partea dificilă:
Prima dată când for
apelează obiectul generator creat din funcția dvs. va rula codul din funcția dvs. de la început până când va ajunge la yield
, , apoi va returna prima valoare din buclă. Apoi, fiecare apel ulterior va rula o altă iterație a buclei pe care ați scris-o în funcție și va returna următoarea valoare. Acest lucru va continua până când generatorul este considerat gol, ceea ce se întâmplă atunci când funcția rulează fără să atingă yield
. Acest lucru se poate datora faptului că bucla s-a încheiat sau pentru că nu mai satisfaceți o funcție "if/else"
.
Codul dvs. este explicat
Generator:
# 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
Apelant:
# 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
Acest cod conține mai multe părți inteligente:
-
Bucla itera pe o listă, dar lista se extinde în timp ce bucla este iterată. Este o modalitate concisă de a trece prin toate aceste date imbricate, chiar dacă este puțin periculoasă, deoarece puteți ajunge la o buclă infinită. În acest caz,
candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
epuizează toate valorile generatorului, darwhile
continuă să creeze noi obiecte de generator care vor produce valori diferite de cele anterioare, deoarece nu este aplicat pe același nod. -
extend()
este o metodă de obiect listă care așteaptă un iterabil și adaugă valorile sale la listă.
De obicei, îi transmitem o listă:
>>> a = [1, 2]
>>> b = [3, 4]
>>> a.extend(b)
>>> print(a)
[1, 2, 3, 4]
Dar în codul dvs., aceasta primește un generator, ceea ce este bine pentru că:
- Nu este nevoie să citiți valorile de două ori.
- S-ar putea să aveți mulți copii și nu doriți să le stocați pe toate în memorie.
Și funcționează pentru că Python nu-i pasă dacă argumentul unei metode este o listă sau nu. Python se așteaptă la iterabile, așa că va funcționa cu șiruri, liste, tupluri și generatoare! Acest lucru se numește duck typing și este unul dintre motivele pentru care Python este atât de cool. Dar aceasta este o altă poveste, pentru o altă întrebare…
Vă puteți opri aici sau puteți citi puțin mai departe pentru a vedea o utilizare avansată a unui generator:
Controlul epuizării unui generator
>>> 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
...
Notă: Pentru Python 3, utilizațiprint(corner_street_atm.__next__())
sau print(next(corner_street_atm))
Poate fi util pentru diverse lucruri, cum ar fi controlul accesului la o resursă.
Itertools, cel mai bun prieten al tău
Modulul itertools conține funcții speciale pentru a manipula iterabile. Ați dorit vreodată să duplicați un generator?Să înlănțuiți două generatoare? Să grupați valori într-o listă imbricata cu un singur rând? Map / Zip
fără a crea o altă listă?
Atunci doar import itertools
.
Un exemplu? Să vedem posibilele ordine de sosire pentru o cursă de patru cai:
>>> 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)]
Înțelegerea mecanismelor interne ale iterației
Iterarea este un proces care implică iterabile (care implementează __iter__()
) și iteratori (care implementează metoda __next__()
).Iterabilele sunt orice obiecte din care se poate obține un iterator. Iteratorii sunt obiecte care vă permit să iterați pe iterabile.
Pentru mai multe informații, consultați acest articol despre cum se utilizează for
funcționează buclele.
- 455
yield
nu este atât de magic pe cât sugerează acest răspuns. Atunci când apelați o funcție care conține unyield
declarație oriunde, veți obține un obiect generator, dar nu se execută niciun cod. Apoi, de fiecare dată când extrageți un obiect din generator, Python execută codul din funcție până când ajunge la unyield
declarație, apoi se oprește și livrează obiectul. Atunci când extrageți un alt obiect, Python reia imediat după ce a fost extrasă declarațiayield
și continuă până când ajunge la o altăyield
(adesea același, dar o iterație mai târziu). Acest lucru continuă până când funcția se termină, moment în care generatorul este considerat epuizat. – > .- „Aceste iterabile sunt la îndemână… dar stochezi toate valorile în memorie și nu este întotdeauna ceea ce vrei”, este fie greșit, fie confuz. Un iterabil returnează un iterator la apelarea iter() asupra iterabilului, iar un iterator nu trebuie întotdeauna să își stocheze valorile în memorie, în funcție de implementarea iter acesta poate, de asemenea, să genereze valori în secvență la cerere. – > .
- Ar fi bine să se adauge la acest lucru mare răspuns de ce Este exact la fel, cu excepția faptului că ați folosit
()
în loc de[]
, , mai exact ce()
este (poate exista o confuzie cu un tuple). – > . - S-ar putea să mă înșel, dar un generator nu este un iterator, un „generator numit” este un iterator. – > .
- @MatthiasFripp „Acest lucru continuă până când funcția se execută la sfârșit” – sau întâlnește un
return
declarație. (return
este permisă într-o funcție care conțineyield
, , cu condiția să nu specifice o valoare de returnare). – > .
49
Scurtătură pentru a înțelege yield
Atunci când vedeți o funcție cu yield
declarații, aplicați acest truc simplu pentru a înțelege ce se va întâmpla:
- Introduceți o linie
result = []
la începutul funcției. - Înlocuiți fiecare
yield expr
curesult.append(expr)
. - Introduceți o linie
return result
în partea de jos a funcției. - Gata – nu mai este nevoie de
yield
declarații! Citiți și descifrați codul. - Comparați funcția cu definiția originală.
Acest truc vă poate da o idee despre logica din spatele funcției, dar ceea ce se întâmplă de fapt cu yield
este semnificativ diferit de ceea ce se întâmplă în abordarea bazată pe liste. În multe cazuri, abordarea prin randament va fi mult mai eficientă din punct de vedere al memoriei și, de asemenea, mai rapidă. În alte cazuri, acest truc vă va bloca într-o buclă infinită, chiar dacă funcția originală funcționează foarte bine. Citiți mai departe pentru a afla mai multe…
Nu vă confundați Iterables, Iterators și Generators
În primul rând, funcțiile protocolul iterator – atunci când scrieți
for x in mylist:
...loop body...
Python efectuează următorii doi pași:
-
Obține un iterator pentru
mylist
:Apelarea
iter(mylist)
-> acest lucru returnează un obiect cu unnext()
metodă (sau__next__()
în Python 3).Acesta este pasul despre care majoritatea oamenilor uită să vă spună.
-
Folosește iteratorul pentru a face o buclă peste elemente:
Continuă să apelezi la
next()
pe iteratorul returnat de la pasul 1. Valoarea returnată de lanext()
este atribuită lax
și se execută corpul buclei. În cazul în care se produce o excepțieStopIteration
este ridicată din interiorulnext()
, înseamnă că nu mai există alte valori în iterator și că bucla este închisă.
Adevărul este că Python execută cei doi pași de mai sus ori de câte ori dorește să o facă să facă o buclă conținutul unui obiect – deci ar putea fi o buclă for, dar ar putea fi și un cod de tipul otherlist.extend(mylist)
(unde otherlist
este o listă Python).
Aici mylist
este un iterabil deoarece implementează protocolul iterator. Într-o clasă definită de utilizator, puteți implementa protocolul __iter__()
pentru a face ca instanțele clasei dvs. să fie iterabile. Această metodă ar trebui să returneze un iterator. Un iterator este un obiect cu un next()
metodă. Este posibil să se implementeze atât __iter__()
și next()
în aceeași clasă și să se poată utiliza __iter__()
return self
. Acest lucru va funcționa în cazuri simple, dar nu și atunci când doriți ca doi iteratori să ruleze în buclă peste același obiect în același timp.
Acesta este protocolul iteratorilor, multe obiecte implementează acest protocol:
- Listele încorporate, dicționarele, tuplurile, seturile, fișierele.
- Clase definite de utilizator care implementează
__iter__()
. - Generatori.
Rețineți că un for
nu știe cu ce tip de obiect are de-a face – urmează doar protocolul iteratorului și este fericit să primească element după element în timp ce apelează la next()
. Listele încorporate își returnează elementele unul câte unul, dicționarele returnează elementele chei una câte una, fișierele returnează elementele liniile una câte una, etc. Iar generatoarele returnează… ei bine, aici se află yield
intervine:
def f123():
yield 1
yield 2
yield 3
for item in f123():
print item
În loc de yield
dacă ați avea trei declarații return
instrucțiuni în f123()
doar prima ar fi executată, iar funcția ar ieși. Dar f123()
nu este o funcție obișnuită. Când f123()
este apelată, ea nu returnează niciuna dintre valorile din declarațiile yield! Ea returnează un obiect generator. De asemenea, funcția nu iese cu adevărat – intră într-o stare suspendată. Când funcția for
încearcă să se bucleze peste obiectul generator, funcția își reia starea de suspendare chiar de la următoarea linie după yield
de la care s-a întors anterior, execută următoarea linie de cod, în acest caz, un yield
și îl returnează ca fiind următorul element. Acest lucru se întâmplă până când funcția iese, moment în care generatorul ridică StopIteration
, , iar bucla iese din buclă.
Așadar, obiectul generator este un fel de adaptor – la un capăt, acesta prezintă protocolul iterator, expunând __iter__()
și next()
pentru a păstra for
bucla să fie mulțumită. La celălalt capăt, însă, execută funcția doar atât cât să obțină următoarea valoare din ea și o pune din nou în modul suspendat.
De ce să folosiți generatoare?
De obicei, puteți scrie un cod care nu folosește generatoare, dar care implementează aceeași logică. O opțiune este să folosiți „trucul” listei temporare pe care l-am menționat mai devreme. Acest lucru nu va funcționa în toate cazurile, de exemplu, dacă aveți bucle infinite, sau poate utiliza ineficient memoria atunci când aveți o listă foarte lungă. Cealaltă abordare este de a implementa o nouă clasă iterabilă SomethingIter care păstrează starea în membrii instanței și care efectuează următorul pas logic în next()
(sau __next__()
în Python 3). În funcție de logică, codul din interiorul clasei next()
poate ajunge să arate foarte complex și să fie predispus la erori. Generatoarele de aici oferă o soluție simplă și curată.
- 26
- „Când vedeți o funcție cu declarații yield, aplicați acest truc simplu pentru a înțelege ce se va întâmpla” Nu cumva acest lucru ignoră complet faptul că se poate
send
într-un generator, ceea ce reprezintă o mare parte din rostul generatoarelor? – > . - „ar putea fi o buclă for, dar ar putea fi, de asemenea, un cod precum
otherlist.extend(mylist)
” -> Acest lucru este incorect.extend()
modifică lista pe loc și nu returnează un iterabil. Încercarea de a face o buclă pesteotherlist.extend(mylist)
va eșua cu o eroareTypeError
deoareceextend()
returnează implicitNone
, și nu se poate face o buclă pesteNone
. – > . - @pedro Ați înțeles greșit această propoziție. Înseamnă că python efectuează cei doi pași menționați pe
mylist
(și nu peotherlist
) atunci când executăotherlist.extend(mylist)
. – > .
Gândiți-vă la asta în felul următor:
Un iterator este doar un termen fantezist pentru un obiect care are un next()
metodă. Deci, o funcție de randament ajunge să fie ceva de genul acesta:
Versiunea originală:
def some_function():
for i in xrange(4):
yield i
for i in some_function():
print i
Aceasta este practic ceea ce face interpretul Python cu codul de mai sus:
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
Pentru mai multe informații despre ceea ce se întâmplă în spatele scenei, aplicația for
bucla poate fi rescrisă astfel:
iterator = some_func()
try:
while 1:
print iterator.next()
except StopIteration:
pass
Asta are mai mult sens sau doar vă încurcă și mai mult? 🙂
Ar trebui să remarc faptul că acest este o simplificare excesivă în scop ilustrativ. 🙂
__getitem__
ar putea fi definit în loc de__iter__
. De exemplu:class it: pass; it.__getitem__ = lambda self, i: i*10 if i < 10 else [][0]; for i in it(): print(i)
, , Se va imprima: 0, 10, 20, …, 90 – > .- Am încercat acest exemplu în Python 3.6 și dacă creez
iterator = some_function()
, , variabilaiterator
nu are o funcție numitănext()
ci doar o funcție__next__()
funcție. M-am gândit să menționez acest lucru. – > . - Unde se află
for
pe care ai scris-o tu apelează funcția__iter__
metoda deiterator
, , instanța instanțiată ait
? – > . - Din păcate, acest răspuns nu este deloc adevărat. Nu este ceea ce face interpretul python cu generatoarele. Acesta nu creează o clasă pornind de la funcția generator și nu implementează
__iter__
și__next__
. Ceea ce face de fapt sub capotă este explicat în această postare stackoverflow.com/questions/45723893/…. Pentru a-l cita pe @Raymond Hettinger „generatoarele nu sunt implementate intern așa cum se arată în clasa ta python pură. În schimb, ei împărtășesc cea mai mare parte a aceleiași logici ca și funcțiile obișnuite” – > .
20
yield
se reduce la două fapte simple:
- Dacă compilatorul detectează
yield
cuvântul cheie oriunde în interiorul unei funcții, funcția respectivă nu se mai întoarce prin intermediul funcțieireturn
nu mai revine. În schimb,, , ea imediat returnează un obiect „listă în așteptare” leneș numit generator - Un generator este iterabil. Ce este un iterabil? Este ceva asemănător cu un
list
sauset
saurange
sau dict-view, cu un protocol încorporat pentru a vizita fiecare element într-o anumită ordine..
Pe scurt: un generator este o listă leneșă, care se execută în mod incremental, , și yield
declarațiile vă permit să folosiți notația de funcție pentru a programa valorile listei pe care generatorul ar trebui să le scuipe în mod incremental.
generator = myYieldingFunction(...)
x = list(generator)
generator
v
[x[0], ..., ???]
generator
v
[x[0], x[1], ..., ???]
generator
v
[x[0], x[1], x[2], ..., ???]
StopIteration exception
[x[0], x[1], x[2]] done
list==[x[0], x[1], x[2]]
Exemplu
Să definim o funcție makeRange
care este la fel ca cea din Python range
. Apelarea makeRange(n)
RETURNEAZĂ UN GENERATOR:
def makeRange(n):
# return 0,1,2,...,n-1
i = 0
while i < n:
yield i
i += 1
>>> makeRange(5)
<generator object makeRange at 0x19e4aa0>
Pentru a forța generatorul să returneze imediat valorile sale în așteptare, îl puteți trece în list()
(la fel ca orice iterabil):
>>> list(makeRange(5))
[0, 1, 2, 3, 4]
Comparând exemplul cu „returnând doar o listă”
Exemplul de mai sus poate fi considerat ca fiind o simplă creare a unei liste pe care o adăugați și o returnați:
# list-version # # generator-version
def makeRange(n): # def makeRange(n):
"""return [0,1,2,...,n-1]""" #~ """return 0,1,2,...,n-1"""
TO_RETURN = [] #>
i = 0 # i = 0
while i < n: # while i < n:
TO_RETURN += [i] #~ yield i
i += 1 # i += 1 ## indented
return TO_RETURN #>
>>> makeRange(5)
[0, 1, 2, 3, 4]
Există totuși o diferență majoră; a se vedea ultima secțiune.
Cum ați putea utiliza generatoarele
Un iterabil este ultima parte a unei înțelegeri de listă, iar toți generatorii sunt iterabili, așa că sunt adesea utilizați astfel:
# _ITERABLE_
>>> [x+10 for x in makeRange(5)]
[10, 11, 12, 13, 14]
Pentru a vă familiariza mai bine cu generatorii, vă puteți juca cu itertools
(asigurați-vă că utilizați chain.from_iterable
în loc de chain
atunci când este cazul). De exemplu, ați putea chiar să utilizați generatorii pentru a implementa liste leneșe infinit de lungi, cum ar fi itertools.count()
. Ați putea implementa propriul dvs. def enumerate(iterable): zip(count(), iterable)
, sau, alternativ, puteți face acest lucru cu yield
într-un while-loop.
Vă rugăm să rețineți: generatorii pot fi utilizați de fapt pentru mult mai multe lucruri, cum ar fi implementarea de corutine sau programarea nedeterministă sau alte lucruri elegante. Cu toate acestea, punctul de vedere al „listelor leneșe” pe care îl prezint aici este cea mai comună utilizare pe care o veți găsi.
În spatele scenei
Iată cum funcționează „protocolul de iterație Python”. Adică, ce se întâmplă atunci când faceți list(makeRange(5))
. Aceasta este ceea ce am descris mai devreme ca fiind o „listă leneșă, incrementală”.
>>> x=iter(range(5))
>>> next(x)
0
>>> next(x)
1
>>> next(x)
2
>>> next(x)
3
>>> next(x)
4
>>> next(x)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Funcția încorporată next()
apelează pur și simplu obiectele .next()
care face parte din „protocolul de iterație” și se găsește pe toți iteratorii. Puteți utiliza manual funcția next()
(și alte părți ale protocolului de iterație) pentru a implementa lucruri fanteziste, de obicei în detrimentul lizibilității, așa că încercați să evitați să faceți asta…
Minutiae
În mod normal, cei mai mulți oameni nu ar fi interesați de următoarele distincții și probabil vor dori să nu mai citească aici.
În limbajul Python, un iterabil este orice obiect care „înțelege conceptul de buclă de căutare”, precum o listă [1,2,3]
, , iar un iterator este o instanță specifică a buclei for-loop solicitate, cum ar fi [1,2,3].__iter__()
. A generator este exact la fel ca orice iterator, cu excepția modului în care a fost scris (cu sintaxa funcției).
Atunci când solicitați un iterator dintr-o listă, acesta creează un nou iterator. Cu toate acestea, atunci când solicitați un iterator de la un iterator (ceea ce se întâmplă foarte rar), acesta vă oferă doar o copie a sa.
Astfel, în cazul puțin probabil în care nu reușiți să faceți ceva de genul acesta…
> x = myRange(5)
> list(x)
[0, 1, 2, 3, 4]
> list(x)
[]
… atunci amintiți-vă că un generator este un iterator; adică, este de unică folosință. Dacă doriți să îl reutilizați, trebuie să apelați myRange(...)
din nou. Dacă aveți nevoie să utilizați rezultatul de două ori, convertiți rezultatul într-o listă și stocați-l într-o variabilă x = list(myRange(5))
. Cei care trebuie neapărat să cloneze un generator (de exemplu, cei care fac o metaprogramare teribil de hacker) pot folosi itertools.tee
dacă este absolut necesar, deoarece iteratorul copiabil Python PEP a fost amânată.
Ce înseamnă
yield
în Python?
Schiță de răspuns/rezumat
- O funcție cu
yield
, , atunci când este apelată, returnează un Generator. - Generatorii sunt iteratori deoarece implementează funcția protocolul iterator, , astfel încât puteți itera peste ei.
- Un generator poate fi, de asemenea, un trimis informații, , ceea ce îl face, din punct de vedere conceptual, un o corutină.
- În Python 3, puteți delegați de la un generator la altul în ambele direcții cu
yield from
. - (Anexa critică câteva răspunsuri, inclusiv pe cel de sus, și discută utilizarea lui
return
într-un generator).
Generatoare:
yield
este legală numai în interiorul unei definiții de funcție, iar includerea lui yield
într-o definiție de funcție face ca aceasta să returneze un generator.
Ideea generatoarelor provine din alte limbaje (a se vedea nota de subsol 1) cu implementări diferite. În cazul generatorilor din Python, execuția codului este înghețată în punctul în care se produce randamentul. Atunci când generatorul este apelat (metodele sunt discutate mai jos), execuția se reia și apoi se blochează la următorul randament.
yield
oferă o modalitate simplă de implementare a protocolului iteratorilor, , definit de următoarele două metode:__iter__
și next
(Python 2) sau __next__
(Python 3). Ambele metode transformă un obiect într-un iterator pe care îl puteți verifica cu ajutorul metodei Iterator
Abstract BaseClass din collections
modul.
>>> 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.
Tipul generator este un subtip al iteratorului:
>>> import collections, types
>>> issubclass(types.GeneratorType, collections.Iterator)
True
Și, dacă este necesar, putem face o verificare de tip în felul următor:
>>> isinstance(gen, types.GeneratorType)
True
>>> isinstance(gen, collections.Iterator)
True
O caracteristică a unui Iterator
este că, odată epuizat, nu se mai poate reutiliza sau reseta:
>>> list(gen)
['I am', 'a generator!']
>>> list(gen)
[]
Va trebui să creați un alt an dacă doriți să îi folosiți din nou funcționalitatea (a se vedea nota de subsol 2):
>>> list(func())
['I am', 'a generator!']
Se pot obține date prin programare, de exemplu:
def func(an_iterable):
for item in an_iterable:
yield item
Generatorul simplu de mai sus este, de asemenea, echivalent cu cel de mai jos – începând cu Python 3.3 (și nu este disponibil în Python 2), puteți utiliza yield from
:
def func(an_iterable):
yield from an_iterable
Cu toate acestea, yield from
permite, de asemenea, delegarea către subgeneratori,care va fi explicată în următoarea secțiune privind delegarea cooperantă cu subcorutine.
Corutine:
yield
formează o expresie care permite trimiterea de date în generator (a se vedea nota de subsol 3).
Iată un exemplu, luați aminte la received
variabila, care va indica datele care sunt trimise în generator:
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)
În primul rând, trebuie să punem în coadă generatorul cu funcția builtin, next
. Aceasta va apela funcția corespunzătoare next
sau __next__
în funcție de versiunea de Python pe care o utilizați:
>>> first_year_interest = next(my_account)
>>> first_year_interest
50.0
Și acum putem trimite date în generator. (Trimiterea None
este același lucru cu apelarea next
.) :
>>> next_year_interest = my_account.send(first_year_interest + 1000)
>>> next_year_interest
102.5
Delegare cooperativă către o subcorectă cu yield from
Acum, reamintim că yield from
este disponibil în Python 3. Acest lucru ne permite să delegăm corutinele către o subcorutine:
def money_manager(expected_rate):
# must receive deposited value from .send():
under_management = yield # yield None to start.
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'''
raise
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'''
# must queue up manager:
next(manager) # <- same as manager.send(None)
# This is where we send the initial deposit to the manager:
manager.send(deposited)
try:
yield from manager
except GeneratorExit:
return manager.close() # delegate?
Și acum putem delega funcționalitatea către un subgenerator și poate fi utilizată de un generator la fel ca mai sus:
my_manager = money_manager(.06)
my_account = investment_account(1000, my_manager)
first_year_return = next(my_account) # -> 60.0
Acum simulați adăugarea a încă 1.000 în cont plus randamentul contului (60,0):
next_year_return = my_account.send(first_year_return + 1000)
next_year_return # 123.6
Puteți citi mai multe despre semantica exactă a yield from
în PEP 380.
Alte metode: close și throw
Site-ul close
ridică GeneratorExit
în momentul în care execuția funcției a fost înghețată. Aceasta va fi apelată și de către __del__
astfel încât puteți pune orice cod de curățare în locul în care vă ocupați de GeneratorExit
:
my_account.close()
Puteți, de asemenea, să aruncați o excepție care poate fi tratată în generator sau propagată către utilizator:
import sys
try:
raise ValueError
except:
my_manager.throw(*sys.exc_info())
Ridică:
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
File "<stdin>", line 6, in money_manager
File "<stdin>", line 2, in <module>
ValueError
Concluzie
Cred că am acoperit toate aspectele următoarei întrebări:
Ce înseamnă
yield
în Python?
Se pare că yield
face multe. Sunt sigur că aș putea adăuga exemple și mai amănunțite la aceasta. Dacă vreți mai multe sau dacă aveți o critică constructivă, anunțați-mă comentând mai jos.
Apendice:
Critica răspunsului de top/acceptat**.
- Este confuz cu privire la ceea ce face un iterabil, , folosind doar o listă ca exemplu. A se vedea referințele mele de mai sus, dar pe scurt: un iterabil are un
__iter__
care returnează o metodă iterator. Un iterator oferă un.next
(Python 2 sau.__next__
(Python 3), care este apelată implicit de cătrefor
bucle până când aceasta ridicăStopIteration
, , iar odată ce o face, va continua să o facă. - Apoi folosește o expresie de generator pentru a descrie ce este un generator. Deoarece un generator este pur și simplu o modalitate convenabilă de a crea un iterator, , nu face decât să încurce lucrurile, iar noi încă nu am ajuns la
yield
parte. - În Controlul epuizării unui generator el apelează la
.next
când, în schimb, ar trebui să folosească funcția încorporată,next
. Acesta ar fi un strat de indirecție adecvat, deoarece codul său nu funcționează în Python 3. - Itertools? Acest lucru nu era relevant pentru ceea ce
yield
face, deloc. - Nici o discuție despre metodele care
yield
oferă împreună cu noua funcționalitateyield from
din Python 3. Răspunsul de top/acceptat este un răspuns foarte incomplet.
Critica răspunsului care sugerează yield
într-o expresie sau înțelegere a generatorului.
Gramatica permite în prezent orice expresie într-o înțelegere de listă.
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
Deoarece yield este o expresie, a fost promovată de unii ca fiind interesantă utilizarea ei în comprehensiuni sau expresii generatoare – în ciuda faptului că nu se citează niciun caz de utilizare deosebit de bun.
Dezvoltatorii CPython de bază sunt discută deprecierea permisiunii sale.Iată o postare relevantă din lista de discuții:
Pe 30 ianuarie 2017 la 19:05, Brett Cannon a scris:
On Sun, 29 Jan 2017 at 16:39 Craig Rodrigues wrote:
Sunt de acord cu oricare dintre cele două abordări. Lăsând lucrurile așa cum sunt în Python 3nu este bine, IMHO.
Votul meu este să fie o SyntaxError, deoarece nu obțineți ceea ce vă așteptați de la sintaxă.
Sunt de acord că acesta este un loc rezonabil pentru noi, deoarece orice cod care se bazează pe comportamentul actual este prea inteligent pentru a fi sustenabil.
Pentru a ajunge acolo, probabil că vom dori:
- SyntaxWarning sau DeprecationWarning în 3.7.
- Avertizare Py3k în 2.7.x
- SyntaxError în 3.8
Noroc, Nick.
— Nick Coghlan | ncoghlan at gmail.com | Brisbane, Australia
În plus, există un problemă nerezolvată (10544) care pare să arate în direcția aceasta niciodată fi o idee bună (PyPy, o implementare Python scrisă în Python, ridică deja avertismente de sintaxă).
În concluzie, până când dezvoltatorii CPython ne vor spune altceva: Nu puneți yield
într-o expresie sau înțelegere a unui generator.
Site-ul return
într-o expresie de generator
În Python 2:
Într-o funcție generator, declarația
return
nu are voie să includă un elementexpression_list
. În acest context, un simplureturn
indică faptul că generatorul a terminat și va provocaStopIteration
să fie ridicată.
Un expression_list
este practic orice număr de expresii separate prin virgule – în esență, în Python 2, puteți opri generatorul cu return
, , dar nu puteți returna o valoare.
În Python 3:
Într-o funcție de generator, simbolul
return
indică faptul că generatorul a terminat și va determina caStopIteration
să fie ridicată. Valoarea returnată (dacă există) este utilizată ca argument pentru a construiStopIteration
și devineStopIteration.value
atribut.
Note de subsol
-
Limbajele CLU, Sather și Icon au fost menționate în propunere pentru a introduce conceptul de generatoare în Python. Ideea generală este că o funcție poate menține starea internă și poate produce puncte de date intermediare la cererea utilizatorului. Acest lucru promitea să fie superioară din punct de vedere al performanțelor față de alte abordări, inclusiv threading-ul Python, , care nici măcar nu este disponibil pe unele sisteme.
-
Acest lucru înseamnă, de exemplu, că
range
obiectele nu suntIterator
s, chiar dacă sunt iterabile, deoarece pot fi reutilizate. La fel ca și listele, obiectele lor__iter__
metodele returnează obiecte iterator.
yield
a fost introdus inițial ca instrucțiune, ceea ce înseamnă că putea apărea doar la începutul unei linii într-un bloc de cod.Acum yield
creează o expresie yield.https://docs.python.org/2/reference/simple_stmts.html#grammar-token-yield_stmtAceastă modificare a fost propusă pentru a permite unui utilizator să trimită date în generator la fel cum le-ar putea primi. Pentru a trimite date, trebuie să le putem atribui la ceva, iar pentru aceasta, o expresie pur și simplu nu va funcționa.
- Aaron, în secțiunea pe care ați descris-o
yield from
, , pe a doua linie avețiunder_management = yield # must receive deposited value
. Poți explica ce înseamnă că este egal cuyield
? Se pare că obțineți o valoare, dar nu este clar de unde. – > . - @RicardoBarrosLourenço Am actualizat puțin comentariile acelui cod – pare mai clar acum? – > .
- Frumos. S-a îmbunătățit acum (de la argumentul de trimitere). – > .
yield
este la fel ca return
– returnează orice îi spui (ca generator). Diferența este că, data viitoare când apelați generatorul, execuția începe de la ultimul apel la yield
instrucțiune. Spre deosebire de return, Cadrul stivei nu este curățat atunci când are loc un randament, însă controlul este transferat înapoi la apelant, astfel încât starea sa va fi reluată la următoarea apelare a funcției.
În cazul codului dumneavoastră, funcția get_child_candidates
se comportă ca un iterator, astfel încât atunci când extindeți lista, aceasta adaugă câte un element pe rând la noua listă.
list.extend
apelează un iterator până la epuizarea acestuia. În cazul exemplului de cod pe care l-ați postat, ar fi mult mai clar să se returneze un tuple și să se adauge la listă.
- 112
- Acest lucru este aproape, dar nu este corect. De fiecare dată când apelați o funcție cu o instrucțiune yield în ea, aceasta returnează un obiect generator nou-nouț. Doar atunci când apelați metoda .next() a acelui generator, execuția se reia după ultimul yield. – > .
Mai este un lucru de menționat: o funcție care cedează nu trebuie de fapt să se termine. Am scris un cod de genul acesta:
def fib():
last, cur = 0, 1
while True:
yield cur
last, cur = cur, last + cur
Apoi îl pot folosi în alt cod de genul acesta:
for f in fib():
if some_condition: break
coolfuncs(f);
Chiar ajută la simplificarea unor probleme și face unele lucruri mai ușor de lucrat.
Pentru cei care preferă un exemplu minimal de lucru, meditați la această sesiune interactivă Python:
>>> def f():
... yield 1
... yield 2
... yield 3
...
>>> g = f()
>>> for i in g:
... print(i)
...
1
2
3
>>> for i in g:
... print(i)
...
>>> # Note that this time nothing was printed
TL;DR
În loc de asta:
def square_list(n):
the_list = [] # Replace
for x in range(n):
y = x * x
the_list.append(y) # these
return the_list # lines
faceți asta:
def square_yield(n):
for x in range(n):
y = x * x
yield y # with this one.
Ori de câte ori vă găsiți construind o listă de la zero, yield
fiecare piesă în parte.
Acesta a fost primul meu moment „aha” cu randamentul.
yield
este un zaharat mod de a spune
a construi o serie de lucruri
Același comportament:
>>> for square in square_list(4):
... print(square)
...
0
1
4
9
>>> for square in square_yield(4):
... print(square)
...
0
1
4
9
Comportament diferit:
Randamentul este o singură trecere: se poate itera doar o singură dată. Atunci când o funcție are un randament în ea, o numim funcție generatoare. Iar un iterator este ceea ce returnează. Acești termeni sunt revelatori. Pierdem comoditatea unui container, dar câștigăm puterea unei serii care este calculată în funcție de necesități și care este arbitrar de lungă.
Randamentul este leneș, , amână calculul. O funcție cu un randament în ea nu se execută de fapt deloc atunci când o apelați. Ea returnează un obiect iterator care își amintește unde a rămas. De fiecare dată când apelați next()
pe iterator (acest lucru se întâmplă într-o buclă de tip „for-loop”), execuția avansează până la următorul randament. return
ridică StopIteration și încheie seria (acesta este finalul natural al unei bucle for-loop).
Randamentul este versatil. Datele nu trebuie să fie stocate toate la un loc, ci pot fi puse la dispoziție pe rând. Poate fi infinită.
>>> def squares_all_of_them():
... x = 0
... while True:
... yield x * x
... x += 1
...
>>> squares = squares_all_of_them()
>>> for _ in range(4):
... print(next(squares))
...
0
1
4
9
Dacă aveți nevoie de mai multe treceri și seria nu este prea lungă, apelați pur și simplu list()
pe ea:
>>> list(square_yield(4))
[0, 1, 4, 9]
O alegere strălucită a cuvântului yield
pentru că ambele sensuri se aplică:
randament – a produce sau a furniza (ca în agricultură)
…furnizează următoarele date din serie.
randament – a ceda sau a renunța (ca în cazul puterii politice)
…renunță la execuția CPU până când iteratorul avansează.
Yield vă oferă un generator.
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
După cum puteți vedea, în primul caz foo
ține întreaga listă în memorie deodată. Nu este o mare problemă pentru o listă cu 5 elemente, dar ce se întâmplă dacă doriți o listă de 5 milioane? Nu numai că acest lucru este un mare mâncător de memorie, dar costă și foarte mult timp pentru a fi construit în momentul în care funcția este apelată.
În al doilea caz, bar
vă oferă doar un generator. Un generator este un iterabil – ceea ce înseamnă că îl puteți folosi într-o funcție for
buclă, etc., dar fiecare valoare poate fi accesată o singură dată. De asemenea, toate valorile nu sunt stocate în memorie în același timp; obiectul generator „își amintește” unde se afla în buclă ultima dată când l-ați apelat – în acest fel, dacă utilizați un iterabil pentru a număra (să zicem) până la 50 de miliarde, nu trebuie să numărați până la 50 de miliarde deodată și să stocați cele 50 de miliarde de numere prin care să numărați.
Din nou, acesta este un exemplu destul de artificial, probabil că ați folosi itertools dacă ați dori cu adevărat să numărați până la 50 de miliarde. 🙂
Acesta este cel mai simplu caz de utilizare a generatoarelor. După cum ați spus, poate fi folosit pentru a scrie permutări eficiente, folosind yield pentru a împinge lucrurile în sus prin stiva de apeluri în loc să folosiți un fel de variabilă de stivă. Generatorii pot fi, de asemenea, utilizați pentru traversarea specializată a arborilor și pentru tot felul de alte lucruri.
- Doar o notă – în Python 3,
range
returnează, de asemenea, un generator în loc de o listă, astfel încât veți vedea o idee similară, cu excepția faptului că__repr__
/__str__
sunt suprapuse pentru a afișa un rezultat mai frumos, în acest cazrange(1, 10, 2)
. – > .
Întoarce un generator. Nu sunt deosebit de familiarizat cu Python, dar cred că este același lucru ca și în cazul lui blocurile iterator din C# dacă sunteți familiarizați cu acestea.
Ideea cheie este că compilatorul/interpretorul/ceva face o șmecherie astfel încât, în ceea ce privește apelantul, acesta poate continua să apeleze next() și va continua să returneze valori – ca și cum metoda generatorului ar fi fost în pauză. Evident, nu puteți „pune pe pauză” o metodă, așa că compilatorul construiește o mașină de stare pentru a vă aminti unde vă aflați în prezent și cum arată variabilele locale etc. Acest lucru este mult mai ușor decât să scrieți singur un iterator.
Există un tip de răspuns care nu cred că a fost dat încă, printre numeroasele răspunsuri excelente care descriu cum să folosești generatoarele. Aici este răspunsul la teoria limbajului de programare:
The yield
din Python returnează un generator. Un generator în Python este o funcție care returnează continuări (și, în special, un tip de corutină, dar continuările reprezintă un mecanism mai general pentru a înțelege ce se întâmplă).
În teoria limbajelor de programare, continuările reprezintă un tip de calcul mult mai fundamental, dar nu sunt utilizate prea des, deoarece sunt extrem de greu de argumentat și, de asemenea, foarte greu de implementat. Dar ideea de continuare este simplă: este starea unui calcul care nu s-a terminat încă. În această stare, sunt salvate valorile curente ale variabilelor, operațiile care nu au fost încă efectuate și așa mai departe. Apoi, la un moment dat, mai târziu în program, continuarea poate fi invocată, astfel încât variabilele programului să fie readuse în starea respectivă și să fie efectuate operațiile care au fost salvate.
Continuările, în această formă mai generală, pot fi implementate în două moduri. În cadrul call/cc
mod, stiva programului este literalmente salvată și apoi, atunci când este invocată continuarea, stiva este restaurată.
În stilul de transmitere a continuării (CPS), continuările sunt doar funcții normale (numai în limbajele în care funcțiile sunt de primă clasă) pe care programatorul le gestionează în mod explicit și le transmite subrutine. În acest stil, starea programului este reprezentată de închideri (și de variabilele care se întâmplă să fie codificate în acestea) mai degrabă decât de variabile care se află undeva pe stivă. Funcțiile care gestionează fluxul de control acceptă continuări ca argumente (în unele variante de CPS, funcțiile pot accepta mai multe continuări) și manipulează fluxul de control prin invocarea lor prin simpla lor apelare și întoarcere ulterioară. Un exemplu foarte simplu de stil de trecere a continuității este următorul:
def save_file(filename):
def write_file_continuation():
write_stuff_to_file(filename)
check_if_file_exists_and_user_wants_to_overwrite(write_file_continuation)
În acest exemplu (foarte simplist), programatorul salvează operațiunea de scriere efectivă a fișierului într-o continuare (care poate fi, potențial, o operațiune foarte complexă, cu multe detalii de scris) și apoi transmite acea continuare (adică, ca o închidere de primă clasă) unui alt operator care face mai multă procesare și apoi o apelează, dacă este necesar. (Folosesc mult acest model de proiectare în programarea GUI reală, fie pentru că îmi economisește linii de cod, fie, mai important, pentru a gestiona fluxul de control după declanșarea evenimentelor GUI).
Restul acestei postări va conceptualiza, fără a pierde din generalitate, continuările ca CPS, deoarece este al naibii de ușor de înțeles și de citit.
Acum să vorbim despre generatoare în Python. Generatorii sunt un subtip specific de continuare. În timp ce continuările sunt capabile, în general, să salveze starea unei calcul (adică, stiva de apeluri a programului), generatoarele sunt capabile să salveze doar starea iterației asupra unui iterator. Cu toate acestea, această definiție este ușor înșelătoare pentru anumite cazuri de utilizare a generatoarelor. De exemplu:
def f():
while True:
yield 4
Acesta este în mod clar un iterabil rezonabil, al cărui comportament este bine definit – de fiecare dată când generatorul itera peste el, acesta returnează 4 (și face acest lucru pentru totdeauna). Dar probabil că nu este tipul prototipic de iterabil care ne vine în minte atunci când ne gândim la iteratori (de ex, for x in collection: do_something(x)
). Acest exemplu ilustrează puterea generatoarelor: dacă ceva este un iterator, un generator poate salva starea iterației sale.
Pentru a reitera: Continuările pot salva starea stivei unui program, iar generatorii pot salva starea iterației. Acest lucru înseamnă că continuările sunt mult mai puternice decât generatoarele, dar și că generatoarele sunt mult, mult mai ușoare. Sunt mai ușor de implementat pentru proiectantul limbajului și sunt mai ușor de utilizat pentru programator (dacă aveți ceva timp liber, încercați să citiți și să înțelegeți această pagină despre continuări și call/cc).
Dar ați putea cu ușurință să implementați (și să conceptualizați) generatoarele ca un caz simplu și specific al stilului de trecere a continuităților:
Ori de câte ori yield
este apelată, aceasta îi spune funcției să returneze o continuare. Când funcția este apelată din nou, aceasta începe de unde a rămas. Astfel, în pseudo-pseudocod (adică nu pseudocod, dar nici cod), generatorul next
este, în principiu, următoarea:
class Generator():
def __init__(self,iterable,generatorfun):
self.next_continuation = lambda:generatorfun(iterable)
def next(self):
value, next_continuation = self.next_continuation()
self.next_continuation = next_continuation
return value
unde yield
este de fapt un zahăr sintactic pentru funcția reală a generatorului, practic ceva de genul:
def generatorfun(iterable):
if len(iterable) == 0:
raise StopIteration
else:
return (iterable[0], lambda:generatorfun(iterable[1:]))
Rețineți că acesta este doar pseudocod și că implementarea reală a generatoarelor în Python este mai complexă. Dar, ca un exercițiu pentru a înțelege ce se întâmplă, încercați să utilizați stilul de trecere a continuității pentru a implementa obiectele generatorului fără a utiliza funcția yield
cuvântul cheie.
Iată un exemplu în limbaj simplu. Voi oferi o corespondență între conceptele umane de nivel înalt și conceptele Python de nivel scăzut.
Vreau să operez asupra unei secvențe de numere, dar nu vreau să mă deranjez cu crearea acelei secvențe, ci vreau să mă concentrez doar pe operația pe care vreau să o fac. Așadar, fac următoarele:
- Vă sun și vă spun că vreau o secvență de numere care să fie produsă într-un anumit mod și vă anunț care este algoritmul.
Această etapă corespundedef
inarea funcției generatoare, adică funcția care conține oyield
. - Ceva mai târziu, vă spun: „OK, pregătește-te să-mi spui secvența de numere”.
Această etapă corespunde apelării funcției generator care returnează un obiect generator. Rețineți că nu-mi spuneți încă niciun număr; doar luați hârtia și creionul. - Eu vă întreb „spuneți-mi următorul număr”, iar dumneavoastră îmi spuneți primul număr; după aceea, așteptați să vă cer următorul număr. Este treaba ta să îți amintești unde ai fost, ce numere ai spus deja și care este următorul număr. Nu mă interesează detaliile.
Această etapă corespunde apelării.next()
la obiectul generator. - … repetați pasul anterior, până când…
- în cele din urmă, s-ar putea să ajungeți la un final. Nu-mi spuneți un număr; doar strigați: „Stați pe loc! Am terminat! Gata cu numerele!”
Acest pas corespunde obiectului generator care își termină treaba și ridică unStopIteration
excepție Funcția generatoare nu trebuie să ridice excepția. Aceasta este ridicată automat atunci când funcția se termină sau emite oreturn
.
Aceasta este ceea ce face un generator (o funcție care conține o funcție yield
); începe să se execute, se oprește ori de câte ori face o yield
, , iar atunci când i se cere un .next()
valoare, el continuă din punctul în care a fost ultima dată. Se potrivește perfect, prin concepție, cu protocolul iterator din Python, care descrie modul de solicitare secvențială a valorilor.
Cel mai cunoscut utilizator al protocolului iterator este aplicația for
din Python. Așadar, ori de câte ori faceți un:
for item in sequence:
nu contează dacă sequence
este o listă, un șir de caractere, un dicționar sau un generator obiect așa cum am descris mai sus; rezultatul este același: citiți elementele dintr-o secvență, unul câte unul.
Rețineți că def
inând o funcție care conține un yield
nu este singurul mod de a crea un generator; este doar cel mai simplu mod de a crea unul.
Pentru informații mai precise, citiți despre tipuri de iteratori, , pagina de declarația yield și generatori în documentația Python.
În timp ce o mulțime de răspunsuri arată de ce ați folosi un yield
pentru a crea un generator, există mai multe utilizări pentru yield
. Este destul de ușor să creezi o cortină, care permite trecerea de informații între două blocuri de cod. Nu voi repeta niciunul dintre exemplele frumoase care au fost deja date despre utilizarea lui yield
pentru a crea un generator.
Pentru a vă ajuta să înțelegeți mai bine ce înseamnă un yield
face în codul următor, puteți folosi degetul pentru a urmări ciclul prin orice cod care are un yield
. De fiecare dată când degetul dvs. lovește butonul yield
, , trebuie să așteptați pentru a next
sau a send
să fie introdus. Atunci când a next
este apelată, urmăriți codul până când atingeți tasta yield
… codul din partea dreaptă a yield
este evaluat și returnat celui care l-a apelat… apoi se așteaptă. Când next
este apelat din nou, se execută o altă buclă prin cod. Cu toate acestea, veți observa că într-o cortină, yield
poate fi utilizat și cu un send
… care va trimite o valoare de la apelant în funcția care produce randamentul. În cazul în care un send
este dată, atunci yield
primește valoarea trimisă și o scuipă pe partea stângă… apoi urmărirea prin cod progresează până când se ajunge la funcția yield
din nou (returnând valoarea la sfârșit, ca și cum next
a fost apelată).
De exemplu:
>>> def coroutine():
... i = -1
... while True:
... i += 1
... val = (yield i)
... print("Received %s" % val)
...
>>> sequence = coroutine()
>>> sequence.next()
0
>>> sequence.next()
Received None
1
>>> sequence.send('hello')
Received hello
2
>>> sequence.close()
- Drăguț! A trambulină (în sensul Lisp). Nu se văd prea des așa ceva! – > .
Mai există o altă yield
utilizare și semnificație (începând cu Python 3.3):
yield from <expr>
De la PEP 380 — Sintaxa pentru delegarea către un subgenerator:
Se propune o sintaxă pentru ca un generator să delege o parte din operațiunile sale unui alt generator. Acest lucru permite ca o secțiune de cod care conține „yield” să fie eliminată și plasată într-un alt generator. În plus, subgeneratorului i se permite să se întoarcă cu o valoare, iar valoarea este pusă la dispoziția generatorului care deleagă.
Noua sintaxă deschide, de asemenea, unele oportunități de optimizare atunci când un generator returnează valorile produse de un alt generator.
În plus, acest va introduce (începând cu Python 3.5):
async def new_coroutine(data):
...
await blocking_action()
pentru a evita confundarea corutinelor cu un generator obișnuit (astăzi yield
este utilizat în ambele).
Toate răspunsurile sunt excelente, totuși un pic dificil pentru începători.
Presupun că ați învățat return
declarația.
Ca o analogie, return
și yield
sunt gemeni. return
înseamnă „a se întoarce și a se opri”, în timp ce „yield` înseamnă „a se întoarce, dar a continua”.
- Încercați să obțineți o listă num_list cu
return
.
def num_list(n):
for i in range(n):
return i
Executați-l:
In [5]: num_list(3)
Out[5]: 0
Vedeți, veți obține un singur număr, în loc de o listă de numere. return
nu vă permite niciodată să prevaleze în mod fericit, doar implementează o dată și renunță.
- Apare
yield
Înlocuiește return
cu 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]
Acum, câștigi pentru a obține toate numerele.
Comparând cu return
care rulează o singură dată și se oprește, yield
rulează de câte ori ai planificat.Poți interpreta return
ca return one of them
, , și yield
ca return all of them
. Acest lucru se numește iterable
.
- Încă un pas și putem rescrie
yield
cureturn
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]
Este vorba de nucleul despre yield
.
Diferența dintre o listă return
de ieșire și obiectul yield
de ieșire este:
Veți obține întotdeauna [0, 1, 2] dintr-o listă de obiecte, dar nu le veți putea prelua decât din „obiectul”. yield
ieșire” o singură dată. Prin urmare, are un nou nume generator
obiect, așa cum este afișat în Out[11]: <generator object num_list at 0x10327c990>
.
În concluzie, ca o metaforă pentru a o înțelege:
return
șiyield
sunt gemenilist
șigenerator
sunt gemeni
- Acest lucru este de înțeles, dar o diferență majoră este că puteți avea mai multe randamente într-o funcție/metodă. Analogia se întrerupe complet în acest punct. Randamentul își amintește locul într-o funcție, astfel încât, data viitoare când apelați next(), funcția dvs. continuă cu următoarea funcție.
yield
. Cred că acest lucru este important și ar trebui să fie exprimat. – > .
Din punct de vedere al programării, iteratorii sunt implementați ca thunks.
Pentru a implementa iteratorii, generatorii și grupurile de fire pentru execuție simultană etc. ca thunks, se utilizează mesaje trimise către un obiect de închidere, , care are un dispecerat, iar dispecerul răspunde la „mesaje”.
„next„ este un mesaj trimis către o închidere, creată de către „iter„.
Există o mulțime de moduri de a implementa acest calcul. Eu am folosit mutația, dar este posibil să se facă acest tip de calcul fără mutație, returnând valoarea curentă și următorul randament (ceea ce îl face pe transparență referențială). Racket utilizează o secvență de transformări ale programului inițial în unele limbaje intermediare, una dintre aceste rescrieri făcând ca operatorul de randament să fie transformat într-un limbaj cu operatori mai simpli.
Iată o demonstrație a modului în care ar putea fi rescris yield, care utilizează structura R6RS, dar semantica este identică cu cea din Python. Este vorba de același model de calcul și este necesară doar o modificare a sintaxei pentru a-l rescrie folosind yield din Python.
Welcome to Racket v6.5.0.3. -> (define gen (lambda (l) (define yield (lambda () (if (null? l) 'END (let ((v (car l))) (set! l (cdr l)) v)))) (lambda(m) (case m ('yield (yield)) ('init (lambda (data) (set! l data) 'OK)))))) -> (define stream (gen '(1 2 3))) -> (stream 'yield) 1 -> (stream 'yield) 2 -> (stream 'yield) 3 -> (stream 'yield) 'END -> ((stream 'init) '(a b)) 'OK -> (stream 'yield) 'a -> (stream 'yield) 'b -> (stream 'yield) 'END -> (stream 'yield) 'END ->
Iată câteva exemple Python despre cum se implementează efectiv generatoarele ca și cum Python nu ar oferi zahăr sintactic pentru ele:
Ca un generator 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))
Folosind închideri lexicale în loc de generatoare
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)
Utilizarea închiderilor de obiecte în loc de generatoare (deoarece ClosuresAndObjectsAreAreEquivalent)
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)
Aveam de gând să postez „citește pagina 19 din cartea lui Beazley ‘Python: Essential Reference’ pentru o descriere rapidă a generatoarelor”, dar atât de mulți alții au postat deja descrieri bune.
De asemenea, rețineți că yield
pot fi utilizate în corutine ca dual al utilizării lor în funcțiile generatoare. Deși nu este aceeași utilizare ca în cazul fragmentului dvs. de cod, (yield)
poate fi utilizat ca expresie într-o funcție. Atunci când un apelant trimite o valoare metodei folosind expresia send()
metoda, atunci corutina se va executa până la următoarea (yield)
instrucțiune este întâlnită.
Generatoarele și corutinele sunt o modalitate interesantă de a configura aplicații de tip flux de date. M-am gândit că ar fi util să știți despre cealaltă utilizare a yield
în funcții.
Iată un exemplu simplu:
def isPrimeNumber(n):
print "isPrimeNumber({}) call".format(n)
if n==1:
return False
for x in range(2,n):
if n % x == 0:
return False
return True
def primes (n=1):
while(True):
print "loop step ---------------- {}".format(n)
if isPrimeNumber(n): yield n
n += 1
for n in primes():
if n> 10:break
print "wiriting result {}".format(n)
Output:
loop step ---------------- 1
isPrimeNumber(1) call
loop step ---------------- 2
isPrimeNumber(2) call
loop step ---------------- 3
isPrimeNumber(3) call
wiriting result 3
loop step ---------------- 4
isPrimeNumber(4) call
loop step ---------------- 5
isPrimeNumber(5) call
wiriting result 5
loop step ---------------- 6
isPrimeNumber(6) call
loop step ---------------- 7
isPrimeNumber(7) call
wiriting result 7
loop step ---------------- 8
isPrimeNumber(8) call
loop step ---------------- 9
isPrimeNumber(9) call
loop step ---------------- 10
isPrimeNumber(10) call
loop step ---------------- 11
isPrimeNumber(11) call
Nu sunt un dezvoltator Python, dar mi se pare că yield
ține poziția fluxului programului și următoarea buclă începe din poziția „yield”. Se pare că așteaptă la acea poziție și, chiar înainte de aceasta, returnează o valoare în afara, iar data viitoare continuă să lucreze.
Pare a fi o abilitate interesantă și plăcută 😀
- Aveți dreptate. Dar care este efectul asupra fluxului care este de a vedea comportamentul „yield” ? Pot să schimb algoritmul în numele matematicii. Va ajuta la obținerea unei evaluări diferite a „randamentului” ? – > .
Iată o imagine mentală a ceea ce yield
face.
Îmi place să mă gândesc la un fir de execuție ca având o stivă (chiar și atunci când nu este implementat în acest fel).
Atunci când este apelată o funcție normală, aceasta își pune variabilele locale pe stivă, face niște calcule, apoi șterge stiva și se întoarce. Valorile variabilelor sale locale nu mai sunt văzute niciodată.
Cu o funcție yield
atunci când codul său începe să ruleze (adică după ce funcția este apelată, returnând un obiect generator, al cărui obiect next()
este apoi invocată), aceasta își plasează în mod similar variabilele locale pe stivă și efectuează calcule pentru o perioadă de timp. Dar apoi, atunci când atinge punctul yield
înainte de a șterge partea sa din stivă și de a se întoarce, aceasta ia o imagine instantanee a variabilelor sale locale și le stochează în obiectul generator. De asemenea, acesta notează locul în care se află în prezent în codul său (și anume, punctul specific yield
declarație).
Deci este un fel de funcție înghețată de care se agață generatorul.
Când next()
este apelat ulterior, acesta recuperează lucrurile care aparțin funcției pe stivă și o reanimează. Funcția continuă să calculeze de unde a rămas, fără să țină cont de faptul că tocmai a petrecut o veșnicie în depozitul rece.
Comparați următoarele exemple:
def normalFunction():
return
if False:
pass
def yielderFunction():
return
if False:
yield 12
Atunci când apelăm a doua funcție, aceasta se comportă foarte diferit de prima. yield
ar putea fi inaccesibilă, dar dacă este prezentă oriunde, schimbă natura lucrurilor cu care avem de-a face.
>>> yielderFunction()
<generator object yielderFunction at 0x07742D28>
Apelarea yielderFunction()
nu îi execută codul, ci face din cod un generator. (Poate că este o idee bună să numim astfel de lucruri cu yielder
prefix pentru ușurința de citire).
>>> 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']
La gi_code
și gi_frame
sunt câmpurile în care este stocată starea înghețată. Explorarea lor cu dir(..)
, , putem confirma că modelul nostru mental de mai sus este credibil.
Un exemplu simplu pentru a înțelege despre ce este vorba: yield
def f123():
for _ in range(4):
yield 1
yield 2
for i in f123():
print (i)
Ieșirea este:
1 2 1 2 1 2 1 2
- Nu ar fi tipărit doar pe o singură linie dacă ați fi rulat instrucțiunea print folosind
print(i, end=' ')
? Altfel, cred că comportamentul implicit ar pune fiecare număr pe o linie nouă – > . - @user9074332, Aveți dreptate, dar este scris pe o singură linie pentru a facilita înțelegerea – > .
Așa cum sugerează fiecare răspuns, yield
este utilizat pentru crearea unui generator de secvențe. Este folosit pentru generarea unor secvențe în mod dinamic. De exemplu, în timp ce citiți un fișier linie cu linie pe o rețea, puteți utiliza funcția yield
funcție după cum urmează:
def getNextLines():
while con.isOpen():
yield con.read()
Puteți să o utilizați în codul dvs. după cum urmează:
for line in getNextLines():
doSomeThing(line)
Execution Control Transfer gotcha
Controlul execuției va fi transferat de la getNextLines() la funcția for
buclă atunci când se execută yield. Astfel, de fiecare dată când getNextLines() este invocată, execuția începe din punctul în care a fost întreruptă ultima dată.
Astfel, pe scurt, o funcție cu următorul cod
def simpleYield():
yield "first time"
yield "second time"
yield "third time"
yield "Now some useful value {}".format(12)
for i in simpleYield():
print i
va imprima
"first time"
"second time"
"third time"
"Now some useful value 12"
(Răspunsul meu de mai jos vorbește doar din perspectiva utilizării generatorului Python, nu și a implementării subiacente a mecanismului generatorului, care implică unele trucuri de manipulare a stivei și a heap-ului).
Atunci când yield
este utilizat în loc de return
într-o funcție python, funcția respectivă este transformată în ceva special numit generator function
. Funcția respectivă va returna un obiect de tip generator
tip. Adresa yield
este un indicator care notifică compilatorul python să trateze această funcție în mod special. Funcțiile normale se vor încheia odată ce o anumită valoare este returnată de la aceasta. Dar, cu ajutorul compilatorului, funcția generatoare poate fi considerată ca fiind ca fiind reluabilă. Adică, contextul de execuție va fi restabilit și execuția va continua de la ultima execuție. Până la apelarea explicită a funcției return, care va ridica un apel de tip StopIteration
excepție (care face parte, de asemenea, din protocolul iteratorului), sau dacă ajungeți la sfârșitul funcției. Am găsit o mulțime de referințe despre generator
dar aceasta una de la functional programming perspective
este cea mai digerabilă.
(Acum vreau să vorbesc despre raționamentul din spatele generator
, , și despre iterator
pe baza propriei mele înțelegeri. Sper că acest lucru vă poate ajuta să înțelegeți motivația esențială a iteratorului și a generatorului. Un astfel de concept apare și în alte limbaje, cum ar fi C#).
Din câte am înțeles, atunci când dorim să procesăm o mulțime de date, de obicei, mai întâi stocăm datele undeva și apoi le procesăm una câte una. Dar acest naivă abordare este problematică. Dacă volumul de date este uriaș, este costisitor să le stocăm în prealabil ca un întreg. Așadar, în loc să stocăm data
în sine în mod direct, de ce să nu stocăm un fel de metadata
indirect, adică the logic how the data is computed
.
Există 2 abordări pentru a înfășura astfel de metadate.
- Abordarea OO, în care înfășurăm metadatele
as a class
. Aceasta este așa-numitaiterator
care implementează protocolul iterator (adică protocolul__next__()
, , și__iter__()
metode). Acesta este, de asemenea, cel mai des întâlnit modelul de proiectare al iteratorilor. - Abordarea funcțională, înfășurăm metadatele
as a function
. Acesta este așa-numitulgenerator function
. Dar, sub capotă, datele returnategenerator object
este încă returnatIS-A
iterator, deoarece implementează și protocolul iterator.
În orice caz, este creat un iterator, adică un obiect care vă poate oferi datele dorite. Abordarea OO poate fi un pic mai complexă. Oricum, care dintre ele să o folosiți depinde de dumneavoastră.
În concluzie, metoda yield
transformă funcția dvs. într-o fabrică care produce un obiect special numit generator
care se înfășoară în jurul corpului funcției dvs. originale. Atunci când generator
este iterată, aceasta execută funcția dvs. până când ajunge la următoarea yield
apoi suspendă execuția și o evaluează la valoarea transmisă către yield
. Se repetă acest proces la fiecare iterație până când calea de execuție iese din funcție. De exemplu,
def simple_generator():
yield 'one'
yield 'two'
yield 'three'
for i in simple_generator():
print i
emite pur și simplu
one
two
three
Puterea provine din utilizarea generatorului cu o buclă care calculează o secvență, generatorul execută bucla oprindu-se de fiecare dată pentru a „ceda” următorul rezultat al calculului, în acest fel el calculează o listă din mers, avantajul fiind memoria economisită pentru calculele deosebit de mari
Să presupunem că doriți să creați un generator propriu range
care să producă un interval iterabil de numere, ați putea proceda în felul următor,
def myRangeNaive(i):
n = 0
range = []
while n < i:
range.append(n)
n = n + 1
return range
și să o folosiți astfel;
for i in myRangeNaive(10):
print i
Dar acest lucru este ineficient deoarece
- creați o matrice pe care o utilizați o singură dată (ceea ce înseamnă risipă de memorie)
- Acest cod face, de fapt, o buclă de două ori peste acel array! 🙁
Din fericire, Guido și echipa sa au fost suficient de generoși pentru a dezvolta generatoare astfel încât să putem face doar acest lucru;
def myRangeSmart(i):
n = 0
while n < i:
yield n
n = n + 1
return
for i in myRangeSmart(10):
print i
Acum, la fiecare iterație, o funcție de pe generator numită next()
execută funcția până când fie ajunge la o instrucțiune „yield” în care se oprește și „cedează” valoarea, fie ajunge la sfârșitul funcției. În acest caz, la primul apel, next()
se execută până la declarația yield și produce „n”, la următorul apel va executa declarația de incrementare, va sări înapoi la „while”, o va evalua și, dacă este adevărată, se va opri și va produce din nou „n”, continuând astfel până când condiția while revine la fals și generatorul sare la sfârșitul funcției.
Yield este un obiect
A return
dintr-o funcție va returna o singură valoare.
Dacă se dorește ca o funcție să returneze un set imens de valori, , utilizați yield
.
Și mai important, yield
este un barieră.
ca și bariera din limbajul CUDA, nu va transfera controlul până când nu se finalizează.
Adică, va rula codul din funcția dvs. de la început până când va ajunge la yield
. Apoi, va returna prima valoare din buclă.
Apoi, fiecare alt apel va rula bucla pe care ați scris-o în funcție încă o dată, returnând următoarea valoare până când nu mai există nicio valoare de returnat.
Mulți oameni folosesc return
în loc de yield
, , dar în unele cazuri yield
poate fi mai eficient și mai ușor de utilizat.
Iată un exemplu care yield
este cu siguranță cel mai bun pentru:
return (în funcție)
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
randament (în funcție)
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.
Apelarea funcțiilor
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)
Ambele funcții fac același lucru, dar yield
utilizează trei linii în loc de cinci și are o variabilă mai puțin de care trebuie să se preocupe.
Acesta este rezultatul din cod:
După cum puteți vedea, ambele funcții fac același lucru. Singura diferență este return_dates()
oferă o listă, iar yield_dates()
dă un generator.
Un exemplu din viața reală ar fi ceva de genul citirii unui fișier linie cu linie sau dacă doriți doar să creați un generator.
yield
este ca un element de întoarcere pentru o funcție. Diferența constă în faptul că yield
element transformă o funcție într-un generator. Un generator se comportă la fel ca o funcție până când ceva este „cedat”. Generatorul se oprește până la următorul apel și continuă exact din același punct din care a început. Puteți obține o secvență a tuturor valorilor „cedate” într-o singură secvență, prin apelarea funcției list(generator())
.
yield
colectează pur și simplu rezultatele returnate. Gândiți-vă la yield
ca la return +=
Iată un simplu yield
abordare simplă, pentru a calcula seria fibonacci, explicată:
def fib(limit=50):
a, b = 0, 1
for i in range(limit):
yield b
a, b = b, a+b
Când introduceți acest lucru în REPL și apoi încercați să îl apelați, veți obține un rezultat mistificator:
>>> fib()
<generator object fib at 0x7fa38394e3b8>
Acest lucru se datorează faptului că prezența lui yield
a semnalat lui Python că doriți să creați un fișier generator, , adică un obiect care generează valori la cerere.
Așadar, cum generați aceste valori? Acest lucru se poate face fie direct, utilizând funcția încorporată next
, , fie, indirect, alimentând o construcție care consumă valori.
Utilizarea funcției încorporate next()
se invocă direct funcția .next
/__next__
, forțând generatorul să producă o valoare:
>>> g = fib()
>>> next(g)
1
>>> next(g)
1
>>> next(g)
2
>>> next(g)
3
>>> next(g)
5
Indirect, dacă furnizați fib
la un for
bucla, o list
inițializator, un tuple
inițializator sau orice altceva care așteaptă un obiect care generează/produce valori, veți „consuma” generatorul până când acesta nu mai poate produce nicio valoare (și se întoarce):
results = []
for i in fib(30): # consumes fib
results.append(i)
# can also be accomplished with
results = list(fib(30)) # consumes fib
În mod similar, cu un tuple
inițializator:
>>> tuple(fib(5)) # consumes fib
(1, 1, 2, 3, 5)
Un generator diferă de o funcție în sensul că este leneș. El realizează acest lucru prin menținerea stării sale locale și prin faptul că vă permite să o reluați ori de câte ori aveți nevoie.
Atunci când se invocă pentru prima dată fib
prin apelarea acestuia:
f = fib()
Python compilează funcția, întâlnește yield
cuvântul cheie și pur și simplu vă returnează un obiect generator. Se pare că nu este foarte util.
Atunci când îi solicitați apoi generează prima valoare, direct sau indirect, execută toate instrucțiunile pe care le găsește, până când întâlnește un yield
, , apoi returnează valoarea pe care ați furnizat-o la yield
și se oprește. Pentru un exemplu care să demonstreze mai bine acest lucru, să folosim niște print
apeluri (înlocuiți cu print "text"
if pe 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.")
Acum, intrați în REPL:
>>> gen = yielder("Hello, yield!")
aveți acum un obiect generator care așteaptă o comandă pentru ca acesta să genereze o valoare. Utilizați next
și vedeți ce se tipărește:
>>> 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!'
Rezultatele necotate sunt ceea ce se tipărește. Rezultatul citat este ceea ce este returnat de la yield
. Apelați la next
din nou acum:
>>> 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!'
Generatorul își amintește că a fost întrerupt la yield value
și își reia activitatea de acolo. Următorul mesaj este tipărit, iar căutarea pentru yield
declarație la care să se facă pauză se efectuează din nou (datorită while
buclă).
Încă un TL;DR
Iterator pe listă: next()
returnează următorul element al listei
Generator de iteratori: next()
va calcula următorul element din mers (execută codul)
Puteți vedea yield/generator ca pe o modalitate de a executa manual fluxul de control din exterior (cum ar fi continuarea buclei cu un pas), prin apelarea next
, , oricât de complex ar fi fluxul.
Notă:: Generatorul este NU ESTE o funcție normală. Acesta își amintește starea anterioară ca și variabilele locale (stivă). Consultați alte răspunsuri sau articole pentru explicații detaliate. Generatorul poate fi doar iterat o singură dată. Ați putea face fără yield
, , dar nu ar fi la fel de frumos, așa că poate fi considerat un zahăr lingvistic „foarte frumos”.
yield este similar cu return. Diferența este următoarea:
yield face ca o funcție să fie iterabilă (în următorul exemplu primes(n = 1)
funcția devine iterabilă).
Ceea ce înseamnă, în esență, că data viitoare când funcția este apelată, aceasta va continua de unde a rămas (adică după linia de yield expression
).
def isprime(n):
if n == 1:
return False
for x in range(2, n):
if n % x == 0:
return False
else:
return True
def primes(n = 1):
while(True):
if isprime(n): yield n
n += 1
for n in primes():
if n > 100: break
print(n)
În exemplul de mai sus, dacă isprime(n)
este adevărat, aceasta va returna numărul prim. În următoarea iterație, va continua de la următoarea linie
n += 1
O analogie ar putea ajuta la înțelegerea ideii de aici:
Imaginați-vă că ați creat o mașinărie uimitoare, capabilă să producă mii și mii de becuri pe zi. Mașina generează aceste becuri în cutii cu un număr de serie unic. Nu aveți suficient spațiu pentru a stoca toate aceste becuri în același timp (adică nu puteți ține pasul cu viteza mașinii din cauza limitării stocării), așa că ați dori să ajustați această mașină pentru a genera becuri la cerere.
Generatoarele Python nu diferă prea mult de acest concept.
Imaginați-vă că aveți o funcție x
care generează numere de serie unice pentru cutii. Evident, puteți avea un număr foarte mare de astfel de coduri de bare generate de funcție. O opțiune mai înțeleaptă, și eficientă din punct de vedere al spațiului, este să generați aceste numere de serie la cerere.
Codul mașinii:
def barcode_generator():
serial_number = 10000 # Initial barcode
while True:
yield serial_number
serial_number += 1
barcode = barcode_generator()
while True:
number_of_lightbulbs_to_generate = int(input("How many lightbulbs to generate? "))
barcodes = [next(barcode) for _ in range(number_of_lightbulbs_to_generate)]
print(barcodes)
# function_to_create_the_next_batch_of_lightbulbs(barcodes)
produce_more = input("Produce more? [Y/n]: ")
if produce_more == "n":
break
După cum puteți vedea, avem o „funcție” autonomă pentru a genera de fiecare dată următorul număr de serie unic. Această funcție returnează înapoi un generator! După cum puteți vedea, nu apelăm funcția de fiecare dată când avem nevoie de un nou număr de serie, ci folosim next()
generatorul pentru a obține următorul număr de serie.
Ieșire:
How many lightbulbs to generate? 5
[10000, 10001, 10002, 10003, 10004]
Produce more? [Y/n]: y
How many lightbulbs to generate? 6
[10005, 10006, 10007, 10008, 10009, 10010]
Produce more? [Y/n]: y
How many lightbulbs to generate? 7
[10011, 10012, 10013, 10014, 10015, 10016, 10017]
Produce more? [Y/n]: n
yield
produce ceva. Este ca și cum cineva ți-ar cere să faci 5 prăjituri. Dacă ați terminat cel puțin o prăjitură, i-o puteți da să o mănânce în timp ce faceți alte prăjituri.
In [4]: def make_cake(numbers):
...: for i in range(numbers):
...: yield 'Cake {}'.format(i)
...:
In [5]: factory = make_cake(5)
Aici factory
se numește un generator, care îți face prăjituri. Dacă apelați make_function
, , obțineți un generator în loc să executați această funcție. Aceasta se datorează faptului că atunci când yield
cuvântul cheie este prezent într-o funcție, aceasta devine un generator.
In [7]: next(factory)
Out[7]: 'Cake 0'
In [8]: next(factory)
Out[8]: 'Cake 1'
In [9]: next(factory)
Out[9]: 'Cake 2'
In [10]: next(factory)
Out[10]: 'Cake 3'
In [11]: next(factory)
Out[11]: 'Cake 4'
Au consumat toate prăjiturile, dar cer din nou una.
In [12]: next(factory)
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-12-0f5c45da9774> in <module>
----> 1 next(factory)
StopIteration:
Și li se spune să nu mai ceară altele. Deci, odată ce ați consumat un generator, ați terminat cu el. Trebuie să apelați make_cake
din nou dacă vrei mai multe prăjituri. Este ca și cum ai face o altă comandă de prăjituri.
In [13]: factory = make_cake(3)
In [14]: for cake in factory:
...: print(cake)
...:
Cake 0
Cake 1
Cake 2
De asemenea, puteți folosi for loop cu un generator ca cel de mai sus.
Încă un exemplu: Să spunem că doriți o parolă aleatorie de fiecare dată când o cereți.
In [22]: import random
In [23]: import string
In [24]: def random_password_generator():
...: while True:
...: yield ''.join([random.choice(string.ascii_letters) for _ in range(8)])
...:
In [25]: rpg = random_password_generator()
In [26]: for i in range(3):
...: print(next(rpg))
...:
FXpUBhhH
DdUDHoHn
dvtebEqG
In [27]: next(rpg)
Out[27]: 'mJbYRMNo'
Iată rpg
este un generator, care poate genera un număr infinit de parole aleatoare. Deci putem spune, de asemenea, că generatoarele sunt utile atunci când nu cunoaștem lungimea secvenței, spre deosebire de listă, care are un număr finit de elemente.
În Python generators
(un tip special de iterators
) sunt utilizate pentru a genera serii de valori, iar yield
este la fel ca și cuvântul cheie return
din funcțiile generatoare.
Celălalt lucru fascinant yield
pe care îl face cuvântul cheie este salvarea valorilor state
a unei funcții generatoare.
Astfel, putem seta un number
la o valoare diferită de fiecare dată când funcția generator
dă randament.
Iată un exemplu:
def getPrimes(number):
while True:
if isPrime(number):
number = yield number # a miracle occurs here
number += 1
def printSuccessivePrimes(iterations, base=10):
primeGenerator = getPrimes(base)
primeGenerator.send(None)
for power in range(iterations):
print(primeGenerator.send(base ** power))
Toate răspunsurile de aici sunt foarte bune; dar numai unul dintre ele (cel mai votat) se referă la modul în care funcționează codul dvs.. Celelalte se referă la generatoare în general și la modul în care funcționează.
Așa că nu voi repeta ce sunt generatoarele sau ce fac randamentele; cred că acestea sunt acoperite de răspunsurile mari existente. Cu toate acestea, după ce am petrecut câteva ore încercând să înțeleg un cod similar cu al tău, voi descompune modul în care funcționează.
Codul tău traversează o structură de arbore binar. Să luăm ca exemplu acest arbore:
5
/
3 6
/
1 4 8
Și o altă implementare mai simplă a traversării unui arbore de căutare binară:
class Node(object):
..
def __iter__(self):
if self.has_left_child():
for child in self.left:
yield child
yield self.val
if self.has_right_child():
for child in self.right:
yield child
Codul de execuție se află pe Tree
care implementează __iter__
ca acesta:
def __iter__(self):
class EmptyIter():
def next(self):
raise StopIteration
if self.root:
return self.root.__iter__()
return EmptyIter()
while candidates
poate fi înlocuită cu for element in tree
; Python traduce acest lucru în
it = iter(TreeObj) # returns iter(self.root) which calls self.root.__iter__()
for element in it:
.. process element ..
Deoarece Node.__iter__
este un generator, codul din interiorul său este executat la fiecare iterație. Astfel, execuția ar arăta astfel:
- elementul rădăcină este primul; se verifică dacă are copii din stânga și
for
îi iterați (să îl numim it1 deoarece este primul obiect iterator). - it are un copil, astfel încât
for
este executat.for child in self.left
creează un nou iterator de laself.left
, , care este el însuși un obiect Node (it2) - Aceeași logică ca și în cazul lui 2, și un nou
iterator
este creat (it3) - Acum am ajuns la capătul stâng al arborelui.
it3
nu mai are copii în stânga, deci continuă șiyield self.value
- La următorul apel la
next(it3)
se ridicăStopIteration
și există, deoarece nu are copii la dreapta (ajunge la sfârșitul funcției fără să cedeze nimic). it1
șiit2
sunt încă active – nu sunt epuizate și apelarea luinext(it2)
ar produce valori, nu ar ridicaStopIteration
- Acum ne-am întors la
it2
context, și apelămnext(it2)
care continuă de unde s-a oprit: imediat dupăyield child
declarație. Deoarece nu mai are copii rămași, continuă și produce valoarea luiself.val
.
Problema aici este că la fiecare iterație creează subiteratori pentru a parcurge arborele și păstrează starea iteratorului curent. Odată ajuns la sfârșit, acesta parcurge înapoi stiva, iar valorile sunt returnate în ordinea corectă (prima valoare este cea mai mică).
Exemplul dvs. de cod a făcut ceva similar printr-o tehnică diferită: a populat un listă cu un singur element pentru fiecare copil, apoi, la următoarea iterație, o scoate și execută codul funcției pe obiectul curent (de aici și self
).
Sper că am contribuit puțin la acest subiect legendar. Am petrecut câteva ore bune desenând acest proces pentru a-l înțelege.
Poate, de asemenea, să trimită date înapoi la generator!
Într-adevăr, așa cum explică multe răspunsuri de aici, folosind yield
creează un generator
.
Puteți utiliza yield
pentru a trimiteți datele înapoi la un generator „viu”.
Exemplu:
Să spunem că avem o metodă care traduce din engleză în altă limbă. La începutul acesteia, se face ceva care este greu și care trebuie făcut o singură dată. Vrem ca această metodă să ruleze la nesfârșit (nu prea știu de ce.. :)), și să primească cuvinte cuvinte care să fie traduse.
def translator():
# load all the words in English language and the translation to 'other lang'
my_words_dict = {'hello': 'hello in other language', 'dog': 'dog in other language'}
while True:
word = (yield)
yield my_words_dict.get(word, 'Unknown word...')
Se execută:
my_words_translator = translator()
next(my_words_translator)
print(my_words_translator.send('dog'))
next(my_words_translator)
print(my_words_translator.send('cat'))
va imprima:
dog in other language
Unknown word...
Pentru a rezuma:
use send
în interiorul unui generator pentru a trimite date înapoi la generator. Pentru a permite acest lucru, trebuie să se utilizeze o (yield)
este utilizat.
yield în python este într-un fel similar cu declarația return, cu excepția unor diferențe. Dacă mai multe valori trebuie returnate de la o funcție, instrucțiunea return va returna toate valorile sub forma unei liste, care trebuie stocată în memorie în blocul apelantului. Dar ce se întâmplă dacă nu dorim să folosim memorie suplimentară? În schimb, dorim să obținem valoarea din funcție atunci când avem nevoie de ea. Aici intervine randamentul. Luați în considerare următoarea funcție :-
def fun():
yield 1
yield 2
yield 3
Iar apelantul este :-
def caller():
print ('First value printing')
print (fun())
print ('Second value printing')
print (fun())
print ('Third value printing')
print (fun())
Segmentul de cod de mai sus (funcția apelantă), atunci când este apelată, produce rezultate :-
First value printing
1
Second value printing
2
Third value printing
3
După cum se poate observa din cele de mai sus, yield returnează o valoare apelantului său, dar atunci când funcția este apelată din nou, aceasta nu pornește de la prima instrucțiune, ci de la instrucțiunea imediat după yield. În exemplul de mai sus, a fost tipărită „First value printing” și funcția a fost apelată. 1 a fost returnat și tipărit. Apoi a fost imprimată „Second value printing” și a fost apelată din nou fun(). În loc să se tipărească 1 (prima instrucțiune), s-a returnat 2, adică instrucțiunea imediat după yield 1. Același proces se repetă în continuare.
- Dacă încercați să rulați acest cod, se afișează
print(fun())
nu tipărește numere. În schimb, acesta tipărește reprezentarea obiectului generator returnat de cătrefun()
(ceva de genul<generator object fun at 0x6fffffe795c8>
) – > . - @FunnyGeeks Am rulat același cod pe Jupyter Notebook și funcționează bine. De asemenea, scopul aici a fost să explicăm funcționarea cuvântului cheie yield. Fragmentul este doar în scop demonstrativ. – > .
- Am încercat în python2 și python3 în consola mea cygwin. Nu a funcționat. github.com/ImAmARobot/PythonTest – > .