Hlavní navigace

Létající cirkus (10)

26. 4. 2002
Doba čtení: 7 minut

Sdílet

V dnešním dílu se budeme věnovat dalším novým vlastnostem jazyka Python, které se objevily v nejnovějších verzích 2.1 a 2.2 - iterátorům a generátorům. Pojďme se tedy naposledy ponořit do světa syntaxe jazyka Python.

Iterátory

V dnešním díle se budeme naposledy věnovat syntaktické stránce jazyka Python. V dalších dílech již bude následovat tolik žádaný popis modulů tohoto jazyka. Začněme jednou z vlastností, které přinesl interpret Pythonu verze 2.1 – iterátory.

Přidávání nových vlastností do jazyka Python se řídí takzvanými dokumenty PEP (Python Enhancement Proposals). Každý PEP je jakousi obdobou internetových standardů RFC, každý má své číslo a jejich přijímání se řídí určitými pravidly – nejprve se musí najít někdo, kdo požaduje určité rozšíření jazyka nebo jakoukoli jinou změnu, která se týká jazyka. Aby tato změna dostala svůj vlastní PEP, musí se jednat o závažnější změnu nebo novinku, menší změny lze přímo zasílat jako patche. Poté tento člověk (autor PEPu, v anglické dokumentaci champion) napíše draft standardu, který zašle správci dokumentů PEP. Ten draft prohlédne a pokud dodržuje všechna doporučení, je autorovi přiděleno číslo PEP, draft je uveřejněn a je otevřena diskuse. Poté, když se zdá, že je požadavek dobře specifikován, se přikročí k druhému kroku – implementování této vlastnosti do jazyka samotného. Více informací o tomto pochodu vám nabídne PEP 001.

Stejnou cestou vznikly i iterátory (PEP 234). Od verze 2.1 interpretu již konstrukce for neiteruje po jednotlivých prvcích sekvence, ale nejprve požádá tuto sekvenci o iterátor, a jednotlivé prvky, které bude dosazovat za řídící proměnnou, mu předává až iterátor. To umožňuje velice jednoduchým způsobem řídit, po kterých prvcích bude konstukce for iterovat.

Iterátor je, jako cokoli jiného v jazyce Python, objekt. Tento objekt podporuje jednu jedinou operaci – vrací další hodnotu. Tuto operaci reprezentuje metoda next(), která nepřejímá žádný argument a její návratová hodnota reprezentuje další prvek.

Nejjednodušším způsobem, jak vytvořit iterátor, je builtin funkce iter(), která má dva tvary:

  • iter(obj), která vrátí iterátor příslušející k objektu obj
  • iter(callable, sentinel), kde callable je funkční objekt a sentinel je nějaká hodnota. Rovná-li se návratová hodnota funkce callable hodnotě sentinel, je ukončena iterace. Jako hodnoty jednotlivých iterací se použijí návratové hodnoty funkce callable.

Interpretr ale ještě potřebuje rozpoznat, kdy byl předán poslední prvek, a kdy se tudíž má ukončit cyklus for. Zavedení nové hodnoty, podobně třeba jako None, se neujalo, protože iterátor by měl být schopen vrátit jakoukoli hodnotu. Nakonec byl tento problém vyřešen použitím výjimky StopIteration. Tu vyvolá zavolání metody next() iterátoru, má-li dojít k ukončení cyklu. Konstrukce for tuto výjimku odchytí a předá řízení za tělo cyklu, případně větvi else, pokud existuje.

Funkce iter(), které předáme jako objekt sekvenci, vytvoří iterátor vracející postupně všechny prvky sekvence:

>>> a = [1, 2, 3]

>>> i = iter(a)
>>> i.next()
1
>>> i.next()
2
>>> i.next()
3
>>> i.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
StopIteration
>>> i.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
StopIteration
>>>

Jak je vidět výše – po vrácení všech prvků sekvence vyústí další volání metody next() k výjimce StopIteration. To však není vše, iterátory lze pomocí funkce iter() vytvořit i k asociativním polím – pak iterátor vrací jako hodnoty klíče tohoto asociativního pole. Následující dva iterátory x a y si tedy jsou ekvivalentní:

>>> p = {1: 'a', 2: 'b', 3: 'c'}
>>> x = iter(p)
>>> y = iter(p.keys())

Iterátor ale lze vytvořit i pro soubory. Na tomto místě je však třeba dát pozor na současnou implementaci, která pracuje bezchybně pro obyčejné soubory, ale třeba pro roury tomu může být jinak – nejsou-li dostupná již žádná data, iterátor ukončí cyklus for výjimkou StopIteration, po ukončení cyklu ale mohou některá další data dorazit, cyklus je však již ukončen a pokud iterátor jednou vrátí StopIteration, má ho vracet pořád. Tím dojde ke ztrátě nově došlých dat. Guido van Rossum ale slibuje nápravu tohoto problému v dalších verzích.

Konstrukce for si od verze 2.1 vždy pomocí funkce iter() vytvoří nový iterátor pro objekt, jehož prvky má procházet, a pro tento iterátor volá metodu next(), jejíž návratové hodnoty dosazuje za řídící proměnnou. Dále je definicí iterátorů zaručeno, že pokud požadujeme iterátor nějakého již existujícího iterátoru, vrátí se ten původní beze změny. Takže následující příklady konstrukce for mají ekvivalentní funkci:

a = [1, 2, 3, 4, 5]
for i in a:
    print i
for i in iter(a):
    print i

Uživatelská třída může poskytovat svou vlastní metodu, která kromě argumentu self nepřebírá žádné jiné argumenty a která vrací iterátor instance této třídy. Tato metoda, podobně jako jiné metody, které mají co k dočinění s „vnitřním chodem“ interpretu, má název __iter__() a je volána vždy při vytvoření nového iterátoru funkcí iter().

Generátory

Mechanismus generátorů je geniálním rozšířením iterátorů. Poprvé byly uvedeny v nejnovější verzi 2.2 interpretu. Protože však generátory používají nové klíčové slovo yield, musíme si o jejich používání zatím říci sami – jak, to si povíme později. Nyní si vysvětlíme, co jsou generátory a k čemu se hodí.

Každý programátor jistě zná mechanismus Producent-Konzument. Producent získává data, Konzument je zpracovává. Jako Producent pracuje třeba parser zdrojového kódu, Konzumentem je v tomto případě jeho kompilátor. Producenta s Konzumentem je ale třeba nějakým způsobem propojit.

Nejjednodušším případem Producenta je jednoduchá funkce, která přebírá další argument navíc – zpětnou vazbu, Konzumenta. Napsat takového Producenta není složité, o to složitější je naprogramovat Konzumenta. Konzument totiž často potřebuje uchovávat informace o předchozích stavech. To lze například pomocí globálních proměnných. Problém se stává ještě složitějším používáním více vláken.

Další možnost – Producent vygeneruje všechna data a Konzument je posléze zpracuje. Jednoduché k implementování, ale velice náročné na paměť. Představte si, že Konzument vyhledává ve 100MB XML datech jediný řetězec. Producent tedy parsuje a natáhne do paměti všech sto megabajtů dat, aby Konzument nakonec zjistil, že se v nich hledaný řetězec nenachází.

Jednou z dalších možností je použití více vláken. Producent jedno vlákno, Konzument druhé. Problém se zdá být vyřešen. Python se ale od počátku potýká s vlákny. Veškeré vnitřní struktury Pythonu jsou chráněny jedním zámkem, takže na víceprocesorových strojích stejně běží v jednom čase pouze jedno vlákno. Navíc zde musíme používat zámky nebo jiné nástroje pro synchronizaci vláken. (Více o vláknech v dílu věnovaném modulu threading.)

Možností, jak tento problém řešit, je několik, ale Python přišel s elegantním a jednoduchým řešením, kterým jsou generátory. Generátor je speciální druh funkce, která místo klíčového slova return používá klíčové slovo yield. Nejjednodušší generátor může vypadat třeba takto:

>>> from __future__ import generators
>>> def gen():
...     yield 1
...     yield 2
...     yield 3
...
>>> gen()
<generator object at 0x81c5198>
>>> for i in gen():
...     print i
...
1
2
3

Protože generátory vyžadují nové klíčové slovo yield, bylo nejprve třeba učinit opatření, aby jeho zavedení nevedlo k chybné funkci existujícího kódu. Slovo yield se stane klíčovým slovem až po spuštění příkazu import tak, jak ho ukazuje první řádek příkladu. Bez jeho spuštění příklad nebude pracovat. Více o nových vlastnostech a „modulu“ __future__ se dozvíte z dokumentace k jazyku Python nebo z PEP 236.

Zavoláním funkce gen() se nespustí její tělo, ale pouze se vytvoří nový generátor. Každý generátor má stejné rozhraní jako iterátor, takže je možné jej použít namísto iterátoru v konstrukci for.

Nejdůležitější na generátorech je to, že uchovávají svůj vnitřní stav. Po prvním zavolání metody next() nějakého generátoru se spustí jeho tělo, které běží, dokud interpret nenarazí na příkaz yield. Příkaz yield má podobnou funkci jako return – přeruší tělo generátoru a metoda next() vrátí hodnotu výrazu zapsaného za slovem yield. Důležité však je, co se stane dále – generátor nadále uchovává svůj stav, všechny lokální proměnné apod. jsou „zakonzervovány“ uvnitř generátoru.

Dalším zavoláním metody next() se generátor „vzbudí“ a jeho běh pokračuje na výrazu následujícím po příkazu yield, který způsobil „uspání“. Narazí-li se na další příkaz yield, generátor se opět „uspí“ a metoda next() vrátí nějakou hodnotu.

Takto celý cyklus pokračuje, dokud se nevykoná celé tělo generátoru, nebo dokud není generátor ukončen příkazem return. Pokud v těle generátoru vznikne výjimka, šíří se normálním způsobem všechny třídy výjimek kromě StopIteration. Ta má podobný (ne však úplně stejný) účinek jako příkaz return – vede k ukončení iterace.

Nakonec si ukážeme příklad trošku složitějšího generátoru, které simuluje házecí kostku a bude demostrovat uchovávání lokálních proměnných generátoru:

CS24_early

>>> from __future__ import generators
>>> from random import choice

>>> def kostka(hodu):
...     cisla = range(1, 7)
...     for i in range(hodu):
...             print "Ziskavam %d. hod" % (i + 1)
...             yield choice(cisla)
...     print "Uz je dohazeno"
...     return
...
>>> k = kostka(3)
>>> for hod in k:
...     print "Hodil jste %d" % hod
...
Ziskavam 1. hod
Hodil jste 1
Ziskavam 2. hod
Hodil jste 3
Ziskavam 3. hod
Hodil jste 5
Uz je dohazeno

Jak vidíme, proměnná ‚cisla‘ a proměnná ‚i‘ se uchovává mezi jednotlivými stavy generátoru. Slovo return na konci generátoru není nutné, jak jsem již řekl, generátor běží, dokud se nevykoná celé tělo generátoru. Při programování generátoru musíte brát zřetel na to, že nevíte, kdy bude tělo příště spuštěno, bude-li vůbec spuštěno.

Příště

Dnešním dílem jsme dokončili výklad syntaxe jazyka Python a v příštích dílech se již podíváme na jeho „baterie“, čili standardní moduly, které dokonale naplňují slogan tohoto pozoruhodného jazyka – „Python – batteries included!“

Byl pro vás článek přínosný?