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

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

Třídy a iterátory

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

 

Ponořme se

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:

[stáhnout fibonacci2.py]

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?

Definice tříd

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           
  1. Jméno této třídy je 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é.
  2. Asi už jste odhadli, že vše uvnitř třídy je odsazené — podobně jako kód uvnitř funkce, v příkazu 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__().

Metoda __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):                                      
  1. Třídy mohou (a měly by) mít své dokumentační řetězce — stejně jako moduly a funkce.
  2. Metoda __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

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'
  1. Vytváříme instanci třídy 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.
  2. fib je nyní instancí třídy Fib.
  3. Každá instance třídy má zabudovaný atribut __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á.
  4. Dokumentační řetězec instance můžeme zpřístupnit stejně jako u funkce nebo u modulu. Všechny instance třídy sdílejí 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.

Členské proměnné

Pokračujeme k dalšímu řádku:

class Fib:
    def __init__(self, max):
        self.max = max        
  1. Co to je self.max? Jde o členskou proměnnou (nebo také instanční proměnnou nebo proměnnou instance). Je to něco zcela jiného než argument max, který byl předán metodě __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:    
  1. self.max je definována metodou __init__()
  2. … a odkazujeme se na ni v metodě __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

Fibonacciho iterátor

Až teď jsme připraveni se naučit, jak se vytváří interátor. Iterátor je jednoduše třída, která definuje metodu __iter__().

[stáhnout fibonacci2.py]

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                                
  1. Abychom vybudovali iterátor od základů, musíme z Fib udělat třídu, a ne funkci.
  2. „Volání“ 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.
  3. Metoda __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__().
  4. Metoda __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.
  5. Když metoda __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.
  6. Vyprodukování další hodnoty provede iterátor tak, že metoda __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í:

Iterátor pro pravidla množného čísla

Přišel čas na finále. Přepišme generátor pravidel pro množné číslo do podoby iterátoru.

[stáhnout plural6.py]

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 = []                                                  
  1. Když vytvoříme instanci třídy LazyRules (líná pravidla), otevře se soubor s definicemi vzorků, ale nic se z něj nečte. (K tomu dojde později.)
  2. Po otevření souboru se inicializuje vyrovnávací paměť (cache). Budeme ji používat později, během čtení řádků ze souboru vzorků (v metodě __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'
  1. Každá instance třídy dědí atribut rules_filename s hodnotou definovanou na úrovni třídy.
  2. Když změníme hodnotu tohoto atributu v jedné instanci, neovlivníme tím ostatní instance…
  3. …a ani neovlivníme atribut třídy. K atributu třídy (v protikladu k atributu jednotlivých instancí) můžeme přistupovat prostřednictvím speciálního atributu __class__, který zpřístupňuje třídu jako takovou.
  4. Pokud změníte hodnotu atributu třídy, pak to ovlivní všechny instance, které tuto hodnotu dosud dědí (zde r1).
  5. Instance, které tento atribut přepsaly (zde r2), ovlivněny nebudou.

Ale zpět k naší ukázce.

    def __iter__(self):       
        self.cache_index = 0
        return self           
  1. Metoda __iter__() bude volána pokaždé, když někdo (dejme tomu cyklus for) zavolá iter(rules).
  2. Jednou z věcí, kterou musí každá metoda __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
  1. Metoda __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.
  2. Poslední část této funkce by vám měla být přinejmenším povědomá. Funkce build_match_and_apply_functions() se nezměnila. Je pořád stejná, jako vždycky byla.
  3. Jediný rozdíl spočívá v tom, že před vrácením rozhodovací a aplikační funkce (jsou uloženy v dvojici funcs) je nejdříve uložíme do self.cache.

Posuňme se zpět…

    def __next__(self):
        .
        .
        .
        line = self.pattern_file.readline()  
        if not line:                         
            self.pattern_file.close()
            raise StopIteration              
        .
        .
        .
  1. Tady použijeme fintu se souborem pro trošku pokročilejší. Metoda 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…)
  2. Pokud mohla 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í.
  3. Když dosáhneme konce souboru, měli bychom soubor zavřít a vyvolat magickou výjimku 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                         
        .
        .
        .
  1. 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.
  2. Na druhou stranu, pokud jsme na položku ve vyrovnávací paměti nenarazili a zároveň je souborový objekt už uzavřen (což se níže v kódu metody může stát — jak jsme viděli v předcházející ukázce), pak už nemůžeme nic víc dělat. Pokud je soubor uzavřen, znamená to, že jsme jeho obsah vyčerpali. Už jsme přečetli každý jeho řádek a vybudovali jsme funkce pro rozhodování a pro aplikaci pro každý vzorek a uložili jsme je do vyrovnávací paměti. Soubor je vyčerpaný, vyrovnávací paměť je vyčerpaná, já jsem vyčerpaný. Počkat! Co? „Выдержай пионер“ [vyděržaj pijaněr], už je to skoro hotové.

Když to dáme všechno dohromady, provádí se následující:

Dosáhli jsme „množnočíselné“ nirvány.

  1. Minimální startovací čas. Jediné činnosti, které se při příkazu import provedou, jsou vytvoření jediné instance třídy a otevření souboru (ale nečte se z něj).
  2. Maximální výkonnost. U předcházejícího příkladu bychom četli ze souboru a dynamicky budovali funkce pokaždé, když bychom chtěli vytvořit množné číslo zadaného slova. V této verzi dochází hned po vybudování funkcí k jejich uložení do vyrovnávací paměti a v nejhorším případě dojde k přečtení celého souboru jednou — nezávisle na tom, z kolika slov tvoříme množné číslo.
  3. Oddělení kódu a dat. Všechny vzorky jsou uložené v odděleném souboru. Kód je kód, data jsou data a ta dvojice se nikdy nesetká.

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řídy LazyRules, 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řídy LazyRules 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 metodou tell() a soubor uzavřít. Později jej znovu otevřeme, použijeme metodu seek() 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í.

Přečtěte si

© 2001–11 Mark Pilgrim