https://docs.python.org/3/reference/expressions.html#yieldexpr
PEP-255 (Simple Generators) [yield is a statement; lazy evaluation]
PEP-342 (Coroutines via Enhanced Generators) [yield is an expression; send(), throw(), close()]
PEP-380 (Syntax for delegating to a Sub-Generator)
PEP-525 (Asynchronous Generators)
http://code.activestate.com/recipes/576727-pure-python-implementation-of-pep-380-yield-from/
Wyrażenia yield (yield expressions) pojawiły się w Pythonie 2.5 do definiowania funkcji generatora (generator function) zamiast zwykłej funkcji. Wyrażenia yield mogą się pojawić jedynie w ciele definicji funkcji.
Z generatorów korzystamy zwykle wtedy, gdy nie potrzebujemy pamiętać pełnej listy, a lista jest tylko pewnym krokiem pośrednim w obliczeniach. Generatory to "leniwe funkcje": obliczają wartości tylko wtedy, gdy są żądane. Generatory są iteratorami, bo obsługują metodę next() [Py2] lub __next__() [Py3] i funkcję wbudowaną next() [Py2.7, Py3]. Więcej o iteratorach powiemy po omówieniu klas i wyjątków (lekcja 7).
Każde wyrażenie yield tymczasowo zatrzymuje przetwarzanie, zapamiętuje stan funkcji. Po wznowieniu generatora (ponownym wywołaniu) przetwarzanie jest kontunuowane od miejsca zatrzymania.
Generatory są iteratorami, a więc można po nich przejść tylko raz. Wartości generatora nie są przechowywane w pamięci, tylko są wytwarzane w locie (on the fly).
# Przykładowa lista składana. x = [i*i for i in range(100)] # lista w całości # Wyrażenie generatora (generator expression) to wyrażenie, które zwraca iterator. y = (i*i for i in range(100)) # generator object # help(y) # Help on generator object ... # type(y) # <class 'generator'> # Tworzenie listy liczb w różnych wersjach Pythona. L = range(100) # Py2 L = list(xrange(100)) # Py2, xrange to generator L = list(range(100)) # UNIWERSALNE
# Funkcja generatora (generator function) to funkcja, która zwraca iterator.
def my_generator(stop):
"""Generator zastępujący xrange(stop)."""
value = 0
while value < stop:
yield value # yield zamiast return
value = value + 1
# help(my_generator) # Help on function my_generator ...
# type(my_generator) # <class 'function'>
# help(my_generator(5)) # Help on generator object ...
# type(my_generator(5)) # <class 'generator'>
# Sposób 1 korzystania z generatora.
# Mamy ukryty w tle protokół iteracji.
for i in my_generator(10):
print(i)
# Po zakończeniu pętli tracimy dostęp do generatora,
# ale i tak jest on już wyczerpany.
# Sposób 2 korzystania z generatora.
# Jawne korzystanie z protokołu iteracji.
x = my_generator(3)
print(next(x)) # 0
print(next(x)) # 1
print(next(x)) # 2
print(next(x)) # StopIteration
# W następnych wywołaniach też otrzymamy wyjątek StopIteration.
# Generator może być skończony lub nieskończony.
def fibonacci():
"""Nieskończony generator liczb Fibonacciego."""
minus1, minus0 = 0, 1
yield minus1
yield minus0
while True:
minus1, minus0 = minus0, minus1 + minus0
yield minus0
for i in fibonacci():
print(i)
if i > 100:
break
# 0 1 1 2 3 5 8 13 21 34 55 89 144
# Generator jest nieskończony, więc należało samemu
# przerwać pętlę przez break. Ten generator mógłby dalej
# produkować liczby Fibonacciego, gdyby mieć do niego dostęp.
fib = fibonacci() # fib to generator object
for i in fib:
print(i)
if i > 100:
break
print(next(fib)) # 233
print(next(fib)) # 377
# Prędkość vs pamięć.
import sys
import timeit
N = pow(10, 7)
sqr_list = [i*i for i in range(N)]
print(sys.getsizeof(sqr_list)) # 81528056
sqr_gen = (i*i for i in range(N))
print(sys.getsizeof(sqr_gen)) # 120
# Obiekty budowane na poczekaniu.
t1 = timeit.Timer(lambda: sum([i*i for i in range(N)]))
t2 = timeit.Timer(lambda: sum(i*i for i in range(N)))
# Obiekty przygotowane wcześniej.
t3 = timeit.Timer(lambda: sum(sqr_list))
t4 = timeit.Timer(lambda: sum(sqr_gen))
print("sum list {}".format(t1.timeit(1))) # sum list 0.5472016650019214
print("sum gen {}".format(t2.timeit(1))) # sum gen 0.5155281510014902
print("sum list2 {}".format(t3.timeit(1))) # sum list2 0.13735308399918722
print("sum gen2 {}".format(t4.timeit(1))) # sum gen2 0.5134274550000555
# Przy generatorach jest problem, jeżeli chcemy wykonać podwójną
# iterację po obiekcie generatora.
x = (i*i for i in range(10)) # generator object
for i in x:
for j in x:
print("{} {}".format(i, j))
# WYNIK
# 0 1
# 0 4
# 0 9
# 0 16
# 0 25
# 0 36
# 0 49
# 0 64
# 0 81
# Druga pętla for wyczerpała generator x.
# Próbujemy z dwoma generatorami
x = (i*i for i in range(10)) # generator object
y = (i*i for i in range(10)) # generator object
for i in x:
for j in y:
print("{} {}".format(i, j))
# WYNIK
0 0
0 1
0 4
0 9
0 16
0 25
0 36
0 49
0 64
0 81
# Generator y wyczerpał się po jednym przebiegu (x=0).
# Rozwiązaniem są dwa niezależne generatory, które powstają w pętli.
for i in (i*i for i in range(10)):
for j in (i*i for i in range(10)):
print("{} {}".format(i, j))
# WYNIK
0 0
0 1
0 4
0 9
0 16
0 25
0 36
0 49
0 64
0 81
1 0
1 1
1 4
1 9
1 16
1 25
1 36
1 49
1 64
1 81
4 0
4 1
4 4
4 9
4 16
4 25
4 36
4 49
4 64
4 81
9 0
9 1
9 4
9 9
9 16
9 25
9 36
9 49
9 64
9 81
16 0
16 1
16 4
16 9
16 16
16 25
16 36
16 49
16 64
16 81
25 0
25 1
25 4
25 9
25 16
25 25
25 36
25 49
25 64
25 81
36 0
36 1
36 4
36 9
36 16
36 25
36 36
36 49
36 64
36 81
49 0
49 1
49 4
49 9
49 16
49 25
49 36
49 49
49 64
49 81
64 0
64 1
64 4
64 9
64 16
64 25
64 36
64 49
64 64
64 81
81 0
81 1
81 4
81 9
81 16
81 25
81 36
81 49
81 64
81 81
Listy składane są żarłoczne (greedy), obliczają wynik od razu jako listę. Generatory są leniwe (lazy), obliczają jedną wartość na raz, kiedy jest potrzebna. Warto zapamiętać regułę:
W Py3.3 pojawiło się wyrażenie (generator delegation) postaci yield from sequence. Zwykle odpowiada to w Py2 wyrażeniu:
for item in sequence:
yield item