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))
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))
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 StopIteration
i 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))
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))
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))
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]