Geschichte der Frage
Faule Verarbeitung (lazy evaluation) ist eine Programmiertechnik, bei der Berechnungen bis zu dem Zeitpunkt aufgeschoben werden, an dem das Ergebnis in diesem Code benötigt wird. In der Programmiersprache Python hat dieses Paradigma durch Generatoren und spezielle Funktionen der Standardbibliothek, wie itertools, an Popularität gewonnen. Ursprünglich stammen solche Techniken aus funktionalen Sprachen, aber Python bietet eigene native und externe Werkzeuge für die faule Verarbeitung.
Problem
Traditionelle eager (gierige) Verarbeitung erfordert, dass alle Daten sofort geladen und berechnet werden (z.B. bei der Verwendung von Listenverständnissen), was zu hohem Speicheraufwand und Leistungseinbußen bei der Arbeit mit großen oder unendlichen Sequenzen führen kann. Faule Verarbeitung ermöglicht es, Elemente nur bei Bedarf „nachzuladen“ und zu verarbeiten, wodurch unnötige Ressourcenkosten vermieden werden.
Lösung
In Python wird die faule Verarbeitung nicht nur mithilfe von Generatoren (yield), sondern auch durch spezielle faule Funktionen in den Modulen itertools, Standardfunktionen wie map, filter, sowie durch Objekte wie Generatorausdrücke (generator expressions) realisiert. Zum Beispiel gibt die Funktion map() einen faulen Iterator zurück, der Werte nur bei Bedarf berechnet:
# Beispiel für faule Verarbeitung: Quadrat jedes Zahl squares = map(lambda x: x ** 2, range(10**10)) # kein Speicher für Liste print(next(squares)) # 0 print(next(squares)) # 1
Hauptmerkmale:
Implementiert die Funktion map() in Python immer faule Verarbeitung? Wie kann man herausfinden, welche Standardfunktionen faul sind?
Nein, seit Python 3 geben die Funktionen map(), filter(), zip() Iteratoren zurück, d.h. sie implementieren faule Verarbeitung. In Python 2 gaben diese Funktionen Listen zurück. Um zu wissen, ob ein Objekt faul ist, muss man seinen Typ überprüfen oder die Dokumentation lesen:
result = map(lambda x: x+1, range(5)) print(type(result)) # 'map' — das ist ein Iterator
Funktioniert die faule Verarbeitung bei der Anwendung von Generatorausdrücken innerhalb der Funktion sum()?
Die Funktion sum() erfordert, dass der gesamte Iterator bis zum Ende durchlaufen wird. Der Generatorausdruck ist an sich faul, aber sum() verbraucht letztlich dennoch die gesamte Sequenz:
s = sum(x**2 for x in range(1000000)) # Generator wird vollständig verbraucht
Kann man faule Verarbeitung auf normale Listen und Tupel anwenden, z.B. über map/lambda?
Ja, das ist möglich, aber Listen und Tupel werden dennoch im Speicher geladen. Map gibt einen faulen Iterator darüber zurück, aber die ursprünglichen Daten sind dennoch vollständig im Speicher. Für eine vollständige faule Kette ist es wünschenswert, an jedem Punkt mit Generatoren zu arbeiten:
def gen(): for i in range(1, 100): yield i squares = map(lambda x: x**2, gen()) # alles faul
Ein Entwickler schreibt eine Verarbeitung für eine große Logdatei und verwendet einen Generatorausdruck zur Filterung von Zeilen, konvertiert sie aber versehentlich sofort in eine Liste:
with open('biglog.txt') as f: important_lines = [line for line in f if 'ERROR' in line] # lädt die gesamte Datei
Vorteile:
Nachteile:
Ein anderes Team nutzt einen faulen Ansatz mit einem Generatorausdruck und verarbeitet die Zeilen, während sie eintreffen:
with open('biglog.txt') as f: for line in (l for l in f if 'ERROR' in l): process(line)
Vorteile:
Nachteile: