❄️ Generatory
Generatory
Generatory to specjalne obiekty, które generują wyniki na żądanie - jeden po drugim - zamiast tworzyć i przechowywać całą serię wyników od razu. Dzięki temu są wydajniejsze przy pracy z dużymi zestawami danych, ponieważ zużywają znacznie mniej pamięci.
Generatory działają na zasadzie lazy evaluation - nie obliczają wszystkich wyników z góry, lecz tylko wtedy, gdy są potrzebne.
Funkcje generatorów
Tworzymy je tak samo jak zwykłe funkcje, ale zamiast return używamy yield. Słowo kluczowe yield zwraca wartość, a następnie zawiesza działanie funkcji - zapamiętując cały jej stan (zmienne lokalne, miejsce wykonania). Przy kolejnym wywołaniu next() funkcja wznawia działanie od miejsca, w którym się zatrzymała.
def licz_do(n):
count = 1
while count <= n:
yield count
count += 1
counter = licz_do(5)
print(next(counter)) # 1
print(next(counter)) # 2
# Możemy też przeiterować przez resztę pętlą
for number in counter:
print(number) # 3, 4, 5
Generatory są jednorazowe
Po wyczerpaniu wszystkich wartości generator jest pusty - nie można go przewinąć do początku. Żeby przeiterować ponownie, trzeba stworzyć nowy obiekt generatora.
counter = licz_do(3)
print(list(counter)) # [1, 2, 3]
print(list(counter)) # [] - już wyczerpany
Wyrażenia generatorów
Bardziej zwięzły sposób na tworzenie generatorów, analogiczny do list składanych. Różnica: nawiasy okrągłe () zamiast kwadratowych []. W przeciwieństwie do listy składanej - nie tworzy całej kolekcji w pamięci od razu.
# Lista składana - oblicza wszystkie wartości natychmiast
lista = [x ** 2 for x in range(1_000_000)]
# Wyrażenie generatora - oblicza wartości na żądanie
gen = (x ** 2 for x in range(1_000_000))
gen = (x ** 2 for x in range(5))
print(next(gen)) # 0
print(next(gen)) # 1
for value in gen:
print(value) # 4, 9, 16
Praktyczne zastosowanie - nieskończone sekwencje
Generator może generować wartości w nieskończoność, bo nie przechowuje ich wszystkich w pamięci:
def kolejne_liczby(start=0):
n = start
while True:
yield n
n += 1
gen = kolejne_liczby(10)
print(next(gen)) # 10
print(next(gen)) # 11
print(next(gen)) # 12
# ... można wywoływać w nieskończoność
Czym generatory różnią się od iteratorów?
Różnią się sposobem tworzenia:
- Iteratory tworzy się z dowolnej kolekcji przez
iter()lub definiując klasę z metodami__iter__()i__next__()(wymaga znajomości OOP). - Generatory tworzy się funkcją z
yieldlub wyrażeniem generatora - Python automatycznie obsługuje cały mechanizm stanu.
Generatory są uproszczonymi iteratorami - robią to samo, ale wymagają znacznie mniej kodu. Iteratory (jako klasy) zostaną omówione po przerobieniu programowania obiektowego.
# Pełny iterator jako klasa (wymaga OOP):
class LiczDo:
def __init__(self, max_value):
self.max_value = max_value
self.current = 1
def __iter__(self):
return self
def __next__(self):
if self.current > self.max_value:
raise StopIteration
value = self.current
self.current += 1
return value
# Odpowiednik jako generator (5 linii zamiast 15):
def licz_do(max_value):
current = 1
while current <= max_value:
yield current
current += 1
📝 Zadania
Stwórz plik python1course/zaj02/generatory.py i wykonaj w nim poniższe zadania.
-
Napisz generator
dni_tygodnia(), który zwraca nazwy dni tygodnia od poniedziałku do niedzieli. Następnie:- Użyj go w pętli
for, żeby wypisać wszystkie dni, - Utwórz nowy obiekt generatora i użyj
next(), żeby pobrać tylko pierwsze trzy dni bez iterowania przez cały tydzień.
Tip
To zadanie można wykonać zarówno funkcją z
yield, jak i wyrażeniem generatora. - Użyj go w pętli
-
Napisz generator
fibonacci(), który w nieskończoność zwraca kolejne liczby Fibonacciego (0, 1, 1, 2, 3, 5, 8, ...). Pobierz i wypisz pierwszych 10 wyrazów. -
Napisz wyrażenie generatora, które z listy słów zwraca tylko te, które mają więcej niż 4 znaki, zamienione na wielkie litery. Przetestuj na liście:
Porównaj zużycie pamięci z analogiczną listą składaną (możesz użyć modułuslowa = ["python", "to", "swietny", "jezyk", "do", "nauki"]sysisys.getsizeof()).