Aller au contenu principal

Décorateurs et enveloppes d'objets appelables

Un décorateur reçoit un objet et renvoie celui que le nom désignera après décoration ; il peut s’agir du même objet. Un décorateur de fonction renvoie souvent une fonction intermédiaire qui appelle la fonction d’origine. C’est ce que fait traced ci-dessous : à chaque appel, cette fonction intermédiaire affiche le nom de la fonction d’origine, lui transmet les arguments et renvoie son résultat. Les annotations décrivent cette transmission : P représente les paramètres d’origine, R le type de retour, et Callable[P, R] un objet appelable ayant ces paramètres et ce type de retour.

from collections.abc import Callable
from functools import wraps
from typing import ParamSpec, TypeVar

P = ParamSpec("P")
R = TypeVar("R")

def traced(function: Callable[P, R]) -> Callable[P, R]:
@wraps(function)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print(f"calling {function.__qualname__}")
return function(*args, **kwargs)
return wrapper

functools.wraps conserve les métadonnées et expose __wrapped__, ce qui compte pour la documentation, l'introspection, le débogage et les autres décorateurs. Les paramètres de type préservent la signature statique de l'objet enveloppé.

Évaluation et composition

Les expressions de décorateurs sont évaluées lorsque la définition englobante s'exécute, généralement pendant l'import du module. Gardez prévisibles les effets de bord d'enregistrement et de configuration.

Les décorateurs empilés s'appliquent de l'intérieur vers l'extérieur :

@outer
@inner
def operation(): ...

# Équivaut à : operation = outer(inner(operation))

L'ordre change donc le comportement de la mise en cache, des nouvelles tentatives, de l'authentification, des transactions et de la journalisation.

Décorateurs configurés

Un décorateur avec arguments est une fabrique qui renvoie le décorateur réel :

class TransientError(Exception):
pass

def retry(*, attempts: int):
if attempts < 1:
raise ValueError("attempts must be positive")

def decorate(function):
@wraps(function)
def wrapper(*args, **kwargs):
for attempt in range(attempts):
try:
return function(*args, **kwargs)
except TransientError:
if attempt == attempts - 1:
raise
return wrapper
return decorate

Une politique de reprise doit identifier les erreurs récupérables et déterminer si répéter l'exécution est sûr. Un décorateur générique ne peut déduire ces règles métier.

Frontières de conception

  • Faites correspondre la nature de l'objet enveloppé. Une enveloppe synchrone autour d'une fonction asynchrone renvoie une coroutine sans l'attendre ; les générateurs posent des questions de cycle de vie semblables.
  • Ne cachez pas un contrôle de flux important, des E/S ou une suppression large d'exceptions derrière une syntaxe décorative.
  • lru_cache exige des arguments hachables et ne convient que si réutiliser le résultat est sémantiquement sûr. L'état externe mutable et les résultats dépendants du temps violent cette hypothèse.
  • property, classmethod, staticmethod, contextmanager et singledispatch emploient la syntaxe des décorateurs mais définissent des contrats différents. Apprenez chaque API au lieu de traiter tout décorateur comme une enveloppe.

Appeler une fonction décorée

L'exemple typé exige Python 3.10+ pour ParamSpec. Après avoir défini traced, essayez :

@traced
def add(left: int, right: int = 1) -> int:
return left + right

assert add(2, right=3) == 5
assert add.__name__ == "add"
assert add.__wrapped__(2, 3) == 5

Seul le premier appel affiche calling add. *args et **kwargs transmettent les arguments positionnels et nommés ; renvoyer le résultat préserve le contrat de retour. wraps ne vérifie pas les types et ne transforme pas une enveloppe synchrone en fonction asynchrone.

Dans l'exemple de reprise, TransientError est une exception de démonstration : remplacez-la par l'erreur récupérable précise de l'opération. Relancer une fonction génératrice ne relance que la création du générateur, pas les erreurs survenant pendant l'itération. De même, une opération asynchrone échoue lors de son attente. Temporisation progressive et aléa sont utiles aux services distants soumis à la contention, mais pas obligatoires pour toute reprise locale. Déterminez d'abord si répéter une opération partiellement terminée est sûr.

Sources

Explorer les liens

Cette note n’a pas encore de liens vers d’autres notes.

Sur les mêmes sujets (46)

Ouvrir le réseau