ProgrammationDéveloppeur Python

Qu'est-ce que les décorateurs Python avec arguments, comment sont-ils réalisés, où leur utilisation est-elle justifiée et quels sont les points importants lors de l'écriture de vos propres décorateurs avec des paramètres ?

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse

Historique de la question

Les décorateurs sont l'un des outils les plus puissants de Python, permettant de modifier le comportement des fonctions ou des méthodes. Parfois, il est nécessaire de "cercler" non seulement la fonction, mais de configurer le décorateur à l'aide de paramètres (arguments). Ces cas surviennent lors de la journalisation, lors de vérifications de temps, de limitations d'accès, etc.

Problème

Les décorateurs ordinaires ne prennent qu'une seule fonction à encadrer. Lorsque des paramètres doivent être transmis au décorateur, la syntaxe devient plus complexe, ce qui conduit souvent à des erreurs, surtout lors de la nesting de fonctions et du passage de *args/**kwargs.

Solution

Un décorateur avec des paramètres est réalisé en tant que fonction de plus haut ordre : d'abord, la fonction "décorante" externe est appelée avec des arguments, qui crée et retourne le propre décorateur, et le décorateur reçoit ensuite la fonction et retourne l'enveloppe :

def repeat(n): def decorator(func): def wrapper(*args, **kwargs): result = None for _ in range(n): result = func(*args, **kwargs) return result return wrapper return decorator @repeat(3) def greet(name): print(f"Hello, {name}!") greet("Python") # Sortie : Hello, Python! (3 fois)

Caractéristiques clés :

  • Un décorateur avec des paramètres est toujours réalisé via une triple nesting de fonctions.
  • Le wrapper doit retourner le résultat, passe correctement *args/**kwargs.
  • N'oubliez pas functools.wraps pour conserver les métadonnées.

Questions piégeuses.

Pourquoi ne peut-on pas réaliser un décorateur avec des paramètres de la même manière qu'un décorateur ordinaire ?

Si vous utilisez @decorator, Python passe la fonction comme argument au décorateur. Si vous ajoutez des parenthèses (@decorator()), Python appelle d'abord la fonction, et seul son résultat est interprété comme un décorateur.

def deco(func): # décorateur ordinaire : @deco def deco_with_args(arg): # décorateur avec argument : @deco_with_args(arg)

En quoi les décorateurs avec des arguments diffèrent-ils fondamentalement des décorateurs sans arguments au niveau de l'appel ?

Les décorateurs sans arguments reçoivent en entrée une fonction, tandis que les décorateurs avec des arguments ne reçoivent pas une fonction, mais des paramètres, et retournent eux-mêmes un décorateur.

Comment utiliser correctement functools.wraps et pourquoi le faire ?

functools.wraps(func) dans le wrapper conserve le nom, la chaîne de documentation et d'autres métadonnées de la fonction originale, sinon toutes ces informations seront remplacées par le wrapper, ce qui complique le débogage et l'introspection.

import functools def deco_with_args(arg): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper return decorator

Erreurs typiques et anti-patterns

  • Oubli de la nécessité de trois fonctions imbriquées, n'en font que deux (ou même une), le résultat ne sera pas un décorateur.
  • Ne pas passer *args/**kwargs dans le wrapper.
  • Perte des métadonnées de la fonction en raison de l'absence de functools.wraps.

Exemple de la vie réelle

Cas négatif

Un décorateur a été réalisé sans tenir compte des paramètres ou avec un nombre incorrect de fonctions imbriquées :

def log(level): def wrapper(func): # erreur - le wrapper devrait être plus profond print(f"Log: {level}") func() # La fonction n'est pas retournée comme décorateur return wrapper @log("INFO") def action(): print("Work!")

Avantages :

  • A l'air simple

Inconvénients :

  • Le décorateur ne fonctionne pas, la fonction est appelée au moment du décorateur, pas lors de l'appel de action().

Cas positif

Utilisation de functools.wraps et de fonctions imbriquées correctes :

import functools def timer(units): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): import time start = time.time() result = func(*args, **kwargs) end = time.time() if units == 'ms': duration = (end - start) * 1000 else: duration = end - start print(f"Duration: {duration:.4f} {units}") return result return wrapper return decorator @timer('ms') def op(): sum(range(1000)) op()

Avantages :

  • Structure correcte, facile à étendre, journaux propres.

Inconvénients :

  • Plus difficile à lire, surtout pour les débutants.