Itt vagy: Kezdőlap ‣ Ugorj fejest a Python 3-ba ‣
Nehézségi szint: ♦♦♦♢♢
❝ A kelet kelet és a nyugat nyugat, és e kettő soha nem találkozhat. ❞
– Rudyard Kipling
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:
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?
⁂
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 ②
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.
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.
__init__()
metódusEz 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): ②
docstring
-ekkel is, mint a modulok és függvények.
__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.
⁂
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'
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.
Fib
osztály egy példánya.
__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.
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.
⁂
Folytassuk a következő sorral:
class Fib:
def __init__(self, max):
self.max = max ①
__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: ②
__init__()
metódusban van definiálva…
__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
⁂
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.
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
-nek osztálynak (class), és nem függvénynek kell lennie.
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á.
__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.
__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.
__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.
__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:
for
ciklus meghívja a Fib(1000)
osztályt, ahogy az látható. Ez visszaadja a Fib
osztály egy példányát. Nevezzük ezt fib_pld-nek.
for
ciklus meghívja az iter(fib_pld)
-t, amely egy iterátorobjektumot ad vissza. Nevezzük ezt fib_iter-nek. Ebben az esetben fib_iter == fib_pld, mivel az __iter__()
metódus a self objektumot adja vissza, de a for
ciklus erről nem tud (vagy nem érdekli).
for
ciklus meghívja a next(fib_iter)
metódust, amely meghívja a fib_iter
objektum __next__()
metódusát, amely végrehajtja a következő Fibonacci szám kiszámítását, és visszaad egy értéket. A for
ciklus veszi ezt az értéket, és hozzárendeli az n-hez, majd végrehajtja a for
ciklus törzsét az n ezen értékére.
for
ciklus, hogy mikor kell leállnia? Örülök, hogy megkérdezted! Amikor a next(fib_iter)
egy StopIteration
kivételt dob, a for
ciklus lenyeli a kivételt, és elegánsan kilép. (Minden más kivétel átjut, és a szokásos módon továbbadódik.) És hol láttál StopIteration
kivételt? A __next__()
metódusban, természetesen!
⁂
Eljött a finálé ideje. Írjuk újra a többesszámszabály-generátort iterátorként.
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 = [] ②
LazyRules
osztályt, megnyitjuk a mintafájlt, de nem olvasunk belőle semmit. (Ez később jön.)
__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'
__class__
attribútumon keresztül érheted el.
Most pedig vissza a műsorunkhoz.
def __iter__(self): ①
self.cache_index = 0
return self ②
__iter__()
metódus meghívásra kerül minden alkalommal, amikor valami – mondjuk egy for
ciklus – meghívja az iter(rules)
metódust.
__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
__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.
build_match_and_apply_functions()
függvény nem változott, ugyanaz mint volt.
self.cache
-ben.
Visszafelé haladva…
def __next__(self):
.
.
.
line = self.pattern_file.readline() ①
if not line: ②
self.pattern_file.close()
raise StopIteration ③
.
.
.
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…)
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.
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 ②
.
.
.
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.
Összegezve a következők történnek:
LazyRules
osztályból, ennek neve rules, amely megnyitja a mintafájlt, de nem olvas belőle.
plural()
függvényt, hogy egy másik szót is többes számba tegyen. A plural()
függvény for
ciklusa meghívja az iter(rules)
-t, ami visszaállítja a gyorsítótár indexét, de nem állítja vissza a nyitott fájlobjektumot.
for
ciklus lekér egy értéket a rules objektumtól, amely meghívja a __next__()
metódusát. Ez alkalommal azonban a gyorsítótárban már van egy illesztési és alkalmazási függvénypár, amely megfelel a mintafájl első sorában lévő mintáknak. Mivel ezek az előző szó többes számba tétele során már felépítésre és gyorsítótárazásra kerültek, a gyorsítótárból lesznek lekérve. A gyorsítótár indexe nő, a megnyitott fájlhoz semmi nem nyúl hozzá.
for
ciklus újra lefut, és újabb értéket kér a rules objektumtól. Ez másodszor is meghívja a __next__()
metódust. Ez alkalommal a gyorsítótár kimerült – csak egy elemet tartalmazott, és most egy másodikat kérünk – így a __next__()
metódus folytatódik. Így beolvas egy újabb sort a nyitott fájlból, felépíti az illesztési és alkalmazási függvényeket, és gyorsítótárazza az eredményt.
readline()
parancsra várakozva. Ezalatt a gyorsítótár már több elemet tartalmaz, és ha a program újra elkezdődik, és megpróbál egy új szót többes számba tenni, akkor a gyorsítótár minden elemét végigpróbálja, mielőtt beolvasná a következő sort a mintafájlból.
Elértük a többes számok nirvánáját.
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).
☞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 aLazyRules
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 aLazyRules
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 atell()
metódussal, bezárhatod a fájlt, és később újranyithatod aseek()
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.
⁂
© 2001–11 Mark Pilgrim