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ż:
Zajrzyj do Informacji na temat tej publikacji... aby pomóc w jej rozwoju.