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

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

Closure-ök és generátorok

Az én írásom imbolygó. Jó írás, de imbolyog, és a betűk rossz helyre kerülnek.
– Micimackó

 

Ugorj fejest

Egy könyvtáros és egy angoltanár fiaként felnőve mindig is lenyűgöztek a nyelvek. Nem a programozási nyelvek. Na jó, a programozási nyelvek is, de a természetes nyelvek. Vegyük az angolt. Az angol egy skizofrén nyelv, amely szavakat kölcsönöz a németből, franciából, spanyolból és latinból (hogy csak párat említsek). Valójában a „kölcsönöz” nem is a jó szó, a „rablás” sokkal jellemzőbb. Vagy talán az „asszimilál” – mint a Borg. Igen, ez lesz az.

Itt a Borg. Nyelvi és etimológiai sajátosságaikat a miénkhez adjuk. Az ellenállás hasztalan.

Ebben a fejezetben a többesszámú angol főnevekről fogsz tanulni. Továbbá függvényeket visszaadó függvényekről, további reguláris kifejezésekről és generátorokról. De előbb hadd beszéljek az többesszámú angol főnevek felépítéséről. (Ha még nem olvastad a reguláris kifejezésekről szóló fejezetet, akkor itt az ideje. Ez a fejezet feltételezi, hogy megérted a reguláris kifejezések alapjait, és gyorsan belemerül a speciálisabb felhasználási módokba.)

Ha angolul beszélő országban nőttél fel, vagy az iskolában tanultál angolt, akkor valószínűleg tisztában vagy az alapvető szabályokkal:

(Tudom, rengeteg kivétel van. A man-ből men lesz és a woman-ből women, de a human-ből humans. A mouse-ból mice és a louse-ból lice, de a house-ból houses. A knife-ból knives lesz, és a wife-ból wives, de a lowlife-ból lowlifes. Az olyan szavakba pedig ne is menjünk bele, amelyek megegyeznek a többesszámaikkal, mint a sheep, deer és haiku.)

Más nyelvek természetesen teljesen mások.

Tervezzünk egy Python függvénytárat, amely automatikusan többesszámba teszi az angol főneveket. Csak ezzel a négy szabállyal indulunk, de ne felejtsd el, hogy elkerülhetetlenül szükség lesz továbbiak hozzáadására is.

Tudom, használjunk reguláris kifejezéseket!

Tehát szavakkal állunk szemben, ez legalábbis angolul azt jelenti, hogy karakterek sorozatai vannak előttünk. Vannak szabályaid, amelyek szerint különböző karakterkombinációkat kell keresned, majd különböző dolgokat kell velük csinálnod. Ez a feladat úgy hangzik, mintha a reguláris kifejezésekre szabták volna!

[a plural1.py letöltése]

import re

def többes(főnév):
    if re.search('[sxz]$', főnév):             
        return re.sub('$', 'es', főnév)        
    elif re.search('[^aeioudgkprt]h$', főnév):
        return re.sub('$', 'es', főnév)
    elif re.search('[^aeiou]y$', főnév):
        return re.sub('y$', 'ies', főnév)
    else:
        return főnév + 's'
  1. Ez egy reguláris kifejezés, de olyan szintaxist használ, amilyet a Reguláris kifejezések fejezetben nem láttál. A szögletes zárójelek azt jelentik: „illeszkedjen ezen karakterek közül pontosan egyre.” Így az [sxz] jelentése „s vagy x vagy z”, de csak az egyik. A $ már ismerős, a karakterlánc végére illeszkedik. Egyesítve ez a reguláris kifejezés azt teszteli, hogy a főnév s, x vagy z betűre végződik-e.
  2. Ez a re.sub() függvény reguláris kifejezésekre épülő karakterlánc-helyettesítéseket végez.

Nézzük meg részletesebben a reguláris kifejezések helyettesítéseit.

>>> 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'
  1. A Mark karakterlánc tartalmaz a, b vagy c betűt? Igen, tartalmaz egy a-t.
  2. Rendben, most keressük meg az a, b vagy c valamelyikét, és cseréljük le egy o-ra. A Mark-ból Mork lesz.
  3. Ugyanez a függvény a rock-ból rook-ot csinál.
  4. Azt gondolhatod, hogy ez a caps-ból oaps-ot csinál, de nem. A re.sub minden találatot helyettesít, nem csak a legelsőt. Így ez a reguláris kifejezés a caps-ból oops-ot csinál, mert a c és az a is o-vá változik.

Most térjünk vissza a többes() függvényhez…

def többes(főnév):
    if re.search('[sxz]$', főnév):
        return re.sub('$', 'es', főnév)         
    elif re.search('[^aeioudgkprt]h$', főnév):  
        return re.sub('$', 'es', főnév)
    elif re.search('[^aeiou]y$', főnév):        
        return re.sub('y$', 'ies', főnév)
    else:
        return főnév + 's'
  1. Itt a karakterlánc végét (amely a $ jelre illeszkedik) cseréled az es karakterláncra. Más szóval hozzáteszed az es-t a karakterlánchoz. Ugyanezt elérhetnéd karakterláncok összefűzésével is, például így: főnév + 'es', de minden szabályhoz a reguláris kifejezések használatát választottam, a fejezet későbbi részeiben világossá váló okok miatt.
  2. Alaposan nézd meg, ez egy másik új variáció. A ^ a szögletes zárójelen belüli első karakterként valami különlegeset jelent: tagadást. A [^abc] azt jelenti: „bármely karakter, kivéve az a, b vagy c”. Így a [^aeioudgkprt] jelentése: bármely karakter, kivéve az a, e, i, o, u, d, g, k, p, r vagy t. Ezután a karaktert egy h, majd a karakterlánc vége kell kövesse. Olyan szavakat keresel, amelyek hallható H-ra végződnek.
  3. Itt ugyanaz a minta: olyan szavak illeszkednek, amelyek Y-ra végződnek, és az Y előtti karakter nem a, e, i, o vagy u. Olyan szavakat keresel, amelyek I-ként hangzó Y-ra végződnek.

Nézzük meg részletesebben a tagadó reguláris kifejezéseket.

>>> 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')     
>>> 
  1. A vacancy illeszkedik erre a reguláris kifejezésre, mert cy-ra végződik, és a c nem a, e, i, o vagy u.
  2. A boy nem illeszkedik, mert oy-ra végződik, és kifejezetten megadtad, hogy az y előtti karakter nem lehet o. A day sem illeszkedik, mert ay-ra végződik.
  3. A pita nem illeszkedik, mert nem y-ra végződik.
>>> re.sub('y$', 'ies', 'vacancy')               
'vacancies'
>>> re.sub('y$', 'ies', 'agency')
'agencies'
>>> re.sub('([^aeiou])y$', r'\1ies', 'vacancy')  
'vacancies'
  1. Ez a reguláris kifejezés a vacancy-ből vacancies-t csinál, az agency-ből pedig agencies-t, és pontosan ezt akartad. Ne feledd el, hogy a boy-ból is boies-t csinálna, de a függvényben ez soha nem fog megtörténni, mert előbb a re.search hívásával eldöntötted, hogy végre kell-e hajtani ezt a re.sub hívást.
  2. Csak mellékesen szeretném felhívni a figyelmed, hogy ezt a két reguláris kifejezést (az egyik a szabály alkalmazhatóságának vizsgálata, a másik a tényleges alkalmazása) egyetlen reguláris kifejezéssé lehet egyesíteni. Az egyesített kifejezés pedig így nézne ki. A zöme ismerős kell legyen: egy megjegyzett csoportot használsz, amelyet az Esettanulmány: telefonszámok értelmezése fejezetben ismerhettél meg. A csoport használatával az y előtti karakter kerül megjegyzésre. Ezután a helyettesítési karakterláncban új szintaxist használsz: a \1 jelentése: „hé, az elsőként megjegyzett csoport? Ide tedd.” Ebben az esetben a c-t jegyzed meg az y előtt; a helyettesítés végrehajtásakor a c a c helyére, az ies az y helyére kerül. (Ha több megjegyzett csoportod is van, akkor használhatod a \2 és \3 stb. jelöléseket.)

A reguláris kifejezések helyettesítései nagyon hatékonyak, és a \1 szintaxis csak még hatékonyabbá teszi őket. A teljes művelet egy reguláris kifejezéssé kombinálása azonban sokkal nehezebben olvasható, és nem felel meg a többesszám-szabályokban először leírt módszernek. Eredetileg ilyen szabályokat fektettél le: „ha a szó S, X vagy Z betűre végződik, tegyél utána egy ES-t”. Ha megnézed a függvényt, akkor két kódsorod van, amelyek azt mondják: „ha a szó S, X vagy Z betűre végződik, tegyél utána egy ES-t”. Ennél sokkal közvetlenebb már nem lehet.

Függvények listája

Most hozzáadunk egy absztrakciós szintet. Egy szabálylista definiálásával kezdted: ha ez van, csináld azt, egyébként lépj a következő szabályra. Ideiglenesen bonyolítsuk meg a program egy részét, hogy egy másik rész egyszerűsíthető legyen.

[a plural2.py letöltése]

import re

def match_sxz(főnév):
    return re.search('[sxz]$', főnév)

def apply_sxz(főnév):
    return re.sub('$', 'es', főnév)

def match_h(főnév):
    return re.search('[^aeioudgkprt]h$', főnév)

def apply_h(főnév):
    return re.sub('$', 'es', főnév)

def match_y(főnév):                             
    return re.search('[^aeiou]y$', főnév)

def apply_y(főnév):                             
    return re.sub('y$', 'ies', főnév)

def match_default(főnév):
    return True

def apply_default(főnév):
    return főnév + 's'

szabályok = ((match_sxz, apply_sxz),               
         (match_h, apply_h),
         (match_y, apply_y),
         (match_default, apply_default)
         )

def többes(főnév):
    for matches_rule, apply_rule in szabályok:       
        if matches_rule(főnév):
            return apply_rule(főnév)
  1. Most minden illesztési szabályhoz saját függvény tartozik, amely a re.search() függvény hívásának eredményét adja vissza.
  2. Minden alkalmazási szabályhoz is saját függvény tartozik, amely meghívja a re.sub() függvényt a megfelelő többes számba tevő szabály alkalmazásához.
  3. Egy több szabályt tartalmazó függvény (többes()) helyett a szabályok nevű adatszerkezetet használjuk, amely függvénypárok sorozatát tartalmazza.
  4. Mivel a szabályok ki lettek szervezve egy önálló adatszerkezetbe, az új többes() függvény néhány sornyi kódra csökkenthető. Egy for ciklus használatával kettesével veheted ki az illesztési és alkalmazási szabályokat (egy illesztés, egy alkalmazás) a szabályok adatszerkezetből. A for ciklus első ismétlésében a matches_rule értéke match_sxz, az apply_rule értéke pedig apply_sxz lesz. A második ismétlésben (feltételezve, hogy eljut odáig), a matches_rule értéke match_h, az apply_rule értéke apply_h lesz. A függvény garantáltan visszaad valamit, mert az utolsó illesztési szabály (match_default) egyszerűen a True értéket adja vissza, ami azt jelenti, hogy a megegyező alkalmazási szabály (apply_default) mindig felhasználásra kerül.

Ez a módszer azért működik, mert Pythonban minden objektum, beleértve a függvényeket is. A szabályok adatszerkezet függvényeket tartalmaz – nem függvényneveket, hanem tényleges függvényobjektumokat. Amikor a for ciklusban értékként átadásra kerülnek, akkor a matches_rule és az apply_rule tényleges, meghívható függvényekké válnak. A for ciklus első ismétlésében ez egyenértékű a matches_sxz(főnév) hívásával, és ha ez találatot ad vissza, akkor az apply_sxz(noun) hívásával.

Ha ez a további absztrakciós szint bonyolult, akkor próbáld meg lebontani a függvényt az azonosság megértéséhez. A teljes for ciklus azonos a következővel:


def többes(főnév):
    if match_sxz(főnév):
        return apply_sxz(főnév)
    if match_h(főnév):
        return apply_h(főnév)
    if match_y(főnév):
        return apply_y(főnév)
    if match_default(főnév):
        return apply_default(főnév)

Ennek az az előnye, hogy a többes() függvény egyszerűbbé vált. Máshol megadott szabályok sorozatát várja, és általános módon halad rajtuk végig.

  1. Végy egy illesztési szabályt
  2. Illeszkedik? Akkor hívd meg az alkalmazási szabályt, és add vissza az eredményt.
  3. Nem illeszkedik? Vissza az 1. lépésre.

A szabályok bárhol megadhatók, tetszőleges módon. A többes() függvényt nem érdekli.

Na most, megérte bevezetni ezt az absztrakciós szintet? Hát, még nem. Fontoljuk meg, hogy mibe kerülne egy új szabályt hozzáadni a függvényhez. Az első példában egy if utasítást kellene hozzáadni a többes() függvényhez. Ebben a második példában két függvény, a match_foo() és az apply_foo() hozzáadása lenne szükséges, majd a szabályok sorozat frissítése annak megadásához, hogy az új illesztés és alkalmazás függvények milyen sorrendben hívandók meg a többi szabályhoz képest.

De ez csak egy lépcső a következő szakaszhoz. Haladjunk tovább…

Minták listája

Igazából nem muszáj külön megnevezett függvényeket megadni minden illesztési és alkalmazási szabályhoz. Ezeket soha nem hívod meg közvetlenül, ugyanis ezeket hozzáadod a rules sorozathoz, és azon keresztül hívod. Továbbá minden függvény egy vagy két mintát követ. Minden illesztési függvény a re.search() metódust hívja, és minden alkalmazási függvény a re.sub() metódust. Szervezzük ki a mintákat, hogy az új szabályok megadása egyszerűbb lehessen.

[a plural3.py letöltése]

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)                           
  1. A build_match_and_apply_functions() egy más függvényeket dinamikusan felépítő függvény. A pattern, search és replace változókat várja, majd definiál egy matches_rule() függvényt, amely meghívja a re.search() metódust a build_match_and_apply_functions() függvénynek átadott pattern változóval, és a felépítendő matches_rule() függvénynek átadott word változóval. Bámulatos, hol tart már a tudomány.
  2. Az alkalmaz függvény ugyanígy működik. Az alkalmaz függvény egy paramétert vár, és meghívja a re.sub() metódust a build_match_and_apply_functions() függvénynek átadott search és replace változókkal, és a felépítendő apply_rule() függvénynek átadott word változóval. Ezt a módszert, a külső paraméterek értékeinek dinamikus függvényeken belüli felhasználását closure-öknek nevezzük. Lényegében konstansokat definiálsz a felépítendő alkalmaz függvényen belül: egy paramétert vár (word), de aztán ezen és még két másik értéken (search és replace) dolgozik, amelyek az alkalmaz függvény definiálásakor lettek megadva.
  3. Végül a build_match_and_apply_functions() függvény egy két értékből álló tuple-t ad vissza: az éppen létrehozott két függvényt. A függvényeken belül definiált konstansok (pattern a matches_rule() függvényen belül, illetve a search és replace az apply_rule() függvényen belül) megmaradnak a függvényekkel, még akkor is, amikor visszatér a build_match_and_apply_functions() függvény. Ez őrülten menő!

Ha ez hihetetlenül zavarosnak is tűnik (ahogy annak lennie is kell, ezek fura dolgok), világosabbá válhat, ha megmutatom hogyan használd.

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]
  1. A többes számot előállító „szabályaink” most karakterláncok (nem függvények) tuple-jének tuple-jeként vannak definiálva. Az első karakterlánc minden csoportban az a reguláris kifejezés minta, amelyet a re.search() metódusban használsz a szabály illeszkedésének meghatározásához. A csoportok második és harmadik karakterláncai a keresés és csere kifejezések, amelyeket a re.sub() metódusban használsz a szabály tényleges alkalmazására a főnév többes számúvá tételéhez.
  2. Itt, a tartalék szabályban egy kis változás van. Az előző példában a match_default() függvény egyszerűen a True értéket adta vissza, amely azt jelentette, hogy a megadott szabályok egyike sem illeszkedett, a kód pedig egy s karaktert illesztett az adott szó végéhez. Ez a példa valami funkcionálisan egyenértékűt tesz. Az utolsó reguláris kifejezés azt ellenőrzi, hogy a szónak vége van-e(a $ a karakterlánc végére illeszkedik). Természetesen minden karakterláncnak van vége, még az üres karakterláncnak is, így ez a kifejezés mindig illeszkedik. Így ugyanazt a célt szolgálja, mint a match_default() függvény, amely mindig a True értéket adta vissza: biztosítja, hogy ha több szabály nem illeszkedik, akkor a kód egy s karaktert fűzzön az adott szó végéhez.
  3. Ez a sor a varázslat. Veszi a karakterláncok sorozatát a patterns tuple-ből, és ezeket függvények sorozatává alakítja. Hogyan? A karakterláncok „leképezésével” a build_match_and_apply_functions() függvényre. Vagyis veszi a karakterlánc-hármasokat, és meghívja a build_match_and_apply_functions() függvényt, argumentumként használva ezt a három karakterláncot. A build_match_and_apply_functions() függvény egy két függvényből álló tuple-t ad vissza. Ez azt jelenti, hogy a rules lista funkcionálisan egyenértékű lesz az előző példával: tuple-ök listája, amelyben minden tuple egy függvénypár. Az első függvény az illesztési függvény, amely meghívja a re.search() metódust, a második függvény pedig az alkalmazási függvény, amely a re.sub() metódust hívja meg.

A parancsfájl ezen változatát a fő belépési ponttal, a plural() függvénnyel zárjuk.

def plural(noun):
    for matches_rule, apply_rule in rules:  
        if matches_rule(noun):
            return apply_rule(noun)
  1. Mivel a rules lista ugyanaz, mint az előző példában (tényleg az), nem érhet meglepetésként, hogy a plural() függvény egyáltalán nem változott. Teljesen általános, a szabályfüggvények listáját várja, és sorban meghívja azokat. A szabályok definiálásának módjával nem foglalkozik. Az előző példában ezek önálló nevesített függvényekként voltak definiálva. Most dinamikusan állnak össze, a build_match_and_apply_functions() függvény kimenetének leképezésével nyers karakterláncok listájára. Nem számít, a plural() függvény ugyanúgy működik.

Minták fájlja

A többször szereplő kódot felszámoltad, és elég absztrakciót adtál hozzá, így a többes számot előállító szabályok karakterláncok listájában vannak meghatározva. A következő logikus lépés ezen karakterláncok önálló fájlba helyezése, ahol az ezeket használó kódtól függetlenül tarthatók karban.

Elsőként hozzunk létre egy szövegfájlt, amely tartalmazza a kívánt szabályokat. Nem kellenek csicsás adatszerkezetek, csak üres helyekkel elválasztott karakterláncok három oszlopban. Nevezzük plural4-rules.txt-nek.

[a plural4-rules.txt letöltése]

[sxz]$               $    es
[^aeioudgkprt]h$     $    es
[^aeiou]y$          y$    ies
$                    $    s

Nézzük, hogyan használhatjuk ezt a szabályfájlt.

[a plural4.py letöltése]

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))
  1. A build_match_and_apply_functions() függvény nem változott. Még mindig closure-öket használsz két függvény dinamikus felépítésére, amelyek a külső függvényben definiált változókat használnak.
  2. A globális open() függvény megnyit egy fájlt, és visszaad egy fájl objektumot. Ebben az esetben a megnyitott fájl minta karakterláncokat tartalmaz, a főnevek többes számba tételéhez. A with utasítás egy kontextusnak nevezett dolgot hoz létre: amikor a with blokk befejeződik, a Python automatikusan lezárja a fájlt, még ha kivétel is történt a with blokkon belül. A with blokkokról és a fájlobjektumokról a Fájlok fejezetben fogsz többet megtudni.
  3. A for line in <fileobject> szerkezet soronként beolvassa az adatokat a megnyitott fájlból, és a szöveget a line változóhoz rendeli. A fájlokból olvasásról a Fájlok fejezetben fogsz többet megtudni.
  4. A fájl minden sora valójában három értéket tartalmaz, ezeket üres hely választja el (tabok vagy szóközök, nem számít). A szétválasztásukhoz használd a split() karakterláncmetódust. A split() metódus első argumentuma a None, amely azt jelenti, hogy „vágd szét az üres helynél (tab vagy szóköz, nem számít).” A második argumentum a 3, ami azt jelenti, hogy „vágd szét az üres helynél háromszor, és hagyd békén a sor többi részét.” Egy ilyen sor: [sxz]$ $ es a következő listává lesz szétvágva: ['[sxz]$', '$', 'es'], amely azt jelenti, hogy a pattern értéke '[sxz]$', a search értéke '$', a replace értéke pedig 'es' lesz. Rengeteg erő van ebben a pici kódsorban!
  5. Végül átadod a pattern, search és replace változókat a build_match_and_apply_functions() függvénynek, amely függvények tuple-jét adja vissza. Ezt a tuple-t a rules listához fűzöd, a rules pedig az illesztés és alkalmazás függvények listáját fogja tárolni, ahogy azokat a plural() függvény várja.

A fejlődés itt abban áll, hogy teljesen elkülönítetted a többes számot előállító szabályokat egy külső fájlba, így az azt használó kódtól külön lehet karbantartani. A kód kód, az adat adat, az élet szép.

Generátorok

Nem lenne nagyszerű egy olyan általános plural() függvény, amely a szabályok fájlját dolgozza fel? Fogd a szabályokat, keress illeszkedést, alkalmazd a megfelelő átalakítást, menj a következő szabályra. Ez minden, amit a plural() függvénynek muszáj megoldania, és ez minden, amit a plural() függvénynek el kell végeznie.

[a plural5.py letöltése]

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

Hogy a csudába működik ez? Nézzünk meg először egy interaktív példát.

>>> 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
  1. A yield kulcsszó jelenléte a make_counter függvényben azt jelenti, hogy ez nem egy átlagos függvény. Ez egy különleges függvény, amely időről időre értékeket állít elő. Úgy képzelheted el, mint egy folytatható függvényt. A meghívása egy generátort ad vissza, amely az x egymást követő értékeinek előállítására használható.
  2. A make_counter generátor egy példányának előállításához csak hívd meg, mint bármely függvényt. Ne feledd, hogy ez nem futtatja le ténylegesen a függvény kódját. Ezt abból láthatod, hogy a make_counter() függvény első sora meghívja a print() függvényt, de még semmi sem került kiírásra.
  3. A make_counter() függvény egy generátor objektumot ad vissza.
  4. A next() függvény egy generátor objektumot vár, és visszaadja a következő értékét. A next() a counter generátorral való első hívásakor végrehajtja a make_counter() kódját az első yield utasításig, majd visszaadja az eredményül kapott értéket. Ebben az esetben ez a 2 lesz, mert a generátort eredetileg a make_counter(2) hívásával hoztad létre.
  5. A next() ismételt hívása ugyanazzal a generátor objektummal pontosan ott folytatja a végrehajtást, ahol abbahagyta, és addig folytatja, amíg nem találja meg a következő yield utasítást. Minden változó, helyi állapot stb. mentésre kerül a yield hívásakor, és visszaállításra kerül a next() hívásakor. A végrehajtásra váró következő kódsor a print() függvényt hívja, amely kiírja az incrementing x kifejezést. Ezután végrehajtja az x = x + 1 utasítást. Majd végighalad újra a while cikluson, és az első dolog, amivel találkozik, az a yield x utasítás, amely elmenti az összes állapotot, és visszaadja az x aktuális értékét (most 3).
  6. A next(counter) újabb hívásakor minden ugyanúgy történik, de most az x értéke 4.

Mivel a make_counter egy végtelen ciklust tartalmaz, elméletileg ezt a végtelenségig csinálhatnád, folyamatosan növelné az x értékét és írná ki azt. De nézzük meg inkább a generátorok értelmesebb felhasználási módjait.

Egy Fibonacci generátor

[a fibonacci.py letöltése]

def fib(max):
    a, b = 0, 1          
    while a < max:
        yield a          
        a, b = b, a + b  
  1. A Fibonacci sorozat egy olyan számsorozat, amelyben minden szám az előtte lévő kettő összege. A 0-val és 1-gyel kezdődik, először lassan, majd egyre gyorsabban növekszik. A sorozat elindításához két változóra van szükséged: az a 0-nál kezdődik, a b pedig 1-nél.
  2. Az a a sorozat aktuális értéke, így a yield-del visszaadjuk.
  3. A b a sorozat következő száma, így azt hozzárendeljük az a-hoz, de kiszámoljuk a következő értéket is(a + b) és hozzárendeljük a b-hez későbbi felhasználásra. Ne feledd, hogy ez párhuzamosan történik: ha a = 3 és b = 5, akkor az a, b = b, a + b beállítja az a-t 5-re (a b előző értékére) és a b-t 8-ra (az a és b előző értékeinek összegére).

Így van egy függvényed, amely egymást követő Fibonacci számokat ír ki. Persze, lehetne rekurziót is használni, de így olvashatóbb. Továbbá remekül működik for ciklusokkal.

>>> 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]
  1. A fib()-hez hasonló generátort közvetlenül használhatod egy for ciklusban. A for ciklus automatikusan meghívja a next() függvényt az értékek kinyeréséhez a fib() generátorból, és hozzárendeli azokat a for ciklus indexváltozójához (n).
  2. A for ciklus minden végrehajtásakor az n új értéket kap a yield utasítástól a fib() generátorban, csak ki kell íratnod. Miután a fib() kifogy a számokból (az a nagyobbá válik, mint a max, amely ebben az esetben 1000), akkor a for ciklus kilép.
  3. Ez egy hasznos sablon: adj át egy generátort a list() függvénynek, és az végigjárja az egész generátort (mint a for ciklus az előző példában), és visszaadja az összes érték listáját.

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

Térjünk vissza a plural5.py programhoz, és nézzük meg, hogyan működik a plural() függvény ezen változata.

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))
  1. Itt nincs varázslat. Emlékezz, hogy a szabályfájl sorai három, üres helyekkel elválasztott értéket tartalmaznak, így a line.split(None, 3) használatával kaphatod meg a három „oszlopot”, majd három helyi változóhoz rendeled azokat.
  2. Ekkor jön a yield. Mit ad vissza a yield? Két függvényt, amelyeket a korábbi példákban szereplő régi barátunk, a build_match_and_apply_functions() épít fel dinamikusan. Más szavakkal a rules() egy generátor, amely illesztési és alkalmazási függvényeket ad vissza igény szerint.
  3. Mivel a rules() egy generátor, közvetlenül használhatod egy for ciklusban. A for ciklus első futásakor meghívod a rules() függvényt, amely megnyitja a mintafájlt, beolvassa az első sort, dinamikusan felépít egy illesztési és egy alkalmazási függvényt a sorban lévő mintákból, és a yield használatával visszaadja a dinamikusan felépített függvényeket. A for ciklus második futásakor pontosan ott folytatod, ahol a rules() függvényt abbahagytad (ami a for line in pattern_file ciklus közepén volt). Ez elsőként a (még mindig nyitva lévő) fájl következő sorának beolvasását fogja elvégezni, majd dinamikusan felépít egy újabb illesztési és alkalmazási függvényt a fájl adott sorának mintái alapján, és a yield használatával visszaadja a két függvényt.

Mit nyertél a 4. lépéshez képest? Indítási időt. Amikor a 4. lépésben importáltad a plural4 modult, az beolvasta a teljes mintafájlt, és felépítette az összes lehetséges szabály listáját, mielőtt akár csak gondolhattál volna a plural() függvény hívására. A generátorokkal mindent lustán csinálhatsz: beolvashatod az első szabályt, létrehozhatod a függvényeket és kipróbálhatod azokat, és ha ez sikeres, akkor nem is olvasod be a fájl többi részét, és újabb függvényeket sem hozol létre.

Mit vesztettél? Teljesítményt! A plural() függvény minden hívásakor a rules() generátor újraindul az elejétől – azaz újra megnyitja a mintafájlt, és soronként beolvassa az elejétől.

Mi lenne, ha egyszerre tudnád kihasználni a két módszer előnyeit: minimális indítási költség (ne hajtson végre kódot az import hívásakor), és maximális teljesítmény (ne építse fel ugyanazokat a függvényeket újra és újra). Ja, és továbbra is önálló fájlban szeretnéd tartani a szabályokat (mert a kód az kód, az adat az adat), hogy soha ne kelljen kétszer beolvasnod ugyanazt a sort.

Hogy ezt megtehesd, saját iterátort kell felépítened. De hogy ezt megtehesd, meg kell ismerkedned a Python osztályokkal.

További olvasnivaló

© 2001–11 Mark Pilgrim