Dany jest generator generator = (x for x in [16,285,386,412,594,625,718,882,91,106,110,12]) zwracający 12 kolejnych liczb będących liczbą sprzedanych butów sportowych w kolejnych miesiącach roku. Napisz funkcję, która zwróci listę zawierającą jaki procent butów został sprzedany w każdym miesiącu.

Zła odpowiedź

Jak się pewnie spodziewaliście w tym pytaniu ukryta jest pułapka. Ma ona na celu sprawdzić znajomość natury generatorów. Zastanówmy się chwilę co trzeba zrobić by policzyć procent sprzedanych butów w danym miesiącu. Po pierwsze potrzebujemy policzyć sumę wszystkich sprzedanych butów w cały roku. Potem na przykład za pomocą  listy składanej wyliczyć procent sprzedaży w każdym miesiącu. Dlatego właśnie częstą odpowiedzią na to zadanie jest taka funkcja:

generator = (x for x in [16,28,386,412,1594,625,718,882,91,106,110,12])

def zle_rozwiazanie(generator):
    suma = sum(generator)
    return [100*x/suma for x in generator]

print(zle_rozwiazanie(generator))

Uruchom w edytorze

Output:

[]

Tak efektem działania tej funkcji jest pusta lista. Wiesz już dlaczego? Zaraz wszytko będzie jasne.

Czemu to nie działa ?

Generator służy do generowania danych. Za każdym jego wywołaniem, a dokładniej za każdym razem gdy wywołamy na nim metodę next zwraca nam kolejną wartość. Czyli nasz generator będzie zwracać kolejne liczby będące ilością sprzedanych butów. Będzie to robić, aż do zakończenia się danych co zostanie zasygnalizowane wystąpieniem wyjątku StopIteration. Oto przykład wywołania next na naszym generatorze 13 razy czyli o raz więcej niż mamy dostępnych danych.

generator = (x for x in [16,28,386,412,1594,625,718,882,91,106,110,12])

for x in range(13):
    print(next(generator))

Uruchom w edytorze

Output:

16
28
386
412
1594
625
718
882
91
106
110
12
Traceback (most recent call last):
  File "main.py", line 4, in <module>
    print(next(generator))
StopIteration

Jak widać 13 wywołanie spowodowało pojawienie się wyjątku. Wyjątek ten jest bardzo ważny i jest formą komunikacji podczas iterowania się przez generator. Gdy korzystamy z konstrukcji:

for x in generator:
    print(x)

Nasza pętla będzie wypisywać zawartość generatora aż do napotkania wyjątku StopIteration. Dodatkowo pamiętaj, że generator pamięta stan. Problem naszego błędnego rozwiązania polega na tym, że wbudowana funkcja sum(generator)przeiterowała się po generatorze by zsumować wszystkie jego elementy. Dotarła do końca danych i po złapaniu wyjątku StopIteration zakończyła swoje działanie. Każda kolejna próba iterowania się po generatorze spowoduje natychmiastowe pojawianie się wyjątku StopIterationi zakończenie iterowania. Dlatego próba kolejnej iteracji podczas tworzenia listy składanej return [100*x/suma for x in generator] zwraca pustą listę.

Jak to naprawić ?

Najprostszym sposobem naprawy naszego kodu jest stworzenie ”tupli” (ang. tuple) na podstawie generatora.

Funkcja wyglądałaby wtedy następująco:

generator = (x for x in [16,28,386,412,1594,625,718,882,91,106,110,12])

def to_dziala(generator):
    nasze_dane = tuple(generator)
    suma = sum(nasze_dane)
    return [100*x/suma for x in nasze_dane]

print(to_dziala(generator))

Uruchom w edytorze

Output:

[0.321285140562249, 0.5622489959839357, 7.751004016064257, 8.273092369477911, 32.00803212851405, 12.550200803212851, 14.417670682730924, 17.710843373493976, 1.8273092369477912, 2.1285140562248994, 2.208835341365462, 0.24096385542168675]

Warto jednak dodać, że niejako zużyliśmy nasz generator na stworzenie listy. Pozwoliło nam to na wywołanie zarówno funkcji sum jak i stworzenie listy składanej.  Niestety nasz generator jest już bezużyteczny. Podobnym sposobem rozwiązania problemu jest skorzystanie z ”tupli” i wypakowanie naszego generatora:

generator = (x for x in [16,28,386,412,1594,625,718,882,91,106,110,12])

def rozwiazanie(*moja_tupla):
    suma = sum(moja_tupla)
    return [100*x/suma for x in moja_tupla]

print(rozwiazanie(*generator))

Uruchom w edytorze

Output:

[0.321285140562249, 0.5622489959839357, 7.751004016064257, 8.273092369477911, 32.00803212851405, 12.550200803212851, 14.417670682730924, 17.710843373493976, 1.8273092369477912, 2.1285140562248994, 2.208835341365462, 0.24096385542168675]

Generator a generator składany

Cały ten problem spowodowany jest tym, że nie mamy do dyspozycji pełnoprawnego generatora. Posiadając takie cudo można by przekazać go do funkcji i stworzyć oddzielny generator na każde wywołanie:

def generator():
    for x in [16,28,386,412,1594,625,718,882,91,106,110,12]:
        yield x

def rozwiazanie(generator):
    suma = sum(generator())
    return [100*x/suma for x in generator()]

print(rozwiazanie(generator))

Uruchom w edytorze

Output:

[0.321285140562249, 0.5622489959839357, 7.751004016064257, 8.273092369477911, 32.00803212851405, 12.550200803212851, 14.417670682730924, 17.710843373493976, 1.8273092369477912, 2.1285140562248994, 2.208835341365462, 0.24096385542168675]