Nacházíte se zde: Domů ‣ Ponořme se do Pythonu 3 ‣
Úroveň obtížnosti: ♦♦♦♢♢
❝ 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ú
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ší.
⁂
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!
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'
[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
.
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'
Mark
znak a
, b
nebo c
? Ano, obsahuje a
.
a
, b
nebo c
a nahraď ho znakem o
. Z Mark
se stane Mork
.
rock
na rook
.
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'
$
) ř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.
^
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.
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') ③ >>>
vacancy
tomuto regulárnímu výrazu vyhovuje, protože končí na cy
a c
nepatří mezi a
, e
, i
, o
nebo u
.
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
.
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'
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
.
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.
⁂
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.
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)
re.search()
.
re.sub()
realizující příslušný způsob vytvoření množného čísla.
plural()
) s mnoha pravidly teď máme datovou strukturu rules
(pravidla), která je posloupností dvojic funkcí.
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.
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...
⁂
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.
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) ③
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!
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.
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]
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.
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.
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)
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.
⁂
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
.
[sxz]$ $ es
[^aeioudgkprt]h$ $ es
[^aeiou]y$ y$ ies
$ $ s
Teď se podívejme na to, jak můžeme soubor s pravidly použít.
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))
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.
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.
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.
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.
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ší.
⁂
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.
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
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.
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.
make_counter()
vrátila objekt generátoru.
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)
.
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
).
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ů.
def fib(max):
a, b = 0, 1 ①
while a < max:
yield a ②
a, b = b, a + b ③
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
.
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]
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.
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čí.
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.
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))
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.
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.
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.
⁂
© 2001–11 Mark Pilgrim