Generatory

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/

WPROWADZENIE

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