2.31. Iterátorok és generátorok

A for ciklus több munkát végzett, mint amennyinek látszik. Ez az oldal az iterátor protokollt tárgyalja, amelyen fut, valamint a yield kulcsszót, amely lehetővé teszi, hogy saját iterátorokat építs.

2.31.1. Az iterátor protokoll

Minden objektum, amelyen végig lehet iterálni, két metódust valósít meg:

  • __iter__() – visszaad egy iterátort az objektum elemei felett.

  • __next__() – az iterátoron visszaadja a következő elemet, vagy StopIteration kivételt vált ki, ha nincs több.

Az iter() beépített függvény az __iter__ metódust hívja; a next() az __next__ metódust hívja. Lépkedj végig egy listán kézzel:

it = iter([10, 20, 30])
print(next(it))    # 10
print(next(it))    # 20
print(next(it))    # 30
print(next(it))    # raises StopIteration
A for loop calls __iter__ once to get an iterator, then calls __next__ repeatedly until StopIteration ends the loop.

A for voltaképpen a következő rövidített formája: „hívd meg egyszer az __iter__ metódust, majd iterálj az __next__ metóduson, amíg StopIteration nem jön.”

Mit csinál valójában a for x in items::

_it = iter(items)
while True:
    try:
        x = next(_it)
    except StopIteration:
        break
    # ... loop body ...

Minden lista, tuple, string, dict, set, fájlobjektum és generátor már megvalósítja az __iter__ és __next__ metódusokat – ezért működnek mind a for ciklussal.

2.31.2. A yield és a generátor függvények

Egy függvény, amely tartalmaz egy yield utasítást, egy generátor függvény. A meghívása nem futtatja le a törzset; egy generátor objektumot (egy iterátort) ad vissza, amely a törzset egyszerre egy yield erejéig futtatja:

def count_up_to(n):
    i = 0
    while i < n:
        yield i
        i += 1

for value in count_up_to(3):
    print(value)

Kimenet:

0
1
2

A next() minden hívása a következő yield pontig folytatja a függvényt, átadja azt az értéket a hívónak, és ott megáll. A lokális állapot (ebben az esetben az i) megmarad a folytatások között.

Sequence diagram with two lifelines (caller and generator body). The caller calls count_up_to(3), which creates a generator without running the body. Each subsequent next() runs the body until the next yield, returns the yielded value, and pauses. The fourth next() falls off the end and raises StopIteration. The variable i is preserved across pauses.

A next() a következő yield pontig futtatja a törzset, visszaadja az értéket, és megáll. A lokális állapot túléli a megállást.

A generátorok a legegyszerűbb módja egy szekvencia lusta előállításának – nem épül lista, az elemek csak akkor kerülnek kiszámításra, amikor a fogyasztó kéri őket, és a függvény akár örökké is adhat elemeket, ha akar.

2.31.3. Lusta folyamatok (pipeline)

A generátorok jól kombinálhatók. Az egyik generátor kimenete betáplálható egy másikba:

def numbers():
    i = 0
    while True:
        yield i
        i += 1

def squares(source):
    for x in source:
        yield x * x

pipeline = squares(numbers())

for v in pipeline:
    if v > 100:
        break
    print(v)

Az értékek egyesével áramlanak végig a folyamaton – nincs köztes lista, nincs felső korlát beépítve a numbers függvénybe, és a fogyasztó (for v in pipeline) dönti el, mikor álljon le.

Three boxes left-to-right: numbers(), squares(source), and the for-v-in-pipeline consumer. Three cycles are drawn beneath. In each cycle, the consumer sends a pull request leftward to squares, which sends a pull leftward to numbers; numbers yields a value rightward to squares, which yields its squared value rightward to the consumer.

A fogyasztón végrehajtott minden next() egy lekérést indít végig a láncon; az értékek csak akkor léteznek, amikor valami kéri őket.

2.31.3.1. yield from

Egy ciklus, amely elemeket húz egy másik iterálható objektumból, és mindegyiket átadja, elég gyakori ahhoz, hogy a Python rövidítést biztosítson rá. A yield from iter kifejezés sorrendben átadja az iterálható objektum által előállított minden értéket – mintha a generátornak egy for x in iter: yield x ciklusa lenne beágyazva:

def chain(*sources):
    for source in sources:
        yield from source

for v in chain([1, 2, 3], (4, 5), "abc"):
    print(v)

Kimenet:

1
2
3
4
5
a
b
c

A yield from pontosan egyenértékű az explicit for ciklussal, csak rövidebb, és tisztán továbbítja a StopIteration kivételt a belső iterálható objektumtól a külső generátorig – hasznos, amikor több generátort fűzöl össze egymás után.

2.31.3.2. Amikor a yield kifogy

Egy generátor függvény végéről való leesés (vagy egy explicit return elérése) automatikusan StopIteration kivételt vált ki. Nincs szükség kézzel kiváltani; a körülvevő for ciklus észleli, és véget ér.

Használj generátorokat, ha az előállító kód természetes módon ciklusként van megírva néhány yield ponttal; használj egyszerű listakifejezést (list comprehension), ha valóban szükséged van az egész szekvenciára a memóriában.