ProgrammationData Engineer

Expliquez comment fonctionne l'évaluation paresseuse (lazy evaluation) en Python et où elle est appliquée en dehors des générateurs. Quels sont les avantages des calculs paresseux ?

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse.

L'évaluation paresseuse (lazy evaluation) est un concept clé de la programmation efficace, où les valeurs ne sont calculées que si nécessaire. Historiquement, toutes les structures de base intégrées en Python (listes, tuples) étaient "affamées" : elles créent et placent en mémoire tous les éléments à l'avance. Avec l'augmentation des volumes de données et des tâches de traitement de flux, il est devenu nécessaire d'avoir des calculs paresseux.

Problème : les calculs affamés entraînent une utilisation inefficace de la mémoire et du temps là où des résultats peuvent être obtenus progressivement - par exemple, lors du filtrage, de la transformation de longues collections ou du streaming de fichiers.

Solution : Python a introduit de nombreux outils pour les calculs paresseux : générateurs, itérateurs, ainsi que des fonctions de la bibliothèque standard (map, filter, zip, enumerate) et le module itertools. Tous retournent non pas des collections prêtes, mais des objets "paresseux" qui produisent un résultat valeur par valeur.

Exemple de code :

result = map(lambda x: x * x, range(100)) # retournera un générateur itérateur for y in result: print(y) # les valeurs sont calculées au fur et à mesure de l'itération import itertools inf = itertools.count(1) for i in inf: if i > 3: break print(i) # 1, 2, 3

Caractéristiques clés :

  • Tous les éléments ne sont pas stockés en mémoire : ils sont calculés "à la demande".
  • Peut travailler avec des flux de données infinis.
  • Permet de traiter des collections qui n'existent même pas physiquement dans leur intégralité (par exemple, un flux de données depuis le réseau).

Questions pièges.

Les fonctions map/filter retournent-elles toujours une liste en Python3 ?

Non, en Python 3, ces fonctions retournent des itérateurs, pas des listes. Pour obtenir une liste, il faut envelopper le résultat dans list().

x = map(int, ['1', '2']) # <objet map> list(x) # [1, 2]

Peut-on obtenir la longueur du résultat de map sans le convertir en liste ?

Non, un itérateur ne sait pas à l'avance combien d'éléments il contient, tant qu'il n'a pas parcouru tous les éléments. Il faut le calculer via list(), ce qui annule la paresse.

La fonction range en Python3 est-elle affamée ou paresseuse ?

Paresseuse : range crée un "objet range" — il "calcule" les éléments à la demande, sans stocker toute la séquence.

Erreurs typiques et anti-patterns

  • Convertissent implicitement les objets paresseux en liste, perdant ainsi l'avantage d'économie de mémoire.
  • Considèrent les itérateurs et les générateurs comme interchangeables avec les listes (mais ils ne peuvent pas être indexés ou parcourus plusieurs fois).
  • Appliquent len() aux objets paresseux et rencontrent des erreurs.

Exemple de la vie

Cas négatif

Un script traite un énorme fichier CSV en créant une liste de toutes les lignes via list(open(f)). Le serveur "meurt" à cause du manque de mémoire avec un grand fichier.

Avantages :

  • Indexation rapide sur une ligne spécifique.

Inconvénients :

  • Forte consommation de mémoire.
  • Le programme ne peut pas évoluer avec de grandes données.

Cas positif

Le code utilise un traitement paresseux : il parcourt les lignes du fichier avec un itérateur for line in open(f), ou les traite via map/filter sans créer de collections intermédiaires.

Avantages :

  • Fonctionnement avec des fichiers de n'importe quel volume.
  • Minimum de mémoire.

Inconvénients :

  • Si un accès aléatoire par index est nécessaire, une structure de données séparée sera requise.