Historia pytania
Leniwa obróbka (lazy evaluation) to technika programowania, w której obliczenia są odkładane do momentu, gdy wynik jest potrzebny przez dany kod. W języku Python ta paradygma zyskała popularność dzięki generatorom i specjalnym funkcjom standardowej biblioteki, takim jak itertools. Początkowo takie techniki przybyły z języków funkcyjnych, ale Python oferuje swoje natywne i zewnętrzne narzędzia do leniwej obróbki.
Problem
Tradycyjna eager (żądna) obróbka wymaga załadowania i obliczenia wszystkich danych jednocześnie (na przykład przy użyciu wyrażeń listowych), co może prowadzić do dużego zużycia pamięci i spadku wydajności podczas pracy z dużymi lub nieskończonymi sekwencjami. Leniwa obróbka pozwala na „podładowywanie” i przetwarzanie elementów tylko w miarę potrzeb, unikając niepotrzebnych kosztów zasobów.
Rozwiązanie
W Pythonie leniwa obróbka jest realizowana nie tylko za pomocą generatorów (yield), ale także poprzez specjalne leniwe funkcje w modułach itertools, standardowe funkcje takie jak map, filter, a także obiekty typu wyrażeń generatorowych (generator expressions). Na przykład, funkcja map() zwraca leniwy iterator, który oblicza wartości tylko w miarę potrzeby:
# Przykład leniwej obróbki: podnoszenie każdego liczby do kwadratu squares = map(lambda x: x ** 2, range(10**10)) # nie zużywa pamięci na listę print(next(squares)) # 0 print(next(squares)) # 1
Kluczowe cechy:
Czy funkcja map() w Pythonie zawsze realizuje leniwą obróbkę? Jak sprawdzić, które standardowe funkcje są leniwe?
Nie, począwszy od Pythona 3, funkcje map(), filter(), zip() zwracają iteratory, to znaczy realizują leniwą obróbkę. W Pythonie 2 te funkcje zwracały listy. Aby sprawdzić, czy obiekt jest leniwy, należy sprawdzić jego typ lub zapoznać się z dokumentacją:
result = map(lambda x: x+1, range(5)) print(type(result)) # 'map' — to iterator
Czy leniwa obróbka zadziała przy zastosowaniu wyrażenia generatorowego wewnątrz funkcji sum()?
Funkcja sum() wymaga przejścia przez cały iterator do końca. Wyrażenie generatorowe samo w sobie jest leniwe, ale sum() ostatecznie zużywa całą sekwencję:
s = sum(x**2 for x in range(1000000)) # generator jest całkowicie zużyty
Czy można zastosować leniwą obróbkę do zwykłych list i krotek, na przykład przez map/lambda?
Tak, można, ale listy i krotki są i tak załadowane w pamięci. Map zwróci leniwy iterator po nich, ale oryginalne dane nadal będą całkowicie w pamięci. Aby uzyskać pełny leniwy łańcuch, zaleca się pracować z generatorami na każdym etapie:
def gen(): for i in range(1, 100): yield i squares = map(lambda x: x**2, gen()) # wszystko leniwe
Programista pisze obróbkę dla dużego pliku logów, używając wyrażenia generatorowego do filtrowania linii, ale przypadkowo przekształca je w listę:
with open('biglog.txt') as f: important_lines = [line for line in f if 'ERROR' in line] # ładowanie całego pliku
Zalety:
Wady:
Inny zespół stosuje leniwe podejście z wyrażeniem generatorowym i przetwarza linie w miarę ich uzyskiwania:
with open('biglog.txt') as f: for line in (l for l in f if 'ERROR' in l): process(line)
Zalety:
Wady: