Atelier : les fonctions avancées, chez Da Lambda
Ce que ce chapitre apporte 5 points
- Relire et écrire une fonction Python sans hésiter sur la syntaxe.
- Maîtriser les arguments : nommés, par défaut, en nombre variable, et le déballage à l'appel.
- Manipuler une fonction comme une valeur : la ranger dans une variable, la passer en paramètre, la renvoyer.
- Écrire des fermetures, des fonctions qui se souviennent du contexte où elles sont nées.
- Écrire et comprendre un décorateur, qui réunit les trois points précédents.
Bienvenue chez Da Lambda.
Da Lambda est une pizzeria qui ouvre la commande en ligne. Son logiciel de caisse tient dans un script de six cents lignes écrit d'un seul tenant, où le calcul du prix est recopié à onze endroits. Chaque promotion ajoutée par le patron a demandé de retoucher toutes les copies, et la dernière en a oublié deux : pendant une semaine, la calzone était à moitié prix le mardi.
La mission consiste à réécrire le moteur de commande avec des fonctions, puis avec les formes avancées que Python offre : celles qui permettent d'ajouter une promotion, un journal ou un contrôle sans toucher au code qui marche déjà.
Cet atelier vient après les deux chapitres sur les fonctions, le premier et celui qui apprend à bien les écrire, dont il suppose tout acquis : def, return, valeurs par défaut, key= et lambda. Il n'y revient pas, il s'en sert. La phase 0 remet la syntaxe en place, les suivantes ajoutent ce qu'ils n'ont pas posé : le déballage, les fermetures et les décorateurs.
Atelier à trous. Chaque bloc contient des
TODOà compléter : remplacer les points de suspension, puis exécuter. Un bloc s'exécute seul, sans voir les précédents : les données dont il a besoin sont recopiées en tête. Les lignesassertdu bas servent de juge : si aucune ne proteste et que le message final s'affiche, la phase est validée.
Déroulé
| Durée | Phase | Notions |
|---|---|---|
| 10 min | 0. Remise en route | def, return, dictionnaires, f-strings |
| 10 min | 1. Appeler proprement | Arguments nommés uniquement, défaut mutable |
| 15 min | 2. Un nombre d'arguments variable | *args, **kwargs, déballage |
| 20 min | 3. La fonction est une valeur | Fonction rangée, passée, choisie dans un dictionnaire |
| 15 min | 4. Des fonctions qui fabriquent des fonctions | Fermetures, nonlocal |
| 15 min | 5. Les décorateurs | @, functools.wraps |
| 5 min | Vérification | Quiz et synthèse |
Deux défis bonus attendent ceux qui terminent en avance : un décorateur paramétré et un générateur.
Phase 0 : Remise en route
Pour qui vient d'un autre langage, voici ce qui change en Python.
| Ailleurs | En Python |
|---|---|
Accolades { } autour d'un bloc | Deux-points en fin d'en-tête, puis indentation de 4 espaces |
int prix(int x) | def prix(x):, aucun type à déclarer |
null | None |
cond ? a : b | a if cond else b |
&& et ! | and, or, not |
for (i = 0; i < n; i++) | for i in range(n): |
"Total : " + total | f"Total : {total}" |
Table associative, Map | Dictionnaire {"clé": valeur} |
Le bloc suivant rassemble l'essentiel. On le lit, on l'exécute, on vérifie qu'il ne reste aucune ligne mystérieuse.
1.Une fonction contient total * 2 sur sa dernière ligne, sans return. Que vaut f(10) ?
2.Que fait la ligne prix_pizza seule, sans parenthèses ?
3.Que renvoie CARTE["hawaienne"] si la clé n'existe pas ?
Mission 0. Écrire prix_pizza(nom, taille), qui renvoie le prix d'une pizza selon sa taille. Une pizza absente de la carte doit lever une ValueError, pas une KeyError que le serveur ne comprendrait pas.
Correction
Le raise vient en premier, avant tout calcul : une fonction vérifie ce qu'elle reçoit avant de s'en servir, et sort tôt plutôt que d'imbriquer un else.
Le round(..., 2) n'est pas cosmétique. 11.5 * 1.3 vaut 14.950000000000001 en virgule flottante, et l'égalité du test échouerait sans lui. Sur des prix, on arrondit au centime, et le plus tard possible.
Phase 1 : Appeler proprement
Python associe les arguments aux paramètres de deux façons : par position, dans l'ordre, ou par nom, dans n'importe quel ordre. Les deux se mélangent, à condition que les positionnels viennent d'abord.
Dans une signature, une étoile seule * sépare les paramètres ordinaires des paramètres nommés uniquement : tout ce qui la suit ne peut être passé que sous la forme nom=valeur.
L'intérêt se voit à l'appel. Dans commander("regina", "L", 2, True), personne ne devine ce que signifie True sans ouvrir la définition. Python refuse donc cet appel :
Reste un piège que le chapitre sur les fonctions a posé et démontré : une valeur par défaut est créée une seule fois, au def. Une liste vide donnée par défaut est donc partagée par tous les appels qui ne fournissent rien, et chaque client hérite de ce que le précédent y a mis. La parade tient en deux lignes : la valeur par défaut est None, et la liste se crée dans le corps de la fonction.
Ici, c'est la pizzeria qui en fait les frais.
Mission 1. Corriger ajouter_supplement pour que chaque appel sans liste reparte d'une liste neuve.
Correction
is None et non == None : on compare une identité, pas une valeur, et certains objets redéfinissent == au point de se prétendre égaux à None.
C'est le remède exact au piège du bloc précédent. None existe une fois pour toutes et ne peut pas être modifié ; la liste, elle, naît à chaque appel qui en a besoin. Alice et Bob repartent chacun avec la leur.
Pourquoi def f(supplements=[]) est-il dangereux ?
Phase 2 : Un nombre d'arguments variable
Un ticket de caisse peut compter une pizza ou douze. Plutôt que d'exiger une liste, la fonction peut accepter autant d'arguments qu'on veut.
Dans une signature, *args ramasse tous les arguments positionnels en trop dans un tuple, et **kwargs ramasse tous les arguments nommés en trop dans un dictionnaire. Les noms args et kwargs sont une convention : ce sont les étoiles qui comptent.
print est déclarée ainsi : print(*objets, sep=" ", end="\n"). C'est pour cela qu'elle accepte n'importe quel nombre de valeurs, et que sep doit être nommé : il vient après *objets.
L'ordre complet d'une signature est toujours le même : def f(obligatoire, avec_defaut=0, *args, nomme_seul, **kwargs).
Les étoiles fonctionnent aussi dans l'autre sens, à l'appel : elles déballent une séquence en arguments positionnels, un dictionnaire en arguments nommés. C'est ainsi qu'on branche une fonction sur des données reçues d'ailleurs, comme une commande arrivée en JSON.
Si le JSON contient une clé que la fonction ne connaît pas, Python lève une TypeError. C'est une protection : une faute de frappe dans le nom d'un champ se voit tout de suite.
Mission 2. Écrire total_ticket, qui accepte autant de prix qu'on veut et une remise nommée, puis creer_pizza, qui décrit une pizza par un dictionnaire. Enfin, appeler creer_pizza en déballant les données fournies.
Correction
*prix rassemble les arguments positionnels dans un tuple, d'où le list(garnitures) : le test attend une liste, et un tuple ne lui est pas égal.
total_ticket() sans aucun prix rend 0, parce que sum(()) vaut 0. Une fonction qui traite naturellement le cas vide est une fonction de moins à protéger.
L'étoile change de sens selon l'endroit : dans la signature elle rassemble, à l'appel elle déballe. creer_pizza("tomate", *garnitures, **options) passe trois garnitures et deux options nommées, exactement comme si on les avait écrites une à une.
Dans def f(*args, **kwargs), quels sont les types de args et kwargs ?
Phase 3 : La fonction est une valeur
En Python, une fonction est un objet comme un autre, au même titre qu'un nombre ou une liste. happy_hour désigne la fonction, happy_hour(25) l'appelle. On peut donc la ranger dans une variable, dans un dictionnaire, ou la passer en paramètre.
Le jour où le patron invente une promotion, on écrit une fonction et on l'ajoute au dictionnaire. encaisser ne change pas d'une ligne. Pour qui connaît les patrons de conception, c'est le patron Stratégie, sans interface ni classe.
L'erreur la plus fréquente consiste à mettre des parenthèses là où il n'en faut pas :
happy_hour() tente d'appeler la fonction tout de suite, sans argument, avant même d'entrer dans encaisser. On voulait lui confier la fonction, pas son résultat.
Ce que les chapitres sur les fonctions ont laissé : lambda et key=
Les deux formes qui servent ici viennent du chapitre sur l'écriture des fonctions : lambda paramètres: expression fabrique une fonction sans nom, réduite à une expression, et sorted, min et max acceptent un paramètre key, la fonction qui dit sur quoi comparer. Un seul geste s'y ajoute, et il sert dès la mission : plusieurs critères à la fois, en rendant un tuple.
Bob et David attendent tous deux depuis 25 minutes : c'est le second critère qui les départage, et le tuple se compare élément par élément, comme deux mots dans un dictionnaire.
map(f, liste) et filter(f, liste) existent et prennent une fonction. En Python, on leur préfère presque toujours une compréhension, plus lisible :[round(p * 0.8, 2) for p in prix] plutôt que list(map(lambda p: round(p * 0.8, 2), prix)).
Mission 3. Trier la carte, écrire une fonction qui applique n'importe quelle promotion à toute la carte, et écrire une promotion sous forme de lambda.
Correction
key= reçoit une fonction, que sorted appelle sur chaque élément pour savoir sur quoi comparer. Ici couple[1], le prix. Sans quoi le tri se ferait sur le nom.
appliquer_promo construit un nouveau dictionnaire par compréhension au lieu de modifier celui qu'on lui passe. C'est ce que vérifie la dernière assertion, et c'est la règle : une fonction qui calcule ne touche pas à ses données d'entrée.
Le plancher s'écrit max(p - 2, 7), pas une condition : « jamais moins de 7 » est un maximum.
1.Pourquoi sorted(commandes, key=prix()) échoue-t-il ?
2.Qu'écrit-on à la place de @journaliser au-dessus de def f ?
3.À quoi sert functools.wraps dans un décorateur ?
Phase 4 : Des fonctions qui fabriquent des fonctions
Le patron veut des codes promo : moins 10 %, moins 15 %, moins 25 %… Écrire une fonction par pourcentage recréerait exactement les copies qu'on cherche à supprimer. On écrit plutôt une fonction qui fabrique la promotion.
creer_promo(25) a terminé son exécution depuis longtemps quand on appelle moins_25(40). Pourtant promo connaît toujours pourcentage.
Une fermeture (closure) est une fonction définie à l'intérieur d'une autre, qui utilise une variable de la fonction englobante. Cette variable reste vivante tant que la fonction intérieure existe, même après la fin de la fonction englobante.
Modifier la variable mémorisée : nonlocal
Le logiciel doit numéroter les tickets. Une fermeture peut garder un compteur, mais ce premier essai échoue :
C'est la règle de portée des fonctions : affecter une variable dans une fonction en fait une variable locale. numero = numero + 1 tente donc de lire une locale qui n'a pas encore de valeur. Le mot-clé nonlocal indique qu'on parle de la variable de la fonction englobante.
Chaque appel à creer_distributeur_tickets crée son propre numero : la caisse et la livraison ne se marchent pas dessus. Une fermeture avec état, c'est un objet à une seule méthode, sans classe.
[lambda t: t * (1 - p / 100) for p in (10, 20, 30)] semble créer trois promotions. Les trois appliquent pourtant 30 % : elles lisent toutes la même variable p, au moment de l'appel, quand la boucle est finie depuis longtemps.La correction propre passe par une fabrique :
[creer_promo(p) for p in (10, 20, 30)]. Chaque appel à creer_promo crée son propre pourcentage.
Mission 4. Écrire une fabrique de tarifs de livraison, puis une fabrique de fours qui refusent les pizzas une fois pleins.
Correction
tarif se souvient de frais_fixes et de prix_km après que la fabrique a rendu la main. C'est une fermeture : la fonction emporte le contexte où elle est née, et deux appels à la fabrique donnent deux fonctions aux tarifs distincts.
nonlocal est indispensable dans enfourner. Sans lui, enfournees += 1 ferait de enfournees une variable locale à enfourner, et pas seulement à partir de cette ligne : affecter un nom quelque part dans une fonction le rend local partout dans cette fonction. Le if enfournees >= capacite d'au-dessus lirait donc une locale encore vide, et le tout premier appel échouerait sur l'UnboundLocalError de la section précédente. nonlocal dit : cette variable est celle d'au-dessus, pas une nouvelle.
La dernière assertion vérifie que chaque four a son propre compteur. C'est ce qui distingue une fermeture d'une variable globale, et ce qui la rend utilisable.
Phase 5 : Les décorateurs
Des tickets faux sont sortis hier soir. Le patron veut un journal de chaque appel aux fonctions de caisse : les arguments reçus, le résultat rendu. Il y a quinze fonctions. Ajouter deux print dans chacune, puis les retirer, puis les remettre au prochain incident ? Non.
Tout ce qu'il faut a déjà été vu. On écrit une fonction qui reçoit une fonction, et renvoie une version enveloppée.
Un décorateur réunit les trois phases précédentes.
journaliser reçoit une fonction et en renvoie une autre (phase 3).
enveloppe est une fermeture qui se souvient de fonction (phase 4).
enveloppe accepte n'importe quels arguments avec *args, **kwargs et les retransmet tels quels (phase 2). C'est ce qui lui permet d'envelopper n'importe quelle fonction.
La ligne prix_pizza = journaliser(prix_pizza) est si courante que Python lui donne une syntaxe dédiée.
Écrire @journaliser juste au-dessus de def prix_pizza(...) équivaut exactement à écrire prix_pizza = journaliser(prix_pizza) juste après la définition. Un décorateur est donc simplement une fonction qui prend une fonction et en renvoie une.
Il reste un défaut. La fonction enveloppée a perdu son identité : son nom et sa docstring sont ceux de enveloppe. Les messages d'erreur, help() et les outils de test s'y trompent. functools.wraps recopie l'identité de la fonction d'origine.
La règle est simple : tout décorateur commence par @functools.wraps(fonction) sur son enveloppe.
Mission 5. Écrire un décorateur qui chronomètre la fonction décorée, puis un décorateur qui refuse tout argument numérique négatif ou nul.
Correction
Un décorateur réunit les trois phases précédentes : il reçoit une fonction (phase 3), en fabrique une autre par fermeture (phase 4), et l'enveloppe accepte *args, **kwargs pour relayer n'importe quel appel (phase 2). @chronometrer n'est qu'une écriture abrégée de preparer_patons = chronometrer(preparer_patons).
resultat doit être renvoyé. L'oublier est l'erreur la plus fréquente : la fonction décorée rend alors None, le chronomètre s'affiche quand même, et rien ne signale le problème avant que le résultat serve.
isinstance(valeur, (int, float)) filtre les nombres, sinon la comparaison porterait sur le nom de la pizza et lèverait un TypeError. Et il faut parcourir args et kwargs.values() : le test passe une quantité négative par position, puis un prix nul par nom.
Sans functools.wraps, commander.__name__ vaudrait "enveloppe". Le décorateur remplace la fonction, et l'identité de l'originale (nom, docstring, signature) disparaîtrait des traces d'erreur et de la documentation.
Une fonction porte @journaliser puis, sur la ligne suivante, @chronometrer. Qu'obtient-on ?
Bonus
Bonus 1 : un décorateur qui prend des paramètres
L'offre « pizza du jour » est limitée à un nombre de clients qui change chaque jour. On veut écrire @limiter(3). Comme @ attend un décorateur, limiter(3) doit renvoyer un décorateur : il y a donc trois fonctions imbriquées.
Bonus 2 : une fonction qui rend ses résultats un par un
Une fonction qui contient yield au lieu de return est un générateur. L'appeler n'exécute rien : on obtient un objet qui produit les valeurs à la demande, en reprenant là où il s'était arrêté.
Le four peut cuire quatre pizzas à la fois. Écrire par_fournee, qui produit les commandes par paquets.
Chapitre révisé en octobre 2026 · Une erreur dans ce chapitre ?
Mettre en pratique
*args, lambda, fermetures, décorateurs et générateurs.
- La garniture qui revient3 · Confirmé
- Débogage : trois promos, une seule remise4 · Avancé
- Un ticket de longueur variable2 · Fonctions
- Le client qui attend le plus2 · Fonctions
- Une fabrique de codes promo3 · Confirmé
- Numéroter les tickets4 · Avancé
- Un décorateur qui arrondit3 · Confirmé
- Défi : cuire par fournées4 · Avancé