Historique de la question
Le traitement paresseux (lazy evaluation) est une technique de programmation où les calculs sont retardés jusqu'à ce que le résultat soit nécessaire pour ce code. Dans le langage Python, cette paradigme a gagné en popularité grâce aux générateurs et aux fonctions spéciales de la bibliothèque standard, comme itertools. À l'origine, de telles techniques proviennent des langages fonctionnels, mais Python fournit ses propres outils natifs et tiers pour le traitement paresseux.
Problème
Le traitement traditionnel 'eager' (avide) nécessite le chargement et le calcul de toutes les données immédiatement (par exemple, lors de l'utilisation d'expressions de liste), ce qui peut entraîner une consommation mémoire importante et une diminution des performances lors du travail avec de grandes ou infinies séquences. Le traitement paresseux permet de "charger" et de traiter les éléments uniquement au besoin, évitant ainsi des dépenses de ressources inutiles.
Solution
En Python, le traitement paresseux est réalisé non seulement par le biais de générateurs (yield), mais aussi grâce à des fonctions paresseuses spéciales dans les modules itertools, aux fonctions standard comme map, filter, et aux objets de type expressions génératrices (generator expressions). Par exemple, la fonction map() retourne un itérateur paresseux qui calcule les valeurs uniquement au fur et à mesure de l'appel :
# Exemple de traitement paresseux : élevation au carré de chaque nombre squares = map(lambda x: x ** 2, range(10**10)) # pas de mémoire dépensée pour une liste print(next(squares)) # 0 print(next(squares)) # 1
Caractéristiques clés :
La fonction map() en Python réalise-t-elle toujours un traitement paresseux ? Comment savoir quelles fonctions standard sont paresseuses ?
Non, depuis Python 3, les fonctions map(), filter(), zip() retournent des itérateurs, c'est-à-dire qu'elles réalisent un traitement paresseux. En Python 2, ces fonctions renvoyaient des listes. Pour savoir si un objet est paresseux, il suffit de vérifier son type ou de consulter la documentation :
result = map(lambda x: x+1, range(5)) print(type(result)) # 'map' — c'est un itérateur
Le traitement paresseux fonctionnera-t-il lors de l'application d'une expression génératrice à l'intérieur de la fonction sum() ?
La fonction sum() nécessite de parcourir l'intégralité de l'itérateur jusqu'à la fin. L'expression génératrice est elle-même paresseuse, mais sum() consomme quand même toute la séquence :
s = sum(x**2 for x in range(1000000)) # le générateur est entièrement consommé
Peut-on appliquer le traitement paresseux à des listes et tuples ordinaires, par exemple avec map/lambda ?
Oui, mais les listes et tuples sont toujours chargés en mémoire. map retournera un itérateur paresseux sur eux, mais les données d'origine sont toujours entièrement en mémoire. Pour une chaîne paresseuse complète, il est souhaitable de travailler avec des générateurs à chaque étape :
def gen(): for i in range(1, 100): yield i squares = map(lambda x: x**2, gen()) # tout est paresseux
Un développeur écrit un traitement pour un gros fichier journal en utilisant une expression génératrice pour filtrer les lignes, mais transforme accidentellement cela en liste immédiatement :
with open('biglog.txt') as f: important_lines = [line for line in f if 'ERROR' in line] # charge tout le fichier
Avantages :
Inconvénients :
Une autre équipe utilise une approche paresseuse avec une expression génératrice et traite les lignes au fur et à mesure qu'elles arrivent :
with open('biglog.txt') as f: for line in (l for l in f if 'ERROR' in l): process(line)
Avantages :
Inconvénients :