3.1 Wprowadzenie do generatorów
Generatory to funkcje, które zwracają obiekt-iterator. Te iteratory generują wartości na żądanie, co pozwala na obsługę potencjalnie dużych zbiorów danych bez konieczności ładowania ich w całości do pamięci.
Istnieje kilka sposobów tworzenia generatorów, poniżej omówimy te najpopularniejsze.
Generatory oparte na funkcjach
Generatory tworzy się za pomocą słowa kluczowego yield wewnątrz funkcji. Kiedy funkcja z yield jest wywoływana, zwraca obiekt-generator, ale nie wykonuje kodu wewnątrz funkcji od razu. Zamiast tego wykonywanie zostaje zatrzymane na wyrażeniu yield i wznawia się przy każdym wywołaniu metody __next__() obiektu-generatora.
def count_up_to(max):
count = 1
while count <= max:
yield count
count += 1
counter = count_up_to(5)
print(next(counter)) # Wyjście: 1
print(next(counter)) # Wyjście: 2
print(next(counter)) # Wyjście: 3
print(next(counter)) # Wyjście: 4
print(next(counter)) # Wyjście: 5
Jeśli w funkcji znajduje się operator yield, Python zamiast tradycyjnego wykonania funkcji tworzy obiekt-generator, który zarządza stanem wykonania funkcji.
Wyrażenia generatorowe
Wyrażenia generatorowe są podobne do wyrażeń listowych (List Comprehension), ale tworzone są za pomocą nawiasów okrągłych zamiast kwadratowych. Zwracają również obiekt-generator.
squares = (x ** 2 for x in range(10))
print(next(squares)) # Wyjście: 0
print(next(squares)) # Wyjście: 1
print(next(squares)) # Wyjście: 4
Która metoda bardziej Ci się podoba?
3.2 Zalety generatorów
Efektywne wykorzystanie pamięci
Generatory obliczają wartości w locie, co pozwala na przetwarzanie dużych danych bez ich pełnego ładowania do pamięci. To czyni generatory idealnym wyborem do pracy z dużymi zbiorami danych lub strumieniami danych.
def large_range(n):
for i in range(n):
yield i
for value in large_range(1000000):
# Przetwarzamy wartości pojedynczo
print(value)
Leniwe obliczenia
Generatory wykonują leniwe obliczenia, co oznacza, że obliczają wartości tylko wtedy, kiedy jest to konieczne. Pozwala to uniknąć zbędnych obliczeń i poprawia wydajność.
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
fib = fibonacci()
for _ in range(10):
print(next(fib))
Wygoda składni
Generatory oferują wygodną składnię do tworzenia iteratorów, co upraszcza pisanie i czytanie kodu.
3.3 Użycie generatorów
Przykłady użycia generatorów w bibliotece standardowej
Wiele funkcji w bibliotece standardowej Pythona używa generatorów. Na przykład, funkcja range() zwraca obiekt-generator, który generuje sekwencję liczb.
for i in range(10):
print(i)
Tak, świat już nigdy nie będzie taki sam.
Tworzenie nieskończonych sekwencji
Generatory pozwalają tworzyć nieskończone sekwencje, które mogą być przydatne w różnych scenariuszach, takich jak generowanie nieskończonych strumieni danych.
def natural_numbers():
n = 1
while True:
yield n
n += 1
naturals = natural_numbers()
for _ in range(10):
print(next(naturals))
Użycie send() i close()
Obiekty-generatorów obsługują metody send() i close(), które pozwalają wysyłać wartości z powrotem do generatora i kończyć jego działanie.
def echo():
while True:
received = yield
print(received)
e = echo()
next(e) # Uruchamiamy generator
e.send("Cześć, świecie!") # Wyjście: Cześć, świecie!
e.close()
3.4 Generatory w praktyce
Generatory i wyjątki
Generatory mogą obsługiwać wyjątki, co czyni je potężnym narzędziem do pisania bardziej odpornych programów.
def controlled_execution():
try:
yield "Start"
yield "Praca"
except GeneratorExit:
print("Generator zamknięty")
gen = controlled_execution()
print(next(gen)) # Wyjście: Start
print(next(gen)) # Wyjście: Praca
gen.close() # Wyjście: Generator zamknięty
Pracę z wyjątkami omówimy na kolejnych wykładach, ale myślę, że będzie dla ciebie przydatne wiedzieć, że generatory radzą sobie z nimi świetnie.
Zagnieżdżone generatory
Generatory mogą być zagnieżdżane, co pozwala na tworzenie złożonych struktur iteracyjnych.
def generator1():
yield from range(3)
yield from "ABC"
for value in generator1():
print(value)
# Wyjście
0
1
2
A
B
C
Wyjaśnienie:
yield from: Ta konstrukcja służy do delegowania części operacji innemu generatorowi, co pozwala na uproszczenie kodu i poprawę jego czytelności.
Generatory a wydajność
Użycie generatorów może znacznie poprawić wydajność programów dzięki zmniejszeniu wykorzystania pamięci i bardziej efektywnemu wykonywaniu iteracji.
Przykład porównania list i generatorów
import time
import sys
def memory_usage(obj):
return sys.getsizeof(obj)
n = 10_000_000
# Użycie listy
start_time = time.time()
list_comp = [x ** 2 for x in range(n)]
list_time = time.time() - start_time
list_memory = memory_usage(list_comp)
# Użycie generatora
start_time = time.time()
gen_comp = (x ** 2 for x in range(n))
gen_result = sum(gen_comp) # Obliczamy sumę dla porównania wyników
gen_time = time.time() - start_time
gen_memory = memory_usage(gen_comp)
print(f"Lista:")
print(f" Czas: {list_time:.2f} sec")
print(f" Pamięć: {list_memory:,} bajtów")
print(f"\nGenerator:")
print(f" Czas: {gen_time:.2f} sec")
print(f" Pamięć: {gen_memory:,} bajtów")
Lista:
Czas: 0.62 sec
Pamięć: 89,095,160 bajtów
Generator:
Czas: 1.13 sec
Pamięć: 200 bajtów
GO TO FULL VERSION