Fonctions avancées : portée, clôtures et décorateurs
En Python, une fonction n'est pas seulement un bloc de code réutilisable : c'est un objet à part entière. Tu peux la stocker dans une variable, la passer en argument à une autre fonction, ou la retourner comme valeur de retour. Ce paradigme, appelé "fonctions de première classe" (first-class functions), ouvre la porte à des patterns puissants que tu vas explorer tout au long de ce cours.
La portée des variables : LEGB
Quand Python résout le nom d'une variable, il suit une règle précise appelée LEGB : Local, Enclosing, Global, Built-in. Il cherche d'abord dans la portée locale (la fonction courante), puis dans les fonctions englobantes (enclosing), puis au niveau du module (global), et enfin dans les noms natifs de Python (built-in).
Comprendre cet ordre évite la grande majorité des bugs liés aux variables. Le mot-clé global permet de modifier une variable de portée globale depuis une fonction, et nonlocal permet de modifier une variable d'une fonction englobante depuis une fonction imbriquée.
Voici un exemple concret : imagine deux fonctions imbriquées. La fonction interne peut lire la variable de la fonction externe sans problème. Mais si elle veut la modifier, elle doit déclarer nonlocal ma_variable, sinon Python crée une nouvelle variable locale et laisse l'originale intacte. Ce comportement surprend souvent au début, mais il est cohérent avec la règle LEGB.
Les clôtures (closures)
Une clôture est une fonction interne qui "capture" des variables de sa fonction englobante, même après que celle-ci a terminé son exécution. La fonction interne garde une référence à l'environnement dans lequel elle a été créée. C'est un mécanisme fondamental pour créer des fabriques de fonctions (function factories) ou pour encapsuler un état sans recourir à une classe.
Exemple pratique : une fabrique de multiplicateurs.
def fabrique_multiplicateur(facteur):
def multiplier(x):
return x * facteur # facteur est capturé depuis la portee englobante
return multiplier
double = fabrique_multiplicateur(2)
triple = fabrique_multiplicateur(3)
print(double(5)) # 10
print(triple(5)) # 15facteur n'existe plus dans la portée globale, mais double et triple s'en souviennent chacun via leur clôture. Tu peux inspecter les variables capturées avec double.__closure__ et double.__code__.co_freevars.
Les décorateurs : des clôtures appliquées
Un décorateur est une fonction qui prend une fonction en entrée et retourne une nouvelle fonction, généralement enrichie d'un comportement supplémentaire. C'est exactement le pattern de clôture appliqué à la modification de fonctions existantes. La syntaxe @mon_decorateur placée au-dessus d'une définition de fonction est du sucre syntaxique (syntactic sugar) pour ma_fonction = mon_decorateur(ma_fonction).
Un décorateur minimal qui mesure le temps d'exécution illustre bien la structure :
import time
import functools
def chronometre(func):
@functools.wraps(func) # preserve les metadonnees de func
def wrapper(*args, **kwargs):
debut = time.perf_counter()
resultat = func(*args, **kwargs)
fin = time.perf_counter()
print(f"{func.__name__} a pris {fin - debut:.4f}s")
return resultat
return wrapper
@chronometre
def calcul_lourd(n):
return sum(range(n))
calcul_lourd(10_000_000)
# calcul_lourd a pris 0.2341sNote l'usage de functools.wraps : sans lui, calcul_lourd.__name__ retournerait "wrapper" au lieu de "calcul_lourd". C'est un réflexe à avoir systématiquement.
Aller plus loin : décorateurs avec paramètres
Quand un décorateur doit lui-même accepter des arguments (comme @retry(max=3)), on ajoute un niveau d'imbrication supplémentaire : une fonction externe reçoit les paramètres du décorateur, retourne le vrai décorateur, qui lui-même retourne le wrapper. C'est trois niveaux de fonctions imbriquées, mais la logique reste identique.
Maîtriser ce pattern te donnera accès à des outils comme @lru_cache(maxsize=128) ou @app.route("/") en Flask, qui sont tous construits sur ce principe.
Points clés
- En Python, les fonctions sont des objets de première classe : elles se passent, se stockent et se retournent comme n'importe quelle valeur.
- La règle LEGB définit l'ordre dans lequel Python cherche un nom de variable : Local, Enclosing, Global, Built-in.
- Une clôture capture les variables de sa portée englobante et les conserve même après la fin d'exécution de la fonction parente.
- Un décorateur est du sucre syntaxique pour envelopper une fonction dans une autre, en s'appuyant directement sur le mécanisme de clôture.
- Utilise toujours
functools.wrapsdans un décorateur pour préserver les métadonnées (nom, docstring) de la fonction décorée.
Lis la suite, gratuitement
Crée ton compte gratuit : tu ouvres tout le cours et ta progression est conservée.