Kodokon kodokon.com

Décorateurs et fonctions d'ordre supérieur

Exploitez les fonctions comme objets de première classe pour écrire des décorateurs robustes, paramétrables et transparents.

10 min · 3 questions

Ouvrir cette leçon dans Kodokon

En Python, une fonction est un objet comme un autre : vous pouvez l'affecter à une variable, la passer en argument, la retourner. Une fonction d'ordre supérieur prend ou renvoie une fonction - sorted(data, key=...) en est une. Ajoutez les fermetures (closures), où une fonction interne capture les variables de la fonction englobante et les garde vivantes après son retour, et vous avez tous les ingrédients des décorateurs.

PYTHON
def make_multiplier(factor):
    def multiply(value):
        return value * factor
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)
print(double(10), triple(10))  # 20 30
Une fermeture : multiply capture factor

Un décorateur est une fonction qui reçoit une fonction et en retourne une autre - généralement un wrapper qui ajoute un comportement autour de l'appel. La syntaxe @timed au-dessus de def slow_sum est un sucre strict pour slow_sum = timed(slow_sum). Le wrapper accepte *args, kwargs pour rester agnostique de la signature, et retourne** le résultat de la fonction décorée - l'oublier est une erreur classique qui transforme tous les retours en None.

PYTHON
import functools
import time

def timed(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        delay = time.perf_counter() - start
        print(f"{func.__name__}: {delay:.4f}s")
        return result
    return wrapper

@timed
def slow_sum(n):
    return sum(range(n))

print(slow_sum(1_000_000))
Un décorateur de chronométrage, prêt pour la prod

Pour paramétrer un décorateur (@repeat(times=3)), il faut un niveau de plus : une fabrique qui reçoit les paramètres et retourne le décorateur, lequel retourne le wrapper. Trois fonctions imbriquées - c'est le prix de la flexibilité. Si l'état devient complexe, une classe dotée de __call__ est souvent plus lisible. Réservez les décorateurs aux préoccupations transversales : journalisation, cache, retry, contrôle d'accès. Un décorateur qui modifie la logique métier cache l'essentiel à la lecture.

PYTHON
import functools

def repeat(times):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            result = None
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(times=3)
def greet(name):
    print(f"Bonjour {name}")

greet("Ada")
Décorateur paramétré : fabrique, décorateur, wrapper

Quiz de validation

Vérifiez que vous avez bien retenu les points clés de cette leçon.

  1. À quoi la syntaxe @timed placée au-dessus de def slow_sum est-elle équivalente ?
    • timed(slow_sum())
    • slow_sum = timed(slow_sum)
    • slow_sum.timed = True
  2. Quel problème @functools.wraps résout-il ?
    • Il accélère l'appel de la fonction décorée
    • Il préserve le nom et la docstring de la fonction décorée
    • Il rend le décorateur compatible avec les méthodes de classe
  3. Dans @repeat(times=3), que retourne l'appel repeat(times=3) ?
    • Le résultat de la fonction décorée
    • Un décorateur, prêt à recevoir la fonction
    • Le wrapper final directement
    • Une liste de trois résultats