Historia pytania
Python od wersji 2.4 wzbogacił wyrażenia listowe (list comprehensions) o tzw. "wyrażenia generatora". Umożliwiają one tworzenie leniwą sekwencję wartości, podobną do generatorów, ale w kompaktowej i czytelnej formie.
Problem
Wyrażenia listowe ([x for x in iterable]) tworzą listę, ładując jednocześnie wszystkie elementy do pamięci. Jest to nieefektywne lub wręcz niebezpieczne, gdy liczba elementów jest bardzo duża. Funkcje generatorów (z użyciem yield) są bardziej elastyczne, ale wymagają osobnego zdefiniowania funkcji i większej liczby linii kodu.
Rozwiązanie
Wyrażenia generatora ((x for x in iterable)) oferują zwięzłą składnię do tworzenia sekwencji leniwe (elementy są obliczane w miarę potrzeby, a nie ładują się wszystkie na raz). Wyglądają podobnie do wyrażeń listowych, ale używają okrągłych nawiasów:
# Wyrażenie listowe ładuje wszystko do pamięci squares_list = [x**2 for x in range(10**6)] # Wyrażenie generatora: elementy są dostarczane na żądanie, pamięć jest prawie nieużywana squares_gen = (x**2 for x in range(10**6)) # Pobierz pierwsze pięć wartości generatora for _ in range(5): print(next(squares_gen))
Kluczowe cechy:
Czy można "przechodzić" to samo wyrażenie generatora kilka razy?
Nie, po iteracji raz generator "wyczerpuje się". Aby ponownie przejść, należy stworzyć nowy generator lub skorzystać z wyrażenia listowego.
it = (x for x in range(3)) print(list(it)) # [0,1,2] print(list(it)) # [] — nie można już uzyskać wartości
Czy generatory zachowują stan między użyciami?
Tak, wyrażenie generatora zachowuje „pozycję” między wywołaniami next() (lub przy kolejnej iteracji), ale nie można go zresetować do początku, chyba że stworzysz nowy obiekt.
Czy można użyć wyrażenia generatora kilka razy w jednej linii?
Nie! Jeśli "rozpakowujesz" generator w kilka miejsc jednocześnie (np. do kilku funkcji naraz, nie zwracając go do listy), część danych zostaje utracona — każde podrzędne użycie przesuwa wskaźnik do przodu.
g = (x for x in range(3)) print(sum(g), list(g)) # sum(g) uzyska wszystko, list(g) pozostanie pusty
W projekcie do analizy dużych plików użyto:
data = (parse_line(line) for line in file) process(list(data)) other_process(list(data))
Plusy:
Minusy:
Użyto wyrażenia listowego, jeśli wymagane było powtórne wykorzystanie danych, lub stworzono generator do jednorazowego spożycia:
# Generator tylko do jednorazowej analizy (na przykład, zliczyć sumę) total = sum(parse_line(line) for line in file)
Plusy:
Minusy: