Naprosto typickým prvkem funkcionálního programování je vytváření (potenciálně i nekonečných) struktur, z nichž se vrací pouze ta část, která je aktuálně vyžadována, a nic dalšího se (zatím) nepočítá.
V Python'u se funkcionálním konstruktorům takových objektů říká generátory (v objektové variantě pak iterátory) a vyrábějí se z obyčejných funkcí doplněním alespoň jednoho klíčového slova yield
.
Pro průchod generátorem se principielně používá metoda next()
:
yield
„zmrazí“ funkci v aktuálním stavu, a tak při příštím zavolání generátoru se řízení vrátí přesně do tohoto stavu (narozdíl od funkce, která se „spustí“ znovu od začátku).
StopIteration
.
Jsou chvíle, kdy je vyhození výjimky funkcí next() nežádoucí (například při načítání souboru pomocí smyčky while). V takové situaci je možno toto chování přepsat přidáním druhého parametru funkce, který bude vrácen místo výjimky:
Smyčka for-in
se o vytvoření příslušné instance generátoru (jakkoli to zní u funkce protismyslně) i automatické ukončení při zachycení výjimky StopIteration
postará sama:
Generátor „v akci“ si s sebou nese svůj aktuální pracovní kontext a smyčka for-in ho „nenaboří“, naopak s ním bude pěkně spolupracovat:
Mezi použitím globální funkce next()
pro vyvolání dalšího yieldu a smyčkou for-in pro proiterování generátoru je několik rozdílů:
next()
očekává na vstupu konkrétní instanci generátoru, nikoli jeho konstruktor => kód next( g() )
bude vracet pokaždé první yield; pro nejspíše zamýšlené fungování tedy budete chtít provést něco takovéhoto:
next()
na ukončený generátor (tj. ten, který už nemá co vrátit) vyvolá výjimku StopIteration
Narozdíl od next()
si tohle všechno smyčka for-in hlídá sama – na vstup jí můžete poslat rovnou konstruktor generátoru a uvedená výjimka je logicky interpretována jako ukončení cyklu.
Na druhou stranu smyčka while nic takového pochopitelně nedělá, takže nepřekvapí, že u ní se můžeme s voláním next()
setkat docela často.
Volání vestavěné funkce next()
způsobí na pozadí zavolání metody __next__()
příslušného generátoru:
Tohle v kódu nebude chtít jen tak použít, ale naznačuje to přímou souvislost s iterátory.
Zatím jsem moc nezdůraznil, že zavolaný generátor vždy vykoná veškerý dostupný kód, než se zarazí po vrácení hodnoty vykonáním příslušného následujícího yield
'u:
Asi nejdůležitější je si uvědomit, že se to především týká veškerého kódu před prvním yield'em. To znamená, že zavolání funkce „konstruktoru“ dekorátoru nezačne automaticky vykonávat v ní (resp. v něm) obsažený kód – to obstará až první zavolání funkce next()
.
Přestože funkce obsahující místo return slovo yield fakticky slouží jako konstruktor generátoru (aneb jejím zavoláním je vytvořen objekt úplně jiného typu – místo vrácené hodnoty dostaneme generátor), není žádný důvod, proč by nemohla přijímat vstupní parametry. Příklad:
Typický generátor někdy skončí, tj. od jistého okamžiku už nebude počítat další hodnoty. Z hlediska příslušné funkce tedy uvedený kód někdy doběhne do svého konce:
Generátor z předchozího slajdu je konečný díky tomu, že funkce použitá k jeho vytvoření někdy skončí. Přitom konec této funkce je navenek signalizován tím, že se z funkce vrátí hodnota None
.
Ačkoli v přechozím příkladě bylo ono vrácení implicitní, můžeme generátor – možná trochu překvapivě – ukončit také explicitním vrácením se z něj pomocí return HODNOTA
. Uvedená hodnota je přitom předána jako atribut value
příslušné výjimky StopIteration:
Ale jde to i jinak – generátory podporují své explicitní ukončení také pomocí metody close()
:
Jak jsme už viděli, jedna z mnoha výhod generátorů tkví v tom, že mohou být potenciálně i nekonečné, aniž bychom se museli bát o dostupnou paměť – počítá (a vrací) se z nich jenom to, co je v danou chvíli skutečně potřeba:
V Python'u 3.3 přibyla možnost použít konstrukci:
Tato slouží (alespoň než se zamotáme do korutin) k odkázání získání návratové hodnoty na sub-generátor: