Nacházíte se zde: Domů ‣ Ponořme se do Pythonu 3 ‣
Úroveň obtížnosti: ♦♦♦♢♢
❝ East is East, and West is West, and never the twain shall meet. ❞
(Východ je východ, západ je západ a ta dvojice se nikdy nesetká.)
— Rudyard Kipling
Iterátory jsou „tajnou omáčkou“ Pythonu 3. Jsou všude, vše je na nich založeno, vždy zůstávají v pozadí, neviditelné. Generátorové notace jsou jednoduchou formou iterátorů. Generátory jsou jednoduchou formou iterátorů. Funkce, která produkuje hodnoty příkazem yield
, je ukázkou pěkného a kompaktního způsobu vytvoření iterátoru, aniž bychom museli iterátor tvořit. Ukážu vám, co tím míním.
Vzpomínáte si na Fibonacciho generátor? Tady ho máme v podobě iterátoru vytvořeného od základu:
class Fib:
'''iterator that yields numbers in the Fibonacci sequence'''
def __init__(self, max):
self.max = max
def __iter__(self):
self.a = 0
self.b = 1
return self
def __next__(self):
fib = self.a
if fib > self.max:
raise StopIteration
self.a, self.b = self.b, self.a + self.b
return fib
Proberme si jeho kód řádek po řádku.
class Fib:
class
? Česky se tomu říká třída. Ale co to je?
⁂
Python je plně objektově orientovaný. Můžete definovat své vlastní třídy, dědit ze svých vlastních nebo ze zabudovaných tříd a z definovaných tříd můžete vytvářet instance.
Třídu definujeme v Pythonu jednoduše. Nepoužívá se zde oddělená definice rozhraní — je to jako u funkcí. Prostě definujeme třídu a začneme psát její kód. Pythonovská třída začíná vyhrazeným slovem class
, za kterým následuje jméno třídy. Z technického pohledu je to vše, co se vyžaduje, protože třída nemusí dědit z žádné jiné třídy.
class PapayaWhip: ①
pass ②
PapayaWhip
. Není odvozena od žádné jiné třídy. Jména tříd se obvykle zapisují s velkými písmeny u slov názvu, KazdeSlovoNazvuTakto
. Ale je to jen konvence, není to závazné.
if
, u cyklu for
nebo v případě jakéhokoliv jiného bloku kódu. Řádek, který není odsazen, už do třídy nepatří.
Třída PapayaWhip
nedefinuje žádnou metodu ani atributy, ale ze syntaktických důvodů v definici něco být musí. Proto jsme zde použili příkaz pass
. V Pythonu je toto slovo vyhrazeno a znamená „pokračuj dál, tady není nic k vidění“. Je to příkaz, který nic nedělá. Hodí se nám právě v případech, kdy potřebujeme napsat funkci nebo třídu, která existuje, ale nic nedělá.
☞Příkaz
pass
znamená v Pythonu totéž co prázdné složené závorky ({}
) v jazycích Java nebo C.
Mnohé třídy dědí z jiných tříd, ale to není náš případ. Mnohé třídy definují metody, ale tato ne. Pythonovská třída nemusí mít nic, jen jméno. Obzvláště programátorům v C++ může přijít divné, že pythonovské třídy nemají explicitní konstruktory a destruktory. Ačkoliv se to nevyžaduje, pythonovské třídy mohou mít něco, co se konstruktoru podobá. Je to metoda __init__()
.
__init__()
Následující příklad ukazuje inicializaci třídy Fib
s využitím metody __init__
.
class Fib:
'''iterator that yields numbers in the Fibonacci sequence''' ①
def __init__(self, max): ②
__init__()
je zavolána bezprostředně po vytvoření instance třídy. Svádí nás to, abychom ji nazývali „konstruktorem“ třídy, ale z technického hlediska to není pravda. Svádí nás to, protože vypadá jako C++ konstruktor (konvence říká, že by metoda __init__()
měla být v definici třídy uvedena jako první), chová se jako konstruktor (je to první kousek kódu, který se v nově vytvořené instanci třídy provádí) a vůbec. Chyba! V době volání metody __init__()
už byl objekt zkonstruován (už existoval) a na novou instanci třídy už máme platný odkaz.
Prvním argumentem metody třídy je vždy odkaz na aktuální instanci třídy a platí to i pro metodu __init__()
. Podle konvence je tento argument pojmenován self. Plní roli vyhrazeného slova, jakým je this
v jazycích C++ nebo Java, ale v Pythonu není self vyhrazeným slovem. Je to jen konvenční pojmenování. Přesto jej, prosím vás, nenazývejte nikdy jinak než self. Jde o velmi silnou konvenci.
U všech metod třídy odkazuje argument self na instanci třídy, jejíž metoda byla zavolána. Ale konkrétně v případě metody __init__()
je tato instance (jejíž metoda byla zavolána) nově vytvořeným objektem. V okamžiku definice metody musíme uvést self explicitně. Ale v okamžiku volání metody už tento argument neuvádíme. Python ho přidá za nás automaticky.
⁂
Vytváření instancí tříd je v Pythonu přímočaré. Jednoduše zavoláme třídu, jako kdyby to byla funkce, a předáme jí argumenty, které vyžaduje metoda __init__()
. Vrátí se nám nově vytvořený objekt.
>>> import fibonacci2 >>> fib = fibonacci2.Fib(100) ① >>> fib ② <fibonacci2.Fib object at 0x00DB8810> >>> fib.__class__ ③ <class 'fibonacci2.Fib'> >>> fib.__doc__ ④ 'iterator that yields numbers in the Fibonacci sequence'
Fib
(definované v modulu fibonacci2
) a nově vytvořenou instanci přiřazujeme do proměnné fib. Předáváme jeden parametr (100
), který se při volání metody __init__()
třídy Fib
stane jejím argumentem max.
Fib
.
__class__
, který odkazuje na třídu objektu. Programátoři v Javě možná znají třídu Class
. Ta poskytuje metody jako getName()
a getSuperclass()
, které nám zpřístupňují metainformace o objektu. V Pythonu je tento druh metadat přístupný prostřednictvím atributů, ale základní myšlenka je stejná.
docstring
.
☞Novou instanci třídy v Pythonu vytvoříme jednoduše zavoláním třídy, jako kdyby to byla funkce. Nenajdeme zde žádný explicitní operátor
new
, jako je tomu u jazyků C++ nebo Java.
⁂
Pokračujeme k dalšímu řádku:
class Fib:
def __init__(self, max):
self.max = max ①
__init__()
. self.max je „globální“ v rámci instance. To znamená, že k této proměnné můžeme přistupovat z jiných metod.
class Fib:
def __init__(self, max):
self.max = max ①
.
.
.
def __next__(self):
fib = self.a
if fib > self.max: ②
__init__()
…
__next__()
.
Členské proměnné jsou pro každou instanci třídy specifické. Pokud například vytvoříme dvě instance třídy Fib
s různými hodnotami maxima, bude si každá z nich pamatovat svou vlastní hodnotu.
>>> import fibonacci2 >>> fib1 = fibonacci2.Fib(100) >>> fib2 = fibonacci2.Fib(200) >>> fib1.max 100 >>> fib2.max 200
⁂
Až teď jsme připraveni se naučit, jak se vytváří interátor. Iterátor je jednoduše třída, která definuje metodu __iter__()
.
class Fib: ①
def __init__(self, max): ②
self.max = max
def __iter__(self): ③
self.a = 0
self.b = 1
return self
def __next__(self): ④
fib = self.a
if fib > self.max:
raise StopIteration ⑤
self.a, self.b = self.b, self.a + self.b
return fib ⑥
Fib
udělat třídu, a ne funkci.
Fib(max)
ve skutečnosti znamená vytvoření instance této třídy a zavolání její metody __init__()
s argumentem max. Metoda __init__()
uloží maximální hodnotu do členské proměnné, takže se na ni mohou později odkazovat ostatní metody.
__iter__()
se volá, kdykoliv někdo zavolá iter(fib)
. (Jak uvidíme za minutku, cyklus for
ji volá automaticky. Ale vy sami ji můžete volat také, ručně.) Po provedení inicializace na začátku iterace (v tomto případě jde o nastavení počátečního stavu dvou počítadel self.a
a self.b
) může metoda __iter__()
vrátit libovolný objekt, který implementuje metodu __next__()
. V našem případě (a ve většině případů) metoda __iter__()
vrátí jednoduše self, protože tato třída implementuje svou vlastní metodu __next__()
.
__next__()
se volá vždy, když někdo zavolá funkci next()
s iterátorem instance třídy. Za minutku to bude dávat větší smysl.
__next__()
vyvolá výjimku StopIteration
, signalizuje tím volajícímu, že iterace skončila. Na rozdíl od většiny jiných výjimek se zde nesignalizuje chyba. Jde o běžnou situaci, která prostě znamená, že iterátor už nemá žádná data, která by generoval. Pokud je volajícím cyklus for
, bude výjimka StopIteration
zachycena a cyklus bude bezproblémově ukončen. (Jinými slovy, cyklus výjimku spolkne.) Toto malé kouzlo je ve skutečnosti klíčem k použití iterátorů v cyklech for
.
__next__()
hodnotu jednoduše vrátí příkazem return
. Nepoužívejte zde příkaz yield
. Ten je pouze syntaktickým cukrátkem a má význam pouze v souvislosti s generátory. Zde vytváříme od základů svůj vlastní iterátor, proto budeme používat return
.
Už jste úplně zmatení? Výborně. Podívejme se, jak budeme iterátor volat:
>>> from fibonacci2 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
Cože? Vždyť je to úplně stejné! V každém bajtu se to shoduje s voláním generátoru Fibonacciho posloupnosti (až na rozdíl jednoho velkého písmene). Ale jak je to možné?
Cykly for
v sobě skrývají trochu magie. Odehrává se v nich následující:
for
volá Fib(1000)
, jak je vidět z kódu. Vrací se instance třídy Fib
. Říkejme jí třeba fib_inst.
for
potají a docela chytře volá funkci iter(fib_inst)
, která vrátí objekt iterátoru. Říkejme mu třeba fib_iter. V našem případě platí fib_iter == fib_inst, protože metoda __iter__()
vrací self. Ale o tom cyklus for
neví (a je mu to jedno).
for
funkci next(fib_iter)
, která zase volá metodu __next__()
objektu fib_iter
. Ta provede výpočet dalšího Fibonacciho čísla a vrací hodnotu. Cyklus for
hodnotu převezme, přiřadí ji do proměnné n a s touto hodnotou v n provede tělo cyklu.
for
ví, kdy má skončit? To jsem rád, že jste se zeptali! Když next(fib_iter)
vyvolá výjimku StopIteration
, cyklus for
ji spolkne a spořádaně se ukončí. (Jakákoliv jiná výjimka se propustí a projeví se obvyklým způsobem.) A kde jsme zahlédli výjimku StopIteration
? No přece v metodě __next__()
!
⁂
Přišel čas na finále. Přepišme generátor pravidel pro množné číslo do podoby iterátoru.
class LazyRules:
rules_filename = 'plural6-rules.txt'
def __init__(self):
self.pattern_file = open(self.rules_filename, encoding='utf-8')
self.cache = []
def __iter__(self):
self.cache_index = 0
return self
def __next__(self):
self.cache_index += 1
if len(self.cache) >= self.cache_index:
return self.cache[self.cache_index - 1]
if self.pattern_file.closed:
raise StopIteration
line = self.pattern_file.readline()
if not line:
self.pattern_file.close()
raise StopIteration
pattern, search, replace = line.split(None, 3)
funcs = build_match_and_apply_functions(
pattern, search, replace)
self.cache.append(funcs)
return funcs
rules = LazyRules()
Tohle je tedy třída, která implementuje metody __iter__()
a __next__()
, takže ji můžeme použít jako iterátor. Za koncem její definice se vytvoří instance třídy a přiřadí se do rules. To se stane jen jednou, při importu.
Proberme si zmíněnou třídu po kouscích.
class LazyRules:
rules_filename = 'plural6-rules.txt'
def __init__(self):
self.pattern_file = open(self.rules_filename, encoding='utf-8') ①
self.cache = [] ②
LazyRules
(líná pravidla), otevře se soubor s definicemi vzorků, ale nic se z něj nečte. (K tomu dojde později.)
__next__()
).
Než budeme pokračovat, podívejme se podrobněji na rules_filename. Tato proměnná není definována uvnitř metody __init__()
. Ve skutečnosti není definována uvnitř žádné metody. Je definována na úrovni třídy. Jde o proměnnou třídy. Ačkoliv k ní můžeme přistupovat stejným způsobem jako k nějaké členské proměnné (self.rules_filename), sdílí ji všechny instance třídy LazyRules
.
>>> import plural6 >>> r1 = plural6.LazyRules() >>> r2 = plural6.LazyRules() >>> r1.rules_filename ① 'plural6-rules.txt' >>> r2.rules_filename 'plural6-rules.txt' >>> r2.rules_filename = 'r2-override.txt' ② >>> r2.rules_filename 'r2-override.txt' >>> r1.rules_filename 'plural6-rules.txt' >>> r2.__class__.rules_filename ③ 'plural6-rules.txt' >>> r2.__class__.rules_filename = 'papayawhip.txt' ④ >>> r1.rules_filename 'papayawhip.txt' >>> r2.rules_filename ⑤ 'r2-overridetxt'
__class__
, který zpřístupňuje třídu jako takovou.
Ale zpět k naší ukázce.
def __iter__(self): ①
self.cache_index = 0
return self ②
__iter__()
bude volána pokaždé, když někdo (dejme tomu cyklus for
) zavolá iter(rules)
.
__iter__()
udělat, je vrácení iterátoru. V tomto případě se vrací self, čímž dáváme najevo, že tato třída definuje nějakou metodu __next__()
, která se postará o vracení hodnot během iterace.
def __next__(self): ①
.
.
.
pattern, search, replace = line.split(None, 3)
funcs = build_match_and_apply_functions( ②
pattern, search, replace)
self.cache.append(funcs) ③
return funcs
__next__()
bude volána pokaždé, když někdo (dejme tomu cyklus for
) zavolá next(rules)
. Smysl této metody pochopíme, když začneme od jejího konce a půjdeme pozpátku. Takže pojďme na to.
build_match_and_apply_functions()
se nezměnila. Je pořád stejná, jako vždycky byla.
self.cache
.
Posuňme se zpět…
def __next__(self):
.
.
.
line = self.pattern_file.readline() ①
if not line: ②
self.pattern_file.close()
raise StopIteration ③
.
.
.
readline()
(poznámka: jednotné číslo, nikoliv množné readlines()
) přečte z otevřeného souboru přesně jeden řádek. Přesněji řečeno, přečte další řádek. (Souborové objekty jsou také iterátory! Iterátory jsou všude, až po základy…)
readline()
přečíst řádek do proměnné line, bude to neprázdný řetězec. Dokonce i kdyby soubor obsahoval prázdný řádek, skončí line jako jednoznakový řetězec '\n'
(znak konce řádku). Pokud se v proměnné line opravdu nachází prázdný řetězec, znamená to, že soubor už neobsahuje žádné další řádky ke čtení.
StopIteration
. Připomeňme si, že do tohoto bodu jsme se dostali, protože jsme potřebovali rozhodovací a aplikační funkci pro další pravidlo. Další pravidlo je definované dalším řádkem souboru… Ale další řádek už nemáme! Takže už nemáme co vrátit. Iterace skončila. (The iteration is over. ♫ The party’s over… ♫)
A jdeme pozpátku až k začátku metody __next__()
…
def __next__(self):
self.cache_index += 1
if len(self.cache) >= self.cache_index:
return self.cache[self.cache_index - 1] ①
if self.pattern_file.closed:
raise StopIteration ②
.
.
.
self.cache
bude mít podobu seznamu funkcí, které potřebujeme pro rozhodování a aplikaci jednotlivých pravidel. (Přinejmenším tohle by vám mělo být povědomé!) V self.cache_index
se pamatuje, která další (už zapamatovaná) položka se má vrátit příště. Pokud jsme dosud nevyčerpali prostor se zapamatovanými položkami (tj. pokud je délka self.cache
větší než self.cache_index
), pak jsme ji našli (cache hit)! Hurá! Rozhodovací a aplikační funkci můžeme vrátit z vyrovnávací paměti a nemusíme je budovat znovu.
Když to dáme všechno dohromady, provádí se následující:
LazyRules
, která je nazvaná rules (pravidla). Tato instance otevřela soubor se vzorky, ale nečetla z něj.
plural()
znovu, protože chce převést do množného čísla jiné slovo. Cyklus for
ve funkci plural()
zavolá iter(rules)
, což vede k nastavení indexu vyrovnávací paměti na začátek, ale nedojde k resetování otevřeného souborového objektu.
for
o hodnotu ze struktury rules, což vede k zavolání jeho metody __next__()
. Ale v tomto okamžiku už vyrovnávací paměť obsahuje jediný pár funkcí pro rozhodování a pro aplikaci — odpovídají vzorkům z prvního řádku souboru. Protože už byly vytvořeny a uloženy do vyrovnávací paměti při zpracování minulého slova, jsou z ní vybrány. Index do vyrovnávací paměti se zvýší a otevřený soubor zůstane nedotčen.
for
udělá další obrátku a zeptá se na další hodnotu ze seznamu rules. Tím se podruhé aktivuje metoda __next__()
. Tentokrát je ale vyrovnávací paměť vyčerpána, protože obsahovala jen jednu položku a my jsme požádali o druhou. Takže metoda __next__()
pokračuje v činnosti. Z otevřeného souboru přečte další řádek, vybuduje podle něj rozhodovací a aplikační funkci a dvojici uloží do vyrovnávací paměti.
readline()
. Ve vyrovnávací paměti se teď nachází více položek. Pokud znovu zahájíme vytváření množného čísla pro nové slovo, vyzkoušíme před případným čtením dalšího řádku souboru nejdříve všechny položky z vyrovnávací paměti.
Dosáhli jsme „množnočíselné“ nirvány.
import
provedou, jsou vytvoření jediné instance třídy a otevření souboru (ale nečte se z něj).
☞Je to opravdu nirvána? Inu, ano i ne. U příkladu s
LazyRules
musíme počítat s následujícím: soubor se vzorky se otevře (během__init__()
) a zůstane otevřen, dokud nebude dosaženo posledního pravidla. Soubor se nakonec uzavře při ukončení Pythonu nebo po zrušení poslední instance třídyLazyRules
, ale může to trvat velmi dlouho. Pokud je tato třída součástí dlouho běžícího procesu, nemusí interpret Pythonu skončit nikdy a také objekt třídyLazyRules
nemusí být nikdy zrušen.Dá se to obejít různými způsoby. Místo toho, aby byl soubor otevřen během
__init__()
a ponechán v otevřeném stavu pro čtení po jednom řádku, můžeme soubor otevřít, přečíst všechny řádky a soubor hned zavřít. Nebo můžeme soubor otevřít, přečíst jeden řádek s pravidlem, uložit pozici v souboru zjištěnou metodoutell()
a soubor uzavřít. Později jej znovu otevřeme, použijeme metoduseek()
a pokračujeme ve čtení tam, kde jsme skončili. A nebo si s tím nebudeme dělat těžkou hlavu a prostě necháme soubor otevřený, jako to dělá tento příklad. Programování úzce souvisí s návrhem a návrh je založen na kompromisech a omezeních. Pokud bude soubor ponechán v otevřeném stavu příliš dlouho, může to vést k problému. Pokud místo toho vytvoříte komplikovanější kód, může to také vést k problému. Který z těchto problémů je větší, záleží na vašem vývojovém týmu, na vaší aplikaci a na provozním prostředí.
⁂
© 2001–11 Mark Pilgrim