Funkcja, która robi pauzy
Generator wygląda jak zwykła funkcja, ale zamiast obliczyć cały wynik i go zwrócić, oddaje (yield) jedną wartość naraz i zatrzymuje się między kolejnymi wartościami, dopóki ktoś nie poprosi o następną.
Najprostszy możliwy generator:
Zwróć uwagę na yield zamiast return. Gdy for po raz pierwszy prosi o wartość, Python wykonuje ciało funkcji, aż dojdzie do yield 1. Funkcja zatrzymuje się właśnie tam, oddaje pętli 1 i dokładnie pamięta, gdzie przerwała, łącznie ze zmiennymi. Następna iteracja wznawia działanie od tego miejsca: current += 1, powrót do while, yield 2. I tak dalej, aż warunek pętli przestanie być spełniony, a wtedy generator po prostu się kończy.
Na tym zatrzymywaniu i wznawianiu polega cała sztuczka.
Dlaczego nie zbudować po prostu listy?
Bo wersja z listą rezerwuje wszystkie wartości z góry:
Przy 5 elementach to w porządku. Teraz wyobraź sobie, że chcesz 50 milionów liczb całkowitych, a interesuje cię tylko pierwsza spełniająca jakiś warunek. Wersja z listą tworzy 50 milionów liczb, z których większość i tak wyrzucisz. Wersja z generatorem tworzy dokładnie tyle, ile zużyje wywołujący. Gdy pętla for znajdzie to, czego szuka, i wykona break, generator po prostu przestaje działać.
To wzorzec, który warto sobie przyswoić: generatory pozwalają pisać kod iteracji bez decydowania z góry, jak dużej części wyniku będziesz potrzebować.
Wyrażenia generatorowe
Jeśli znasz już list comprehension, znasz też składnię: zamień nawiasy kwadratowe na okrągłe:
squares_gen jeszcze niczego nie oblicza. To tylko przepis. Iteracja wykonuje go krok po kroku.
Wyrażenia generatorowe świetnie sprawdzają się jako argumenty funkcji, które zużywają obiekt iterowalny:
Bez pośredniej listy. sum, max i any czytają wartości po jednej, a właśnie tego potrzebują.
Odczyt dużego pliku linia po linii
To klasyczny praktyczny przypadek dla generatorów: przetwarzanie pliku, który jest za duży, by wczytać go do pamięci:
def parse_log_lines(path):
with open(path) as f:
for line in f:
if line.startswith("ERROR"):
yield line.rstrip()
for error in parse_log_lines("app.log"):
print(error)
Plik jest czytany leniwie. Każde pobranie z generatora ściąga z dysku jedną linię, filtruje ją i oddaje. Zużycie pamięci pozostaje stałe niezależnie od rozmiaru pliku.
Raz i koniec
Po generatorze można przejść tylko raz. Gdy dojdziesz do końca, jest wyczerpany:
Druga pętla nic nie wypisuje. W generatorze nic już nie zostało.
Jeśli musisz iterować więcej niż raz, wywołaj funkcję generatora ponownie, żeby dostać nowy generator, albo zamień sekwencję w listę przez list(...) i iteruj po liście wielokrotnie. Wybierz według kosztu: ponowne budowanie jest w porządku, gdy praca jest tania, a lista, gdy sekwencja jest mała.
next() i ręczna iteracja
Nie musisz używać pętli for. next() pobiera jedną wartość naraz:
StopIteration to sposób, w jaki generator sygnalizuje "to już koniec". Pętle for łapią go po cichu. W ręcznym kodzie możesz przekazać wartość domyślną do next(gen, default), żeby uniknąć wyjątku.
Generatory nieskończone
Skoro wartości powstają na żądanie, generator może reprezentować sekwencję bez końca, o ile konsument w pewnym momencie przestanie prosić:
while True z yield w środku nie zawiesza programu. Znaczy tylko "dopóki ktoś prosi, wytwarzaj dalej". To konsument decyduje, kiedy przestać.
Ten wzorzec pojawia się przy strumieniach danych, pętlach zdarzeń i wszędzie tam, gdzie pobierasz wartości ze źródła bez określonej długości.
yield from: przekazanie do innego obiektu iterowalnego
Jeśli generator ma oddać każdą wartość z innego obiektu iterowalnego, yield from robi to w jednej linii:
Bez yield from trzeba by napisać zagnieżdżoną pętlę for z yield x w środku. Poprawnie przekazuje też wywołania send() i throw(), jeśli kiedyś ich użyjesz, ale w codziennym kodzie myśl o nim jak o "oddaj każdą wartość z tego obiektu".
Kiedy sięgnąć po generator
Trzy sygnały, że generator to właściwe narzędzie:
- Sekwencja jest duża, być może nieskończona, albo jej pełne wytworzenie jest kosztowne.
- Konsument może przestać przed końcem (na przykład
breakprzy pierwszym trafieniu). - Chcesz łączyć przekształcenia (filtrowanie, mapowanie, branie pierwszych elementów) bez budowania pośrednich list.
A kiedy nie:
- Potrzebujesz dostępu swobodnego (
seq[42]). Generatory idą tylko do przodu. - Musisz iterować po tej samej sekwencji kilka razy. Użyj listy.
- Sekwencja jest mała i już ją masz. List comprehension będzie prostsze.
Generatory, list comprehensions i zwykłe listy to właściwe odpowiedzi w różnych zadaniach. Cała umiejętność polega na tym, by wybierać bez długiego zastanawiania się, a najszybciej wyrobisz ten instynkt, zauważając przy każdej pisanej iteracji, czy lepiej pasuje "wytwórz najpierw wszystko", czy "wytwarzaj po jednym".
Dalej: menedżery kontekstu w szczegółach
Znasz już większość idiomów, których Python używa do iteracji. Następne są menedżery kontekstu, czyli instrukcja with, które dobrze łączą się z generatorami przy strumieniowym pobieraniu danych z plików i połączeń sieciowych.
Najczęściej zadawane pytania
Czym jest generator w Pythonie?
Generator to funkcja, która wytwarza wartości po jednej i zatrzymuje się między nimi. Piszesz ją przez def jak zwykłą funkcję, ale zamiast return używasz yield. Wywołanie zwraca obiekt generatora, a każda iteracja for albo każde wywołanie next() wykonuje funkcję do następnego yield.
Czym różni się lista od generatora?
Lista trzyma w pamięci wszystkie elementy naraz. Generator oblicza elementy na żądanie i zapomina je po ich zużyciu. Przy dużych albo nieskończonych sekwencjach generatory zużywają małą, stałą ilość pamięci, a przy niewielkich wynikach potrzebnych wielokrotnie lepsza jest lista.
Czy można iterować po generatorze dwa razy?
Nie. Generator wyczerpuje się po pierwszym pełnym przejściu, a druga pętla for po nim nic nie daje. Jeśli musisz iterować więcej niż raz, wywołaj funkcję generatora ponownie, żeby dostać nowy generator, albo zapisz wyniki w liście.