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) 'yield from sequence'. Zwykle odpowiada to w Py2 wyrażeniu:
for item in sequence: yield item