3 PEP 255: Proste generatory

W Pythonie 2.2 generatory pojawiły się jako cecha opcjonalna, dostępna po użyciu instrukcji from __future__ import generators. W wersji 2.3 generatory są już dostępne domyślnie, bez konieczności ich uaktywniania. Oznacza to również, że yield jest zawsze słowem kluczowym. Pozostała część tej sekcji jest kopią opisu generatorów z dokumentu "Co nowego w Pythonie 2.2". Jeśli więc znasz jego treść z poprzedniej wersji, możesz dalszą część pominąć.

Z pewnością znany ci jest sposób wywoływania funkcji w językach takich jak Python czy C. W chwili wywołania funkcji otrzymuje ona prywatną przestrzeń nazw, w której tworzone są zmienne lokalne. Po osiągnięciu instrukcji return wewnątrz funkcji wszystkie zmienne lokalne są niszczone, a obliczona wartość jest przekazywana do punktu wywołania. Późniejsze wywołanie tej samej funkcji spowoduje użycie "czystego" nowego zbioru zmiennych lokalnych. Co jednak stałoby się, gdyby zmienne lokalne nie były niszczone przy opuszczaniu funkcji? Co, gdyby możliwe było późniejsze wznowienie funkcji w miejscu, w którym ją opuściliśmy? To właśnie oferują generatory -- można je traktorać jak funkcje, których działanie można wstrzymywać i wznawiać.

Oto najprostszy przykład funkcji generującej:

def wygeneruj_calkowite(N):
    for i in range(N):
        yield i

Na potrzeby generatorów zostało wprowadzone nowe słowo kluczowe yield. Każda funkcja, która zawiera instrukcję yield staje się automatycznie funkcją generującą. Jest to rozpoznawane przez kompilator, który generuje w tym przypadku specjalny kod dla funkcji. Przy wywołaniu funkcji generującej nie jest zwracana pojedyncza wartość. Zamiast tego zwracany jest obiekt generatora, który obsługuje protokół iteratora. Przy wykonaniu instrukcji yield generator daje na wyjściu wartość i, podobnie, jak ma to miejsce przy instrukcji return. Znacząca różnica pomiędzy tymi instrukcjami polega jednak na tym, że w przypadku instrukcji yield zostaje zapamiętany stan wykonywania generatora oraz wartości wszystkich zmiennych lokalnych. Przy kolejnym wywołaniu metody .next() generatora wykonywanie funkcji jest wznawiane bezpośrednio po ostatnio napotkanej instrukcji yield. (Z pewnych skomplikowanych przyczyn użycie instrukcji yield nie jest dozwolone wewnątrz bloku try instrukcji try...finally. Pełne wyjaśnienie zależności pomiędzy instrukcją yield i wyjątkami znajduje się w dokumencie PEP 255.)

Oto przykład użycia generatora wygeneruj_calkowite():

>>> gen = wygeneruj_calkowite(3)
>>> gen
<generator object at 0x8117f90>
>>> gen.next()
0
>>> gen.next()
1
>>> gen.next()
2
>>> gen.next()
Traceback (most recent call last):
  File "stdin", line 1, in ?
  File "stdin", line 2, in wygeneruj_calkowite
StopIteration

Podobny efekt można osiągnąć używając zapisu for i in generate_ints(5) lub też a,b,c = generate_ints(3).

Wewnątrz funkcji generującej dopuszczalne jest używanie instrukcji return wyłącznie w postaci bez wartości i sygnalizuje ona w takim przypadku zakończenie generowania wartości, po którym generator nie może już zwrócić kolejnych. Użycie instrukcji return z wartością, np. return 5, jest wewnątrz funkcji generującej traktowane jak błąd składniowy. Zakończenie generowania wartości przez generator można również zasygnalizować poprzez ręczne wygenerowanie wyjątku StopIteration, ten sam efekt zostanie osiągnięty, gdy sterowanie osiągnie koniec bloku funkcji.

Możliwe byłoby ręczne osiągnięcie efektu podobnego do tego, jaki dają generatory, poprzez utworzenie własnej klasy i zapisanie wszystkich zmiennych lokalnych generatora jako zmiennych instancji. Na przykład wygenerowanie listy liczb całkowitych można byłoby osiągnąć poprzez ustawienie self.count na 0 oraz zwiększanie, a następnie zwracanie wartości self.count w metodzie next(). Jednak już przy średnio złożonych generatorach napisanie odpowiedniej klasy byłoby nie lada zadaniem i mogłoby prowadzić do bałaganu. Plik Lib/test/test_generators.py zawiera szereg bardziej interesujących przykładów. Najprostszy z nich jest implementacją przechodzenia po drzewie w kolejności "in-order" poprzez rekurencyjne użycie generatorów.

# Rekurencyjny generator, który generuje liście drzewa w
# kolejności określanej mianem "in-order".
def inorder(t):
    if t:
        for x in inorder(t.left):
            yield x
        yield t.label
        for x in inorder(t.right):
            yield x

Dwa inne przykłady z pliku Lib/test/test_generators.py tworzą rozwiązania problemu N hetmanów (umieszczenie hetmanów na szachownicy o rozmiarach pól, tak, aby żaden z hetmanów nie był bity przez innego) oraz wędrówki skoczka (wyznaczenie drogi skoczka na szachownicy o rozmiarach pól w taki sposób, aby każde pole odwiedził dokładnie raz).

Pomysł generatorów pochodzi z innych języków programowania, w szczególności z języka Icon (http://www.cs.arizona.edu/icon/), w którym stanowią one centralny mechanizm języka. W języku Icon każde wyrażenie i wywołanie funkcji zachowuje się jak generator. Demonstracją sposobu działania tego mechanizmu w języku Icon jest przykład z dokumentu "Przegląd języka programowania Icon", dostępnego pod adresem http://www.cs.arizona.edu/icon/docs/ipd266.htm:

zdanie := "Store it in the neighboring harbor"
if (i := find("or", zdanie)) > 5 then write(i)

W języku Icon funkcja find() zwraca indeksy, pod którymi rozpoczyna się wyszukiwany napis (w naszym wypadku "or"). Uzyskamy więc wartości: 3, 23, 33. W instrukcji if następuje najpierw przypisanie do i wartości 3, jednak nie spełnia ona podanego warunku, więc w następnej kolejności przypisywana jest wartość 23. Tym razem i jest już większe od 5, więc warunek jest spełniony i zostaje wypisana wartość 23.

W Pythonie nie posunięto się tak daleko i generatory nie stanowią centralnego elementu języka. Są one częścią rdzenia języka, jednak poznawanie ich nie jest obowiązkowe -- jeśli generatory nie rozwiązują żadnego z twoich problemów, to możesz je swobodnie zignorować. Nowatorską cechą interfejsu generatorów w Pythonie w porównaniu z Iconem jest to, że stan generatora jest reprezentowany przez konkretny obiekt (iterator), który można dowolnie przekazywać do innych funkcji lub zapisywać w strukturach danych.

Zobacz też:

PEP 255, Proste generatory
Autorzy dokumentu PEP: Neil Schemenauer, Tim Peters, Magnus Lie Hetland. Implementacja: głównie Neil Schemenauer i Tim Peters, inne poprawki: Python Labs crew.

Zajrzyj do Informacji na temat tej publikacji... aby pomóc w jej rozwoju.