3.3Fonctions avancées : décorateurs, générateurs, lambdas, map/filter
L'essentiel en 30 secondes
- Lambda = fonction anonyme jetable d'une ligne : key=lambda t: t[1] pour trier — def dès que ça se nomme ou se réutilise.
- Le décorateur enveloppe sans toucher : @chrono ajoute mesure, log ou contrôle en recevant la fonction et renvoyant une enveloppe — avec functools.wraps.
- Le générateur produit à la demande : yield traite un fichier immense en mémoire constante — listes pour le petit, générateurs pour le grand.
Mini-cas filé — Nabil passe au pythonique
Nabil, data analyst junior à Alger, trie ses ventes avec des fonctions de trois lignes à usage unique et charge un export de plusieurs Go en liste — son poste rame. Il trie en une ligne (key=lambda v: v["montant"]), compare map/filter aux compréhensions, lit l'export ligne à ligne via un générateur yield, et mesure ses traitements avec un décorateur @chrono à functools.wraps. Même données, code court et poste soulagé.
Ce même cas revient dans le cours, le schéma, le script et le défi 48h.
1. Lambda, map/filter : le petit outillage
La lambda est une fonction anonyme d'une ligne, faite pour l'usage jetable en argument : sorted(ventes, key=lambda v: v["montant"]) trie sur critère sans nommer une fonction. map applique une fonction à chaque élément, filter ne garde que ceux qui satisfont une condition — mais la compréhension ([f(x) for x in xs if ok(x)]) est souvent plus lisible en Python.
Règle de Nabil : lambda pour du jetable court, def dès que ça se nomme, se réutilise ou dépasse une ligne. Une lambda de cinq lignes est un def déguisé : le nommer, le documenter, le tester. Cet outillage se croise partout (clés de tri, callbacks) — le connaître, c'est lire couramment le code des autres.
2. Décorateurs et générateurs : envelopper et alléger
Le décorateur enrichit une fonction sans modifier son code : il la reçoit, renvoie une enveloppe qui ajoute mesure, log, cache ou contrôle, via la syntaxe @decorateur. Toujours avec functools.wraps pour préserver nom et docstring. Nabil écrit @chrono qui affiche le temps d'exécution — et reconnaît le mécanisme derrière @app.route ou @pytest.fixture.
Le générateur (fonction à yield) produit ses valeurs une à une, à la demande : suspendu à chaque yield, repris à l'appel suivant. Un export de plusieurs Go se lit ligne par ligne en mémoire constante au lieu d'exploser la RAM. Listes pour le petit réutilisé, générateurs pour le grand ou l'infini : c'est le choix qui scale.
La liste qui a mangé la RAM
Grand flux, petit yield : la mémoire vous remercie.
🔎 Exemple concret — Chronométrer sans toucher + lire sans charger
Nabil mesure son traitement et lit un gros export ligne à ligne.
import time
import functools
def chrono(f):
@functools.wraps(f)
def enveloppe(*args, **kwargs):
debut = time.perf_counter()
resultat = f(*args, **kwargs)
duree = time.perf_counter() - debut
print(f"{f.__name__}: {duree:.2f}s")
return resultat
return enveloppe
def lignes(fichier):
with open(fichier, encoding="utf-8") as f:
for ligne in f:
yield ligne.strip()
Lecture : @chrono mesure sans toucher le corps ; yield ne charge qu'une ligne à la fois.
Leçon : wraps préserve nom et docstring de la fonction décorée.
Vocabulaire de la section
- Lambda
- Fonction anonyme d'une ligne (lambda x: x * 2), utile pour un traitement court passé en argument.
Exemple simple : Le key=lambda v: v["montant"] du tri de Nabil.
Ce que ce n'est pas : une fonction nommée réutilisée et documentée.
Réflexe : lambda jetable, def dès que ça se nomme.
- map / filter
- map applique une fonction à chaque élément ; filter ne garde que ceux qui satisfont une condition.
Exemple simple : map(lambda x: x ** 2, montants) puis filter des pairs chez Nabil.
Ce que ce n'est pas : une compréhension souvent plus lisible.
Réflexe : comparer à la compréhension avant de choisir.
- Générateur
- Fonction à yield produisant ses valeurs une à une, à la demande : mémoire minimale, idéal gros flux.
Exemple simple : Les lignes d'export lues une par une par Nabil.
Ce que ce n'est pas : une liste géante chargée d'un coup en RAM.
Réflexe : yield dès que c'est grand ou infini.
- yield
- Renvoie une valeur en suspendant la fonction, reprise à l'appel suivant : le cœur des générateurs.
Exemple simple : Chaque yield ligne.strip() de l'export de Nabil.
Ce que ce n'est pas : un return qui termine la fonction.
Réflexe : itérer sans tout charger.
- Décorateur
- Fonction enveloppant une autre pour ajouter un comportement (chrono, log, contrôle), via @decorateur et functools.wraps.
Exemple simple : Le @chrono de Nabil sur ses traitements.
Ce que ce n'est pas : copier le code de mesure dans chaque fonction.
Réflexe : factoriser le transverse en décorateur.
En pratique — Écrire du Python pythonique
- Triez des tuples par 2e élément avec sorted(..., key=lambda t: t[1]) sur les ventes de Nabil.
- Transformez avec map(lambda x: x ** 2, nombres) et filtrez avec filter(lambda x: x % 2 == 0, nombres), puis comparez aux compréhensions.
- Écrivez un générateur à yield produisant les pairs à l'infini ; consommez-en 10 avec break.
- Créez un décorateur @chrono avec functools.wraps qui affiche le temps d'exécution, puis appliquez-le.
Script prêt à dire — présenter son code pythonique (30 s)
« Tri en lambda jetable sur le montant, générateur yield pour l'export volumineux en mémoire constante, décorateur chrono avec wraps pour mesurer sans toucher. Map et filter comparés aux compréhensions au cas par cas. »
À dire en revue ; montrer le @chrono et le yield.
Modèle copier-coller — décorateur chrono + générateur
import time
import functools
def chrono(f):
@functools.wraps(f)
def enveloppe(*args, **kwargs):
debut = time.perf_counter()
resultat = f(*args, **kwargs)
print(f"{f.__name__}: {time.perf_counter() - debut:.2f}s")
return resultat
return enveloppe
@chrono
def total(montants):
return sum(montants)
ventes_triees = sorted([{"v": "Alger", "m": 9}, {"v": "Oran", "m": 4}], key=lambda v: v["m"])
Indépendant
Un @chrono + un tri en lambda suffit à suivre les perfs des rapports.
PME à Alger
Nabil lit l'export caisses de plusieurs Go en générateur sans changer de poste.
Équipe produit
Imposer wraps dans tout décorateur pour garder noms et docs exploitables.
Débutant
Cette semaine : un tri en lambda et un générateur de pairs à l'infini.
✓ Bonnes pratiques
- Réserver lambda au jetable court ; nommer un def dès que ça se réutilise.
- Ajouter functools.wraps dans tout décorateur (nom et doc préservés).
- Passer en yield dès que les données sont grandes ou infinies.
✗ Pièges fréquents
- Écrire une lambda de cinq lignes illisible au lieu d'un def.
- Oublier wraps (nom perdu, introspection cassée).
- Charger un fichier géant en liste au lieu d'un générateur.
Points clés à retenir
- LAMBDA = fonction anonyme d'UNE ligne, en argument (key de sorted…) ; sinon def.
- DÉCORATEUR = enveloppe SANS toucher (@ + wraps) : mesure, log, contrôle factorisés.
- GÉNÉRATEUR (yield) = production À LA DEMANDE, mémoire constante : le choix du grand.
- Ces outils sont PARTOUT dans les frameworks : les manier, c'est lire le code des pros.
Défi 48h — appliquez demain
Écrivez 1 décorateur @chrono avec wraps et 1 générateur yield sur vos propres données.
- Je trie en lambda sans fonction jetable nommée.
- Mon décorateur préserve nom et doc via wraps.
- Mon gros flux passe en générateur, pas en liste.
Questions fréquentes
Quand préférer un générateur à une liste ?
Quand les données sont volumineuses ou infinies : une liste charge tout en mémoire, un générateur produit une valeur à la fois. Pour lire un fichier de plusieurs Go ligne par ligne, le générateur est indispensable ; pour quelques éléments réutilisés, une liste suffit.
Un décorateur, à quoi ça sert concrètement ?
À ajouter un comportement répétitif (chrono, log, contrôle, cache) à plusieurs fonctions sans le recopier partout. Vous en utilisez déjà : @app.route, @staticmethod, @pytest.fixture — même mécanisme, même élégance.
Pourquoi functools.wraps dans un décorateur ?