Achtergrond van de vraag
Lazy evaluation is een programmeertechniek waarbij berekeningen worden uitgesteld totdat het resultaat daadwerkelijk nodig is in de code. In de programmeertaal Python is dit paradigma populair geworden dankzij generators en speciale functies in de standaardbibliotheek, zoals itertools. Deze technieken hebben oorspronkelijk hun oorsprong in functionele talen, maar Python biedt zijn eigen native en externe tools voor lazy processing.
Probleem
Traditionele eager (hongerige) verwerking vereist dat alle gegevens onmiddellijk worden geladen en berekend (bijvoorbeeld bij het gebruik van lijstcomprehensies), wat kan leiden tot hoge geheugengebruik en verminderde prestaties bij het werken met grote of oneindige reeksen. Lazy processing maakt het mogelijk om elementen alleen op aanvraag te laden en te verwerken, waardoor onnodige kosten voor middelen worden vermeden.
Oplossing
In Python wordt lazy processing niet alleen geïmplementeerd met behulp van generators (yield), maar ook via speciale lazy functies in de itertools modules, standaard functies zoals map, filter, en ook objecten van generator-expressies. Bijvoorbeeld, de functie map() retourneert een lazy iterator die waarden alleen berekent wanneer daarom wordt gevraagd:
# Voorbeeld van lazy verwerking: elk getal kwadrateren squares = map(lambda x: x ** 2, range(10**10)) # geen geheugen verspilling voor lijst print(next(squares)) # 0 print(next(squares)) # 1
Belangrijkste kenmerken:
Implementeert de functie map() in Python altijd lazy processing? Hoe weet je welke standaardfuncties lazy zijn?
Nee, sinds Python 3 retourneren de functies map(), filter(), zip() iterators, dat wil zeggen, ze implementeren lazy processing. In Python 2 retourneerden deze functies lijsten. Om te weten of een object lazy is, moet je naar het type kijken of de documentatie bestuderen:
result = map(lambda x: x+1, range(5)) print(type(result)) # 'map' is een iterator
Zal lazy processing werken wanneer een generator-expressie wordt toegepast binnen de functie sum()?
De functie sum() vereist dat het de hele iterator doorloopt tot het einde. De generator-expressie zelf is lazy, maar sum() verbruikt uiteindelijk toch de volledige reeks:
s = sum(x**2 for x in range(1000000)) # generator wordt volledig verbruikt
Kan lazy processing worden toegepast op gewone lijsten en tuples, bijvoorbeeld via map/lambda?
Ja, dat kan, maar lijsten en tuples worden nog steeds in het geheugen geladen. Map retourneert een lazy iterator over hen, maar de oorspronkelijke gegevens zijn nog steeds volledig in het geheugen. Voor een volledige lazy keten is het wenselijk om op elke stap met generators te werken:
def gen(): for i in range(1, 100): yield i squares = map(lambda x: x**2, gen()) # alles is lazy
Een ontwikkelaar schrijft een verwerking voor een groot logbestand, waarbij hij een generator-expressie gebruikt om regels te filteren, maar per ongeluk deze onmiddellijk omzet in een lijst:
with open('biglog.txt') as f: important_lines = [line for line in f if 'ERROR' in line] # laadt het hele bestand
Voordelen:
Nadelen:
Een ander team gebruikt een lazy benadering met een generator-expressie en verwerkt de regels terwijl ze binnenkomen:
with open('biglog.txt') as f: for line in (l for l in f if 'ERROR' in l): process(line)
Voordelen:
Nadelen: