Nacházíte se zde: Domů Ponořme se do Pythonu 3

Úroveň obtížnosti: ♦♦♦♢♢

Uzávěry a generátory

My spelling is Wobbly. It’s good spelling but it Wobbles, and the letters get in the wrong places.
(Mé jméno je Houpavý. Hláskuji to správně, ale Houpe se to a písmenka se dostávají na špatná místa.)
— Medvídek Pú

 

Ponořme se

Vyrůstal jsem jako syn knihovnice, která vystudovala angličtinu, a vždycky mě fascinovaly jazyky. Nemyslím programovací jazyky. Tedy ano, i programovací jazyky, ale také přirozené jazyky. Dejme tomu angličtina. Angličtina je schizofrenní jazyk, který si slova půjčuje z němčiny, francouzštiny, španělštiny a latiny (když už mám pár vyjmenovat). Slova „půjčuje si“ ve skutečnosti nejsou ta pravá, „vykrádá“ je přiléhavější. Nebo si je možná „asimiluje“ — jako Borg. Jo, to se mi líbí.

My jsme Borg. Zvláštnosti vašeho jazyka a původu slov budou přidány do našeho vlastního. Odpor je marný.

V této kapitole se naučíte něco o anglických podstatných jménech v množném čísle. A také o funkcích, které vracejí jiné funkce, o regulárních výrazech pro pokročilé a o generátorech. Ale nejdříve si řekněme něco o tom, jak se tvoří podstatná jména v množném čísle. (Pokud jste nečetli kapitolu o regulárních výrazech, tak je na to vhodná doba právě teď. V této kapitole se předpokládá, že základům regulárních výrazů už rozumíte, protože se rychle dostaneme k látce pro pokročilé.)

Pokud jste vyrostli v anglicky mluvící zemi nebo pokud jste se angličtinu učili ve školních lavicích, pak pravděpodobně základní pravidla znáte:

(No ano, existuje spousta výjimek. Z man se stává men a z woman zase women, ale human se mění na humans. Mouse přechází v mice a z louse je zase lice, ale house se mění v houses. Knife přechází v knives a z wife se stávají wives, ale lowlife se mění v lowlifes. A nechtějte, abych začal o slovech, která jsou sama svým množným číslem (tj. pomnožná), jako jsou sheep, deer a haiku.)

V jiných jazycích je to, samozřejmě, úplně jiné.

Pojďme si navrhnout pythonovskou knihovnu, která automaticky převádí anglická podstatná jména do množného čísla. Začneme s uvedenými čtyřmi pravidly. Ale myslete na to, že budeme nevyhnutelně muset přidávat další.

Já vím jak na to! Použijeme regulární výrazy!

Takže se díváme na slova, což znamená (přinejmenším v angličtině), že se díváme na řetězce znaků. Pak tady máme pravidla, která nám říkají, že potřebujeme najít různé kombinace znaků a podle nich něco udělat. Vypadá to jako práce pro regulární výrazy!

[stáhnout plural1.py]

import re

def plural(noun):
    if re.search('[sxz]$', noun):             
        return re.sub('$', 'es', noun)        
    elif re.search('[^aeioudgkprt]h$', noun):
        return re.sub('$', 'es', noun)
    elif re.search('[^aeiou]y$', noun):
        return re.sub('y$', 'ies', noun)
    else:
        return noun + 's'
  1. Jde o regulární výraz, ale používá syntaxi, se kterou jste se v kapitole Regulární výrazy nesetkali. Hranaté závorky znamenají „napasuj se přesně na jeden z těchto znaků“. Takže [sxz] znamená „s nebo x nebo z“, ale jenom jeden z nich. Znak $ by vám měl být povědomý. Vyjadřuje shodu s koncem řetězce. Když to dáme dohromady, pak tento regulární výraz testuje, zda noun (podstatné jméno) končí znakem s, x nebo z.
  2. Funkce re.sub() provádí náhrady v řetězci, které jsou založeny na použití regulárního výrazu.

Podívejme se na náhrady předepsané regulárním výrazem podrobněji.

>>> import re
>>> re.search('[abc]', 'Mark')    
<_sre.SRE_Match object at 0x001C1FA8>
>>> re.sub('[abc]', 'o', 'Mark')  
'Mork'
>>> re.sub('[abc]', 'o', 'rock')  
'rook'
>>> re.sub('[abc]', 'o', 'caps')  
'oops'
  1. Obsahuje řetězec Mark znak a, b nebo c? Ano, obsahuje a.
  2. Fajn. Teď najdi a, b nebo c a nahraď ho znakem o. Z Mark se stane Mork.
  3. Stejná funkce změní rock na rook.
  4. Mohli byste si myslet, že stejná funkce změní caps na oaps, ale není tomu tak. Funkce re.sub nahrazuje všechny shody s regulárním výrazem, nejenom první z nich. Takže tento regulární výraz změní caps na oops, protože jak c, tak a se změní na o.

A teď zpět k funkci plural() (množné číslo)…

def plural(noun):
    if re.search('[sxz]$', noun):
        return re.sub('$', 'es', noun)         
    elif re.search('[^aeioudgkprt]h$', noun):  
        return re.sub('$', 'es', noun)
    elif re.search('[^aeiou]y$', noun):        
        return re.sub('y$', 'ies', noun)
    else:
        return noun + 's'
  1. Zde nahrazujeme konec řetězce (shoda s předpisem $) řetězcem es. Jinými slovy, přidáváme es na konec řetězce. Stejného efektu byste mohli dosáhnout konkatenací řetězců (spojením), například použitím noun + 'es'. Ale z důvodu, které budou jasnější později, jsem se rozhodl každé pravidlo realizovat pomocí regulárního výrazu.
  2. Teď se pořádně podívejte na následující novinku. Znak ^ uvedený v hranatých závorkách na začátku má speciální význam — negaci. Zápis [^abc] znamená „libovolný znak s výjimkou a, b nebo c“. Takže [^aeioudgkprt] znamená libovolný znak s výjimkou a, e, i, o, u, d, g, k, p, r nebo t. Tento znak musí být následován znakem h a koncem řetězce. Hledáme slova, která končí písmenem H a ve kterých je H slyšet.
  3. Stejně postupujeme v tomto případě: napasuj se na slova, která končí písmenem Y, kde předcházejícím znakem není a, e, i, o nebo u. Hledáme slova, která končí písmenem Y, které zní jako I.

Podívejme se na regulární výrazy s negací podrobněji.

>>> import re
>>> re.search('[^aeiou]y$', 'vacancy')  
<_sre.SRE_Match object at 0x001C1FA8>
>>> re.search('[^aeiou]y$', 'boy')      
>>> 
>>> re.search('[^aeiou]y$', 'day')
>>> 
>>> re.search('[^aeiou]y$', 'pita')     
>>> 
  1. vacancy tomuto regulárnímu výrazu vyhovuje, protože končí na cy a c nepatří mezi a, e, i, o nebo u.
  2. boy k regulárnímu výrazu nepasuje, protože končí oy a regulárním výrazem jsme přímo řekli, že před znakem y nemůže být o. Nepasuje ani day, protože končí na ay.
  3. pita nevyhovuje také, protože nekončí y.
>>> re.sub('y$', 'ies', 'vacancy')               
'vacancies'
>>> re.sub('y$', 'ies', 'agency')
'agencies'
>>> re.sub('([^aeiou])y$', r'\1ies', 'vacancy')  
'vacancies'
  1. Tento regulární výraz mění vacancy na vacancies a agency na agencies, což jsme chtěli. Všimněte si, že by změnil také boy na boies, ale k tomu uvnitř funkce nikdy nedojde, protože provedení re.sub je podmíněno výsledkem předchozího re.search.
  2. Když už jsme u toho, chtěl bych upozornit, že uvedené dva regulární výrazy (jeden, který rozhoduje o uplatnění pravidla, a druhý, který ho realizuje) můžeme zkombinovat do jednoho. Vypadalo by to nějak takto. S většinou výrazu už byste neměli mít problém. Používáme zapamatovanou skupinu, o které jsme si povídali v případové studii zabývající se analýzou telefonních čísel. Skupina se používá k zapamatování si znaku, který se nachází před písmenem y. V řetězci s náhradou se pak používá nový syntaktický prvek \1, který znamená: „Máš tu první zapamatovanou skupinu? Vlož ji sem.“ V tomto případě se před y zapamatovalo c. V okamžiku substituce se na místo c vloží c a y se nahradí ies. (Pokud pracujete s více než jednou zapamatovanou skupinou, můžete použít \2 a \3 a tak dále.)

Náhrady pomocí regulárních výrazů jsou velmi mocné a syntaxe \1 je činí ještě mocnějšími. Ale zkombinování celé operace do jednoho regulárního výrazu snižuje čitelnost a navíc toto řešení nevyjadřuje přímočaře způsob popisu pravidla pro vytváření množného čísla. Původně jsme pravidlo vyjádřili ve stylu „pokud slovo končí S, X nebo Z, pak přidáme ES“. Když se podíváte na zápis funkce, vidíte dva řádky kódu, které říkají „jestliže slovo končí S, X nebo Z, pak přidej ES“. Přímočařeji už to snad ani vyjádřit nejde.

Seznam funkcí

Teď přidáme úroveň abstrakce. Začali jsme definicí seznamu pravidel: Jestliže platí tohle, udělej tamto, v opačném případě přejdi k dalšímu pravidlu. Dočasně zkomplikujeme jednu část programu, abychom mohli zjednodušit jinou.

[download plural2.py]

import re

def match_sxz(noun):
    return re.search('[sxz]$', noun)

def apply_sxz(noun):
    return re.sub('$', 'es', noun)

def match_h(noun):
    return re.search('[^aeioudgkprt]h$', noun)

def apply_h(noun):
    return re.sub('$', 'es', noun)

def match_y(noun):                             
    return re.search('[^aeiou]y$', noun)

def apply_y(noun):                             
    return re.sub('y$', 'ies', noun)

def match_default(noun):
    return True

def apply_default(noun):
    return noun + 's'

rules = ((match_sxz, apply_sxz),               
         (match_h, apply_h),
         (match_y, apply_y),
         (match_default, apply_default)
         )

def plural(noun):
    for matches_rule, apply_rule in rules:       
        if matches_rule(noun):
            return apply_rule(noun)
  1. V tomto okamžiku má každé rozhodovací (match) pravidlo svou vlastní funkci, která vrací výsledek volání funkce re.search().
  2. Každé aplikační pravidlo má také svou vlastní funkci, která volá funkci re.sub() realizující příslušný způsob vytvoření množného čísla.
  3. Místo jedné funkce (plural()) s mnoha pravidly teď máme datovou strukturu rules (pravidla), která je posloupností dvojic funkcí.
  4. A protože pravidla byla rozbita do podoby oddělené datové struktury, může být nová funkce plural() zredukována na pár řádků kódu. V cyklu for můžeme z datové struktury rules po dvojicích vybírat rozhodovací a aplikační pravidla (jedno rozhodovací a jedno aplikační). Při prvním průchodu cyklem for nabude matches_rule hodnoty match_sxz a apply_rule hodnoty apply_sxz. Při druhém průchodu (za předpokladu, že se tak daleko dostaneme) bude proměnné matches_rule přiřazena match_h a proměnné apply_rule bude přiřazena apply_h. Je zaručeno, že funkce nakonec něco vrátí, protože poslední rozhodovací funkce (match_default) vrací prostě True. To znamená, že se provede odpovídající aplikační pravidlo (apply_default).

Funkčnost této techniky je zaručena tím, že v Pythonu je objektem všechno, včetně funkcí. Datová struktura rules obsahuje funkce — nikoliv jména funkcí, ale skutečné objekty funkcí. Když v cyklu for dojde k jejich přiřazení, stanou se z proměnných matches_rule a apply_rule skutečné funkce, které můžeme volat. Při prvním průchodu cyklu for je to stejné, jako kdyby se volala funkce matches_sxz(noun). A pokud by vrátila objekt odpovídající shodě, zavolala by se funkce apply_sxz(noun).

Pokud se vám přidaná úroveň abstrakce jeví jako matoucí, zkuste si cyklus uvnitř funkce rozepsat a shodu rozpoznáte snadněji. Celý cyklus for je ekvivalentní následujícímu zápisu:


def plural(noun):
    if match_sxz(noun):
        return apply_sxz(noun)
    if match_h(noun):
        return apply_h(noun)
    if match_y(noun):
        return apply_y(noun)
    if match_default(noun):
        return apply_default(noun)

Výhodou je, že funkce plural() se zjednodušila. Přebírá sadu pravidel, která mohla být definována kdekoliv, a prochází jimi zobecněným způsobem.

  1. Získej rozhodovací pravidlo (match rule).
  2. Došlo ke shodě? Tak volej aplikační pravidlo a vrať výsledek.
  3. Nedošlo ke shodě? Přejdi ke kroku 1.

Pravidla mohou být definována kdekoliv, jakýmkoliv způsobem. Funkci plural() je to jedno.

Dobrá, ale bylo vůbec přidání úrovně abstrakce k něčemu dobré? No, zatím ne. Zvažme, co to znamená, když k funkci chceme přidat nové pravidlo. V prvním příkladu by to znamenalo přidat do funkce plural() příkaz if. V tomto druhém příkladu by to vyžadovalo přidání dalších dvou funkcí match_foo() a apply_foo(). Pak bychom museli určit, do kterého místa posloupnosti rules má být dvojice s rozhodovací a aplikační funkcí zařazena (poloha vůči ostatním pravidlům).

Ale to jsme již jen krůček od následující podkapitoly. Pojďme na to...

Seznam vzorků

Ono ve skutečnosti není nezbytné, abychom pro každé rozhodovací a aplikační pravidlo definovali samostatné pojmenované funkce. Nikdy je nevoláme přímo. Přidáváme je do posloupnosti rules a voláme je přes tuto strukturu. Každá z těchto funkcí navíc odpovídá jednomu ze dvou vzorů. Všechny rozhodovací funkce volají re.search() a všechny aplikační funkce volají re.sub(). Rozložme tyto vzory tak, abychom si usnadnili budování nových pravidel.

[download plural3.py]

import re

def build_match_and_apply_functions(pattern, search, replace):
    def matches_rule(word):                                     
        return re.search(pattern, word)
    def apply_rule(word):                                       
        return re.sub(search, replace, word)
    return (matches_rule, apply_rule)                           
  1. build_match_and_apply_functions() je funkce, která vytváří další funkce dynamicky. Přebírá argumenty pattern, search a replace. Pak definuje rozhodovací funkci matches_rule(), která volá re.search() s vzorkem pattern, který byl předán funkci build_match_and_apply_functions(), a se slovem word, které se předává právě budované funkci matches_rule(). Ty jo!
  2. Aplikační funkce se vytváří stejným způsobem. Aplikační funkce přebírá jeden parametr a volá re.sub() s argumenty search a replace, které byly předány funkci build_match_and_apply_functions(), a s parametrem word, který se předává právě budované funkci apply_rule(). Této technice, kdy se uvnitř dynamicky budované funkce použijí vnější hodnoty, se říká uzávěr (closure). Uvnitř budované aplikační funkce v podstatě definujeme konstanty. Funkce přebírá jeden parametr (word), potom se chová podle něj, ale také podle dalších dvou hodnot (search a replace), které platily v době definice aplikační funkce.
  3. Nakonec funkce build_match_and_apply_functions() vrátila dvojici hodnot — dvě funkce, které jsme právě vytvořili. Konstanty, které jsme uvnitř těchto funkcí definovali (pattern uvnitř funkce matches_rule() a search a replace uvnitř funkce apply_rule()), v nich zůstávají uzavřené dokonce i po návratu z funkce build_match_and_apply_functions(). To je prostě špica!

Pokud se vám to zdá neuvěřitelně matoucí (a to by mělo, protože to je fakt ujeté), může se to vyjasnit, když uvidíte, jak se to používá.

patterns = \                                                        
  (
    ('[sxz]$',           '$',  'es'),
    ('[^aeioudgkprt]h$', '$',  'es'),
    ('(qu|[^aeiou])y$',  'y$', 'ies'),
    ('$',                '$',  's')                                 
  )
rules = [build_match_and_apply_functions(pattern, search, replace)  
         for (pattern, search, replace) in patterns]
  1. Naše pravidla (rules) pro tvorbu množného čísla jsou nyní definována jako n-tice trojic řetězců (ne funkcí). Prvním řetězcem v každé skupině je regulární výraz, který se bude používat v re.search() pro rozhodování, zda se toto pravidlo uplatňuje. Druhý a třetí řetězec ve skupině jsou výrazy pro vyhledání a náhradu, které se použijí v re.sub() pro aplikaci pravidla, které sloveso převede do množného čísla.
  2. U záložního pravidla došlo k drobné změně. Pokud v předchozím příkladu nebylo nalezeno žádné ze specifičtějších pravidel, vracela funkce match_default() hodnotu True, což znamenalo, že se na konec slova jednoduše přidá s. Tento dosahuje stejné funkčnosti trochu jinak. Poslední regulární výraz zjišťuje, jestli slovo končí ($ odpovídá konci řetězce). A samozřejmě, každý řetězec končí (dokonce i prázdný řetězec), takže shoda s tímto výrazem je nalezena vždy. Tento přístup tedy plní stejný účel jako funkce match_default(), která vždycky vracela True. Pokud nepasuje žádné specifičtější pravidlo, zajistí přidání s na konec daného slova.
  3. Tento řádek je magický. Přebírá řetězce z posloupnosti patterns a mění je na posloupnost funkcí. Jak to dělá? „Zobrazením“ řetězců prostřednictvím funkce build_match_and_apply_functions(). To znamená, že se vezme každá trojice řetězců a ty se předají jako argumenty funkci build_match_and_apply_functions(). Funkce build_match_and_apply_functions() vrátí dvojici funkcí. To znamená, že struktura rules získá funkčně shodnou podobu jako v předchozím příkladu — seznam dvojic, kde každá obsahuje dvě funkce. První funkce je rozhodovací (match; pasovat) a volá re.search(), druhá funkce je aplikační a volá re.sub().

Skript zakončíme hlavním vstupním bodem, funkcí plural().

def plural(noun):
    for matches_rule, apply_rule in rules:  
        if matches_rule(noun):
            return apply_rule(noun)
  1. A protože je seznam rules stejný jako v předchozím příkladu (a to opravdu je), nemělo by být žádným překvapením, že se funkce plural() vůbec nezměnila. Je zcela obecná. Přebírá seznam funkcí realizujících pravidla a volá je v uvedeném pořadí. Nestará se o to, jak jsou pravidla definována. V předcházejícím příkladu byla definována jako pojmenované funkce. Teď jsou funkce pravidel budovány dynamicky zobrazením řetězců ze vstupního seznamu voláním funkce build_match_and_apply_functions(). Na tom ale vůbec nezáleží. Funkce plural() pracuje stále stejným způsobem.

Soubor vzorků

Jsme v situaci, kdy už jsme rozpoznali veškeré duplicity v kódu a přešli jsme na dostatečnou úroveň abstrakce. To nám umožnilo definovat pravidla pro vytváření množného čísla v podobě seznamu řetězců. Další logický krok spočívá v uložení těchto řetězců v odděleném souboru. Pravidla (v podobě řetězců) pak mohou být udržována odděleně od kódu, který je používá.

Nejdříve vytvořme textový soubor, který obsahuje požadovaná pravidla. Nebudeme používat žádné efektní datové struktury. Stačí nám tři sloupce řetězců oddělené bílými znaky (whitespace; zde mezery nebo tabulátory). Soubor nazveme plural4-rules.txt.

[stáhnout plural4-rules.txt]

[sxz]$               $    es
[^aeioudgkprt]h$     $    es
[^aeiou]y$          y$    ies
$                    $    s

Teď se podívejme na to, jak můžeme soubor s pravidly použít.

[stáhnout plural4.py]

import re

def build_match_and_apply_functions(pattern, search, replace):  
    def matches_rule(word):
        return re.search(pattern, word)
    def apply_rule(word):
        return re.sub(search, replace, word)
    return (matches_rule, apply_rule)

rules = []
with open('plural4-rules.txt', encoding='utf-8') as pattern_file:  
    for line in pattern_file:                                      
        pattern, search, replace = line.split(None, 3)             
        rules.append(build_match_and_apply_functions(              
                pattern, search, replace))
  1. Funkce build_match_and_apply_functions() se nezměnila. Pro dynamické vytvoření funkcí, které používají proměnné definované vnější funkcí, pořád používáme uzávěry.
  2. Globální funkce open() otvírá soubor a vrací souborový objekt. V tomto případě otvíráme soubor, který obsahuje vzorky řetězců pro převádění podstatných jmen do množného čísla. Příkaz with vytváří takzvaný kontext. Jakmile blok příkazu with skončí, Python soubor automaticky uzavře, a to i v případě, kdyby byla uvnitř bloku with vyvolána výjimka. O blocích with a o souborových objektech se dozvíte více v kapitole Soubory.
  3. Obrat for line in <souborový_objekt> čte data z otevřeného souborového objektu řádek po řádku a přiřazuje text do proměnné line (řádek). O čtení ze souboru se dozvíte více v kapitole Soubory.
  4. Každý řádek souboru obsahuje tři hodnoty, ale jsou oddělené bílými znaky (tabulátory nebo mezerami, na tom nezáleží). Rozdělíme je použitím řetězcové metody split(). Prvním argumentem metody split() je None, což vyjadřuje požadavek „rozdělit v místech posloupností bílých znaků (tabulátorů nebo mezer, na tom nezáleží)“. Druhým argumentem je hodnota 3, což znamená „rozdělit na místě bílých znaků maximálně 3krát a zbytek řádku ponechat beze změny“. Například řádek [sxz]$ $ es bude rozložen na seznam ['[sxz]$', '$', 'es']. To znamená, že proměnná pattern získá hodnotu '[sxz]$', proměnná search hodnotu '$' a proměnná replace hodnotu 'es'. V tak krátkém řádku kódu se skrývá docela hodně síly.
  5. Nakonec předáme pattern, search a replace funkci build_match_and_apply_functions(), která vrátí dvojici funkcí. Tuto dvojici připojíme na konec seznamu pravidel, takže nakonec bude rules uchovávat seznam rozhodovacích a aplikačních funkcí, které potřebuje funkce plural().

Zdokonalení spočívá v tom, že jsme pravidla pro vytváření množného čísla podstatných jmen oddělili do vnějšího souboru, který může být udržován odděleně od kódu, který pravidla využívá. Kód se stal kódem, z dat jsou data a život je krásnější.

Generátory

Nebylo by skvělé, kdybychom měli obecnou funkci plural(), která si umí sama zpracovat soubor s pravidly? Získala by pravidla, zkontrolovala by, které se má uplatnit, provedla by příslušné transformace, přešla by k dalšímu pravidlu. To je to, co bychom po funkci plural() chtěli. A to je to, co by funkce plural() měla dělat.

[stáhnout plural5.py]

def rules(rules_filename):
    with open(rules_filename, encoding='utf-8') as pattern_file:
        for line in pattern_file:
            pattern, search, replace = line.split(None, 3)
            yield build_match_and_apply_functions(pattern, search, replace)

def plural(noun, rules_filename='plural5-rules.txt'):
    for matches_rule, apply_rule in rules(rules_filename):
        if matches_rule(noun):
            return apply_rule(noun)
    raise ValueError('no matching rule for {0}'.format(noun))

Jak sakra funguje tohle? Podívejme se nejdříve na interaktivní příklad.

>>> def make_counter(x):
...     print('entering make_counter')
...     while True:
...         yield x                    
...         print('incrementing x')
...         x = x + 1
... 
>>> counter = make_counter(2)          
>>> counter                            
<generator object at 0x001C9C10>
>>> next(counter)                      
entering make_counter
2
>>> next(counter)                      
incrementing x
3
>>> next(counter)                      
incrementing x
4
  1. Přítomnost klíčového slova yield v make_counter znamená, že nejde o obyčejnou funkci. Jde o speciální druh funkce, která generuje hodnoty jednu po druhé. Můžeme si ji představit jako funkci, která umí při dalším volání pokračovat v činnosti. Když ji zavoláme, vrátí nám generátor, který můžeme použít pro generování posloupnosti hodnot x.
  2. Instanci generátoru make_counter vytvoříme tím, že ji zavoláme jako každou jinou funkci. Poznamenejme, že tím ve skutečnosti nedojde k provedení kódu funkce. Jde to poznat i podle toho, že se na prvním řádku funkce make_counter() volá print(), ale nic se zatím nevytisklo.
  3. Funkce make_counter() vrátila objekt generátoru.
  4. Funkce next() přebírá objekt generátoru a vrací jeho další hodnotu. Při prvním volání funkce next() pro generátor counter se provede kód z make_counter() až do prvního příkazu yield a vrátí se vyprodukovaná hodnota. V našem případě to bude 2, protože jsme generátor vytvořili voláním make_counter(2).
  5. Při opakovaném volání funkce next() pro stejný generátorový objekt se dostáváme přesně do místa, kde jsme minule skončili, a pokračujeme až do místa, kdy znovu narazíme na příkaz yield. Při provedení yield jsou všechny proměnné, lokální stav a další věci uloženy a při dalším volání next() jsou obnoveny. Další řádek kódu, který čeká na provedení, volá funkci print(), která vytiskne incrementing x (zvyšuji hodnotu x). Poté je proveden příkaz x = x + 1. Pak se provede další obrátka cyklu while a hned se narazí na příkaz yield x. Ten uloží stav všeho možného a vrátí aktuální hodnotu proměnné x (v tomto okamžiku 3).
  6. Při druhém volání next(counter) se vše opakuje, ale tentokrát má x hodnotu 4.

Protože make_counter definuje nekonečný cyklus, mohli bychom pokračovat teoreticky do nekonečna a docházelo by k neustálému zvyšování proměnné x a vracení její hodnoty. Místo toho se ale podívejme na užitečnější použití generátorů.

Generátor Fibonacciho posloupnosti

[stáhnout fibonacci.py]

def fib(max):
    a, b = 0, 1          
    while a < max:
        yield a          
        a, b = b, a + b  
  1. Fibonacciho posloupnost je řada čísel, kde každé další číslo je součtem dvou předchozích. Začíná hodnotami 0 a 1, zpočátku roste pomalu a pak rychleji a rychleji. Na začátku potřebujeme dvě proměnné: a s počáteční hodnotou 0 a b s počáteční hodnotou 1.
  2. Proměnná a obsahuje aktuální číslo posloupnosti, takže hodnotu vyprodukujeme (yield).
  3. Proměnná b představuje další číslo v posloupnosti, takže je přiřadíme do a, ale současně vypočteme další hodnotu (a + b) a přiřadíme ji do b pro pozdější použití. Poznamenejme, že se to děje paralelně. Pokud má a hodnotu 3 a b hodnotu 5, pak a, b = b, a + b nastaví a na 5 (předchozí hodnota b) a b na 8 (součet předchozí hodnoty a a b).

Dostali jsme funkci, která postupně chrlí Fibonacciho čísla. Mohli byste to popsat i rekurzivním řešením, ale tento způsob je čitelnější. A navíc dobře funguje při použití v cyklech for.

>>> from fibonacci import fib
>>> for n in fib(1000):      
...     print(n, end=' ')    
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
>>> list(fib(1000))          
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987]
  1. Generátor jako fib() můžete v cyklu for použít přímo. Cyklus for automaticky získává hodnoty generátoru fib() voláním funkce next() a přiřazuje je do proměnné cyklu n.
  2. Při každé obrátce cyklu for získává proměnná n novou hodnotu, která je uvnitř fib() produkována příkazem yield. Stačí ji jen vytisknout. Jakmile fib() dojdou čísla (a nabude hodnoty větší než max, což je v našem případě 1000), cyklus for elegantně skončí.
  3. Toto je užitečný obrat. Funkci list() předáme generátor. Funkce projde (iteruje přes) všechny jeho hodnoty (stejně jako tomu bylo v předchozím příkladu u cyklu for) a vrátí seznam všech generovaných hodnot.

Generátor pravidel pro množné číslo

Vraťme se k plural5.py a podívejme se, jak tato verze funkce plural() pracuje.

def rules(rules_filename):
    with open(rules_filename, encoding='utf-8') as pattern_file:
        for line in pattern_file:
            pattern, search, replace = line.split(None, 3)                   
            yield build_match_and_apply_functions(pattern, search, replace)  

def plural(noun, rules_filename='plural5-rules.txt'):
    for matches_rule, apply_rule in rules(rules_filename):                   
        if matches_rule(noun):
            return apply_rule(noun)
    raise ValueError('no matching rule for {0}'.format(noun))
  1. Není v tom žádná magie. Vzpomeňte si, že řádky souboru s pravidly obsahují vždy tři hodnoty oddělené bílými znaky. Takže použijeme line.split(None, 3) k získání tří „sloupců“ a jejich hodnoty přiřadíme do tří lokálních proměnných.
  2. A pak vyprodukujeme výsledek (yield). Jaký výsledek? Dvojici funkcí, které byly dynamicky vytvořeny naší starou známou funkcí build_match_and_apply_functions() (je stejná jako v předchozích příkladech). Řečeno jinak, rules() je generátor, který na požádání produkuje rozhodovací a aplikační funkce.
  3. Protože rules() je generátor, můžeme jej přímo použít v cyklu for. Při první obrátce cyklu for zavoláme funkci rules(), která otevře soubor se vzorky, načte první řádek, na základě vzorků uvedených na řádku dynamicky vybuduje rozhodovací funkci a aplikační funkci a tyto funkce vrátí (yield). Ale během druhé obrátky cyklu for se dostáváme přesně do místa, kde jsme kód rules() opustili (což je uprostřed cyklu for line in pattern_file). První věcí, která se provede, bude načtení řádku souboru (který je pořád otevřen). Na základě vzorků z tohoto řádku souboru se dynamicky vytvoří další rozhodovací a aplikační funkce a tato dvojice se vrátí (yield).

Co jsme vlastně proti verzi 4 získali navíc? Startovací čas. Ve verzi 4 se při importu modulu plural4 — než jsme mohli vůbec uvažovat o volání funkce plural() — načítal celý soubor vzorků a budoval se seznam všech možných pravidel. Při použití generátorů můžeme vše dělat na poslední chvíli. Přečteme si první pravidlo, vytvoříme funkce a vyzkoušíme je. Pokud to funguje, nemusíme číst zbytek souboru nebo vytvářet další funkce.

A co jsme ztratili? Výkonnost! Generátor rules() startuje znovu od začátku pokaždé, když voláme funkci plural(). To znamená, že soubor se vzorky musí být znovu otevřen a musíme číst od začátku, jeden řádek po druhém.

Chtělo by to nějak získat to nejlepší z obou řešení: minimální čas při startu (žádné provádění kódu při import) a maximální výkonnost (žádné opakované vytváření funkcí). Ale pokud nebudeme muset číst stejné řádky dvakrát, bylo by dobré, aby pravidla mohla zůstat v odděleném souboru (protože kód je kód a data jsou data).

Abychom toho dosáhli, budeme muset vytvořit svůj vlastní iterátor. Ale předtím se musíme naučit něco o pythonovských třídách.

Přečtěte si

© 2001–11 Mark Pilgrim