Itt vagy: Kezdőlap Ugorj fejest a Python 3-ba

Nehézségi szint: ♦♦♦♢♢

Osztályok és iterátorok

A kelet kelet és a nyugat nyugat, és e kettő soha nem találkozhat.
Rudyard Kipling

 

Ugorj fejest

Az iterátorok a Python 3 „titkos hozzávalói”. Mindenütt ott vannak, mindennek alapjául szolgálnak, de soha nincsenek szem előtt. A feldolgozók csupán az iterátorok egyszerűbb formái. A generátorok csupán az iterátorok egyszerűbb formái. Egy értékeket a yield használatával visszaadó függvény csupán egy elegáns, tömör módszer iterátor készítésére, iterátor készítése nélkül. Hadd mutassam meg, mit értek ez alatt.

Emlékszel a Fibonacci generátorra? Itt van egy nulláról felépített iterátorként:

[a fibonacci2.py letöltése]

class Fib:
    '''a Fibonacci sorozat számait eredményező iterátor'''

    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

Nézzük meg soronként.

class Fib:

class? Mi az a class?

Osztályok definiálása

A Python teljesen objektumorientált: saját osztályokat definiálhatsz, örököltethetsz saját vagy beépített osztályokból, és példányosíthatod az általad definiált osztályokat.

Pythonban egyszerűen lehet osztályokat definiálni. A függvényekhez hasonlóan itt sincs önálló felületdefiníció. Csak definiáld az osztályt, és kezdj kódolni. A Python osztály a class foglalt szóval kezdődik, amelyet az osztálynév követ. Technikailag ez minden ami kell, mivel az osztály nem egy másik osztályból öröklődik.

class PapayaWhip:  
    pass           
  1. Ennek az osztálynak a neve PapayaWhip, és nem egy másik osztályból öröklődik. Az osztálynevek általában nagybetűsekMindenSzótNagybetűvelKezdve, de ez csak megállapodás, nem követelmény.
  2. Már valószínűleg rájöttél, de az osztályon belül minden be van húzva, mint a függvényeken, az if utasításon, a for cikluson vagy bármely más kódblokkon belüli kód. Az első be nem húzott sor már kívül van az osztályon.

Ez a PapayaWhip osztály nem definiál metódusokat vagy attribútumokat, de szintaktikailag kell valaminek lennie a definícióban, így jön a képbe a pass utasítás. Ez a Python egyik foglalt szava, és azt jelenti: „haladj tovább, nincs itt semmi látnivaló”. Ez az utasítás nem csinál semmit, és remek helykitöltő, amikor függvények vagy osztályok vázát hozod létre.

A pass utasítás Pythonban olyan, mint egy üres pár kapcsos zárójel ({}) Java-ban vagy C-ben.

Sok osztály más osztályokból öröklődik, de ez nem. Sok osztály definiál metódusokat, de ez nem. Nincs semmi, amit egy Python osztálynak feltétlenül tartalmaznia kell, a nevén kívül. Különösen a C++ programozók találhatják furának, hogy a Python osztályok nem rendelkeznek explicit konstruktorokkal és destruktorokkal. Noha nem kötelező, a Python osztályok rendelkezhetnek a konstruktorhoz hasonló dologgal: ez az __init__() metódus.

Az __init__() metódus

Ez a példa bemutatja a Fib osztály inicializálását az __init__ metódus használatával.

class Fib:
    '''a Fibonacci sorozat számait eredményező iterátor'''  

    def __init__(self, max):                                      
  1. Az osztályok rendelkezhetnek (és kell is rendelkezniük) docstring-ekkel is, mint a modulok és függvények.
  2. Az __init__() metódus az osztály minden példányának létrehozása után azonnal meghívásra kerül. Csábító  – de technikailag helytelen –  lenne ezt az osztály „konstruktorának” nevezni. Csábító, mert úgy néz ki, mint egy C++ konstruktor (megállapodás szerint az osztályhoz először az __init__() metódus kerül definiálásra), úgy is működik (az osztály új példányában ez az elsőként lefutó kód), és még úgy is hangzik. De ez tévedés, mert az objektum már kész van, amikor az __init__() metódus meghívásra kerül, és már rendelkezel érvényes hivatkozással az osztály új példányára.

Minden osztálymetódus első argumentuma, beleértve az __init__() metódust is, mindig egy hivatkozás az osztály aktuális példányára. Megállapodás szerint ennek az argumentumnak a neve self. Ez az argumentum tölti ki a C++ vagy Java this foglalt szavának szerepét, de a self nem foglalt szó a Pythonban, csupán elnevezési megállapodás. Ezzel együtt ne nevezd másnak, csak self-nek; ez egy nagyon erős megállapodás.

Minden osztálymetódusban, a self arra a példányra vonatkozik, amelynek metódusa meghívásra került. De az __init__() metódus egyedi esetében az a példány, amelynek metódusa meghívásra került, egyben az újonnan létrehozott objektum is. Noha a metódus definiálásakor explicit módon meg kell adnod a self-et, a metódus hívásakor nem kell megadnod: a Python automatikusan felveszi helyetted.

Osztályok példányosítása

Az osztályok példányosítása Pythonban magától érthetődő. Egy osztály példányosításához egyszerűen hívd meg azt, mintha függvény lenne, átadva az __init__() metódus által igényelt argumentumokat. A visszatérési érték az újonnan létrehozott objektum lesz.

>>> import fibonacci2
>>> fib = fibonacci2.Fib(100)  
>>> fib                        
<fibonacci2.Fib object at 0x00DB8810>
>>> fib.__class__              
<class 'fibonacci2.Fib'>
>>> fib.__doc__                
'a Fibonacci sorozat számait eredményező iterátor'
  1. Itt létrehozol egy példányt a Fib osztályból (amely a fibonacci2 modulban van definiálva), és hozzárendeled az újonnan létrehozott példányt a fib változóhoz. Egy paramétert adsz át: a 100 a Fib __init__() metódusának max argumentuma lesz.
  2. A fib most a Fib osztály egy példánya.
  3. Minden osztálypéldány rendelkezik egy beépített attribútummal: a __class__ az objektum osztályát tartalmazza. A Java programozóknak ismerős lehet a Class osztály, amely olyan metódusokat tartalmaz, mint a getName() és getSuperclass() az objektum metaadatainak lekéréséhez. A Pythonban az ilyen metaadatok attribútumokon keresztül érhetők el, de az ötlet ugyanaz.
  4. A példány docstring-jét ugyanúgy érheted el, mint egy függvényét vagy modulét. Egy osztály minden példánya ugyanazzal a docstring-gel rendelkezik.

Pythonban egyszerűen hívd meg az osztályt a példányosításhoz, mintha csak egy függvény lenne. Nincs explicit new operátor, mint C++-ban vagy Java-ban.

Példányváltozók

Folytassuk a következő sorral:

class Fib:
    def __init__(self, max):
        self.max = max        
  1. Mi az a self.max? Ez egy példányváltozó. Teljesen elkülönül az __init__() metódusnak argumentumként átadott max értéktől. A self.max „globális” a példányra vonatkozóan. Ez azt jelenti, hogy más metódusokból is elérheted.
class Fib:
    def __init__(self, max):
        self.max = max        
    .
    .
    .
    def __next__(self):
        fib = self.a
        if fib > self.max:    
  1. A self.max az __init__() metódusban van definiálva…
  2. …és a __next__() metódus hivatkozik rá.

A példányváltozók egy osztály egy példányára jellemzők. Ha például két Fib példányt hozol létre eltérő maximális értékekkel, akkor mindkettő megjegyzi a saját értékeit.

>>> import fibonacci2
>>> fib1 = fibonacci2.Fib(100)
>>> fib2 = fibonacci2.Fib(200)
>>> fib1.max
100
>>> fib2.max
200

Egy Fibonacci iterátor

Most már készen állsz az iterátorok felépítésének megtanulására. Az iterátor csupán egy olyan osztály, amely egy__iter__() metódust is definiál.

[a fibonacci2.py letöltése]

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. Egy iterátor nulláról való felépítéséhez a Fib-nek osztálynak (class), és nem függvénynek kell lennie.
  2. A Fib(max) „hívása” valójában egy példányt hoz létre az osztályból, és meghívja az __init__() metódusát a max értékkel. Az __init__() metódus példányváltozóként elmenti a maximális értéket, így más metódusok később hivatkozhatnak rá.
  3. Az __iter__() metódus az iter(fib) hívásakor kerül meghívásra. (Amint azt egy percen belül látni fogod, egy for ciklus automatikusan meghívja ezt, de saját kezűleg is hívhatod.) Az iterációt kezdő inicializálás végrehajtása (ebben az esetben a self.a és a self.b, a két számlálónk alaphelyzetbe állítása) után az __iter__() metódus tetszőleges olyan objektumot adhat vissza, amely megvalósít egy __next__() metódust. Ebben az esetben (és a legtöbb esetben) az __iter__() egyszerűen a self objektumot adja vissza, mivel ez az osztály megvalósítja a saját __next__() metódusát.
  4. A __next__() metódus mindig meghívásra kerül, amikor valami a next() metódust hívja egy osztálypéldány iterátorán. Ennek egy percen belül több értelme lesz.
  5. Amikor a __next__() metódus StopIteration kivételt dob, ez jelzi a hívónak, hogy az iteráció kimerült. A legtöbb kivétellel ellentétben ez nem hiba: egy normális helyzet, amely azt jelenti, hogy az iterátor kifogyott az előállítható értékekből. Ha a hívó egy for ciklus, akkor észlelni fogja ezt a StopIteration kivételt, és elegánsan kilép a ciklusból. (Más szóval, lenyeli a kivételt.) Ez az apró varázslat valójában az iterátorok for ciklusokban való használatának a kulcsa.
  6. A következő érték kiköpéséhez az iterátor __next__() metódusa egyszerűen visszaadja az értéket a return utasítással. Ne használd itt a yield utasítást, ez egy olyan szintaktikai máz, amely csak generátorok használatakor érvényes. Itt saját iterátort hozol létre a nulláról, így használd helyette a return utasítást.

Rendesen összezavarodtál már? Kitűnő. Lássuk, hogyan hívhatod meg ezt az iterátort:

>>> 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

Hogyan, hisz ez pontosan ugyanaz! Bájtról bájtra egyezik a Fibonacci, mint generátor hívásával (mínusz egy nagybetű). De hogyan?

A for ciklusokban egy kis varázslat történik. A következő megy végbe:

Egy többesszámszabály-iterátor

Eljött a finálé ideje. Írjuk újra a többesszámszabály-generátort iterátorként.

[a plural6.py letöltése]

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()

Ez tehát egy olyan osztály, amely megvalósítja az __iter__() és __next__() metódusokat, így használható iterátorként. Ezután példányosítod az osztályt, és hozzárendeled a rules változóhoz. Ez csak egyszer történik, az importáláskor.

Nézzük meg az osztályt, apró falatonként.

class LazyRules:
    rules_filename = 'plural6-rules.txt'

    def __init__(self):
        self.pattern_file = open(self.rules_filename, encoding='utf-8')  
        self.cache = []                                                  
  1. Amikor példányosítjuk a LazyRules osztályt, megnyitjuk a mintafájlt, de nem olvasunk belőle semmit. (Ez később jön.)
  2. A mintafájl megnyitása után inicializáljuk a gyorsítótárat. Ezt a gyorsítótárat később fogod használni (a __next__() metódusban), ahogy kiolvasod a sorokat a mintafájlból.

Mielőtt folytatjuk, vessünk egy közelebbi pillantást a rules_filename-re. Ez nem az __iter__() metóduson belül van definiálva. Tulajdonképpen egyik metódusban sincs definiálva. Az osztály szintjén van definiálva. Ez egy osztályváltozó, és noha úgy érheted el, mint egy példányváltozót (self.rules_filename), meg van osztva a LazyRules osztály minden példánya között.

>>> 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. Az osztály minden példánya örökli a rules_filename attribútumot, az osztály által meghatározott értékkel.
  2. Az attribútum értékének módosítása az egyik példányban nem befolyásolja a többi példányt…
  3. …és az osztályattribútumot sem módosítja. Az osztályattribútumot (szemben az egyedi példányok attribútumaival) az osztály elérésére használt speciális __class__ attribútumon keresztül érheted el.
  4. Az osztályattribútum módosítása az összes olyan példányt (mint itt az r1) befolyásolja, amely még mindig az adott örökölt értékkel rendelkezik.
  5. Azon példányokat (mint itt az r2), amelyek az attribútumot felülbírálták, ez nem befolyásolja.

Most pedig vissza a műsorunkhoz.

    def __iter__(self):       
        self.cache_index = 0
        return self           
  1. Az __iter__() metódus meghívásra kerül minden alkalommal, amikor valami – mondjuk egy for ciklus – meghívja az iter(rules) metódust.
  2. Az egyetlen dolog, amit minden __iter__() metódusnak meg kell tennie, az egy iterátor visszaadása. Ebben az esetben a self-et adja vissza, ami azt jelzi, hogy ez az osztály definiál egy __next__() metódust, amely elvégzi az értékek visszaadását az iteráció során.
    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. A __next__() metódus meghívásra kerül, amikor valami – mondjuk egy for ciklus – meghívja a next(rules)-t. Ez a metódus csak akkor nyeri el értelmét, ha visszafelé haladva kezdjük megfejteni. Tegyünk így.
  2. A függvény utolsó része mindenképp ismerős kell legyen. A build_match_and_apply_functions() függvény nem változott, ugyanaz mint volt.
  3. Az egyetlen különbség, hogy a (funcs tuple-ben tárolt) illesztési és alkalmazási függvények visszaadása előtt ezeket elmentjük a self.cache-ben.

Visszafelé haladva…

    def __next__(self):
        .
        .
        .
        line = self.pattern_file.readline()  
        if not line:                         
            self.pattern_file.close()
            raise StopIteration              
        .
        .
        .
  1. Ez itt némi haladó fájltrükközés. A readline() metódus (figyelj: ez egyes számban van, nem a többes számú readlines()) pontosan egy sort olvas egy megnyitott fájlból. Egész pontosan a következő sort. (A fájlobjektumok is iterátorok! Itt végig iterátorokkal dolgozunk…)
  2. Ha a readline() be tudott olvasni egy sort, akkor a line egy nem üres karakterlánc lesz. Ha a fájl csak egy üres sort tartalmazna, a line egy '\n' (kocsivissza) karakterből álló sor lenne. Ha a line egy ténylegesen üres karakterlánc, akkor elfogytak a beolvasható sorok a fájlból.
  3. A fájl végének elérésekor bezárjuk a fájlt, és a bűvös StopIteration kivételt dobjuk. Ne feledd, azért jutottunk ide, mert egy illesztés és alkalmazás függvényre volt szükség a következő szabályhoz. A következő szabály a fájl következő sorából jön… de nincs következő sor! Emiatt nincs visszaadható érték. Az iterációnak vége. ( Vége a dalnak… )

Visszafelé haladva egészen a __next__() metódus elejéig…

    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. A self.cache az egyes szabályok illesztéséhez és alkalmazásához szükséges függvények listája lesz. (Ha másnak nem, ennek ismerősen kell hangzania!) A self.cache_index nyilvántartja, hogy melyik gyorsítótárazott elemet kell legközelebb visszaadni. Ha még nem ürítettük ki a gyorsítótárat (azaz ha a self.cache hossza nagyobb, mint a self.cache_index), akkor találat van a gyorsítótárban! Hurrá! Visszaadhatjuk az illesztési és alkalmazási függvényeket a gyorsítótárból, azok nulláról való felépítése helyett.
  2. Másrészről, ha nincs találat a gyorsítótárból, és a fájlobjektum le lett zárva (ami megtörténhet, a metódus későbbi részében, ahogy az előző kóddarabban láthattad), akkor nincs mit tenni. Ha a fájl le van zárva, akkor kimerítettük – már beolvastuk a mintafájl összes sorát, valamint felépítettük és gyorsítótáraztuk az illesztési és alkalmazási függvényeket minden mintához. A fájl kimerült, a gyorsítótár kimerült, én is kimerültem. Várjunk csak? Maradj még, már majdnem kész vagyunk.

Összegezve a következők történnek:

Elértük a többes számok nirvánáját.

  1. Minimális indítási költség. Az import hívásakor egyetlen osztály példányosítása és egy fájl megnyitása történik (de nem olvasunk belőle).
  2. Maximális teljesítmény. Az előző példa végigolvassa a fájlt, és dinamikusan felépíti a függvényeket minden alkalommal, amikor többes számba szeretnél tenni egy szót. Ez a verzió azonnal gyorsítótárazza a függvényeket, amint azok elkészülnek, és a legrosszabb esetben is csak egyszer olvassa végig a mintafájlt, akárhány szót is próbálsz többes számba tenni.
  3. A kód és adatok elkülönítése. Minden minta egy önálló fájlban kerül tárolásra. A kód kód, az adat adat, és e kettő soha nem találkozhat.

Tényleg ez a nirvána? Hát igen is, meg nem is. Egy dolgot szem előtt kell tartani a LazyRules példával kapcsolatban: a mintafájl megnyitásra kerül (az __init__() során), és az utolsó szabály eléréséig nyitva marad. A Python végül bezárja a fájlt amikor kilép, vagy miután a LazyRules osztály utolsó példánya is megsemmisült, de akkor is, ez sokáig tarthat. Ha ez az osztály egy sokáig futó Python folyamat része, akkor a Python értelmező lehet, hogy soha nem lép ki, és a LazyRules objektum soha nem kerül megsemmisítésre.

Ezt el lehet kerülni. A fájl __init__() alatti megnyitása és nyitva hagyása helyett a szabályok egyenkénti beolvasása alatt, megnyithatod a fájlt, beolvashatod az összes szabályt, és azonnal bezárhatod a fájlt. Vagy megnyithatod a fájlt, beolvashatsz egy szabályt, elmentheted a fájlon belüli pozíciót a tell() metódussal, bezárhatod a fájlt, és később újranyithatod a seek() metódussal az olvasás folytatásához onnan, ahol abbahagytad. Vagy dönthetsz úgy, hogy nem aggódsz emiatt, és egyszerűen nyitva hagyhatod a fájlt, ahogy ez a példakód teszi. A programozás tervezés, és a tervezés a kompromisszumokról és megszorításokról szól. Egy fájlt túl sokáig nyitva hagyni gondot jelenthet, de a kódot bonyolultabbá tenni is gondot jelenthet. Hogy melyik a nagyobb probléma, az a fejlesztői csapattól, az alkalmazástól és a futási környezettől függ.

További olvasnivaló

© 2001–11 Mark Pilgrim