Cours
Un décorateur est un modèle de conception en Python qui permet à un utilisateur d'ajouter de nouvelles fonctionnalités à un objet existant sans en modifier la structure. Les décorateurs sont généralement appliqués aux fonctions et jouent un rôle crucial dans l'amélioration ou la modification du comportement des fonctions. Traditionnellement, les décorateurs sont placés avant la définition de la fonction que vous souhaitez décorer. Dans ce tutoriel, nous allons montrer comment utiliser efficacement les décorateurs dans les fonctions Python.
En Python, les fonctions sont des citoyens de première classe. Cela signifie qu'ils supportent des opérations telles que la transmission en tant qu'argument, le retour d'une fonction, la modification et l'affectation à une variable. Cette propriété est cruciale car elle permet de traiter les fonctions comme n'importe quel autre objet dans Python, ce qui permet une plus grande flexibilité dans la programmation.
Pour exécuter facilement vous-même tous les exemples de code de ce tutoriel, vous pouvez créer gratuitement un classeur DataLab dans lequel Python est préinstallé et qui contient tous les exemples de code. Pour plus de pratique sur les décorateurs, consultez cet exercice pratique de DataCamp.
Apprenez Python à partir de zéro
Affectation de fonctions à des variables
Pour commencer, nous créons une fonction qui ajoutera un à un nombre chaque fois qu'elle sera appelée. Nous assignerons ensuite la fonction à une variable et utiliserons cette variable pour appeler la fonction.
def plus_one(number):
return number + 1
add_one = plus_one
add_one(5)
6
Définition de fonctions à l'intérieur d'autres fonctions
Ensuite, nous allons illustrer comment vous pouvez définir une fonction à l'intérieur d'une autre fonction en Python. Restez avec moi, nous allons bientôt découvrir en quoi tout cela est pertinent pour créer et comprendre les décorateurs en Python.
def plus_one(number):
def add_one(number):
return number + 1
result = add_one(number)
return result
plus_one(4)
5
Passer des fonctions comme arguments à d'autres fonctions
Les fonctions peuvent également être transmises en tant que paramètres à d'autres fonctions. Nous allons l'illustrer ci-dessous.
def plus_one(number):
return number + 1
def function_call(function):
number_to_add = 5
return function(number_to_add)
function_call(plus_one)
6
Fonctions renvoyant à d'autres fonctions
Une fonction peut également générer une autre fonction. Nous allons le montrer ci-dessous à l'aide d'un exemple.
def hello_function():
def say_hi():
return "Hi"
return say_hi
hello = hello_function()
hello()
'Hi'
Comprendre les fermetures
Python permet à une fonction imbriquée d'accéder à la portée extérieure de la fonction englobante. Il s'agit d'un concept essentiel dans les décorateurs, connu sous le nom de "fermeture".
Une fermeture en Python est une fonction qui se souvient de l'environnement dans lequel elle a été créée, même après que cet environnement n'est plus actif. Cela signifie qu'une fonction imbriquée peut "refermer" les variables de la portée qui l'entoure et continuer à les utiliser.
Les fermetures sont essentielles pour comprendre les décorateurs, car ceux-ci reposent sur la capacité d'une fonction enveloppante imbriquée à accéder à l'état de la fonction décoratrice qui l'entoure et à le modifier.
Exemple de fermeture :
def outer_function(message):
def inner_function():
print(f"Message from closure: {message}")
return inner_function
closure_function = outer_function("Hello, closures!")
closure_function()
# Output: Message from closure: Hello, closures!
Dans cet exemple :
inner_function
est une fermeture car elle accède àmessage
, une variable de la portée qui l'entoure (outer_function
).- Même si l'exécution de
outer_function
est terminée,inner_function
conserve l'accès àmessage
.
Lorsque vous créez un décorateur, la fonction enveloppante (à l'intérieur du décorateur) est une fermeture. Il conserve l'accès à la fonction décorée et à tout état ou argument supplémentaire défini dans la fonction décoratrice. Par exemple :
def simple_decorator(func):
def wrapper():
print("Before the function call")
func()
print("After the function call")
return wrapper
@simple_decorator
def greet():
print("Hello!")
greet()
# Output:
# Before the function call
# Hello!
# After the function call
Ici, wrapper
est une fermeture qui se souvient de la fonction greet
et ajoute un comportement avant et après son exécution.
Création de décorateurs
Ces conditions préalables étant posées, créons un décorateur simple qui convertira une phrase en majuscules. Pour ce faire, nous définissons un "wrapper" à l'intérieur d'une fonction fermée. Comme vous pouvez le constater, cette fonction est très similaire à la fonction à l'intérieur d'une autre fonction que nous avons créée plus tôt.
def uppercase_decorator(function):
def wrapper():
func = function()
make_uppercase = func.upper()
return make_uppercase
return wrapper
Notre fonction décoratrice prend une fonction en argument, et nous allons donc définir une fonction et la passer à notre décorateur. Nous avons appris précédemment que nous pouvions assigner une fonction à une variable. Nous utiliserons cette astuce pour appeler notre fonction de décorateur.
def say_hi():
return 'hello there'
decorate = uppercase_decorator(say_hi)
decorate()
'HELLO THERE'
Cependant, Python nous offre un moyen beaucoup plus simple d'appliquer des décorateurs. Il suffit d'utiliser le symbole @ devant la fonction que l'on souhaite décorer. Nous allons le montrer en pratique ci-dessous.
@uppercase_decorator
def say_hi():
return 'hello there'
say_hi()
'HELLO THERE'
Appliquer plusieurs décorateurs à une seule fonction
Nous pouvons utiliser plusieurs décorateurs pour une seule fonction. Cependant, les décorateurs seront appliqués dans l'ordre dans lequel nous les avons appelés. Nous allons définir ci-dessous un autre décorateur qui divise la phrase en une liste. Nous allons ensuite appliquer les décorateurs uppercase_decorator
et split_string
à une seule fonction.
import functools
def split_string(function):
@functools.wraps(function)
def wrapper():
func = function()
splitted_string = func.split()
return splitted_string
return wrapper
@split_string
@uppercase_decorator
def say_hi():
return 'hello there'
say_hi()
['HELLO', 'THERE']
Le résultat ci-dessus montre que l'application des décorateurs se fait de bas en haut. Si nous avions interverti l'ordre, nous aurions constaté une erreur, car les listes n'ont pas d'attribut upper
. La phrase a d'abord été convertie en majuscules, puis divisée en liste.
Note: Lorsque vous empilez des décorateurs, il est courant d'utiliser functools.wraps
pour garantir que les métadonnées de la fonction d'origine sont préservées tout au long du processus d'empilement. Cela permet de maintenir la clarté et la cohérence dans le débogage et la compréhension des propriétés de la fonction décorée.
Acceptation d'arguments dans les fonctions du décorateur
Il peut arriver que nous ayons besoin de définir un décorateur qui accepte des arguments. Pour ce faire, nous transmettons les arguments à la fonction "wrapper". Les arguments seront ensuite transmis à la fonction décorée au moment de l'appel.
def decorator_with_arguments(function):
def wrapper_accepting_arguments(arg1, arg2):
print("My arguments are: {0}, {1}".format(arg1,arg2))
function(arg1, arg2)
return wrapper_accepting_arguments
@decorator_with_arguments
def cities(city_one, city_two):
print("Cities I love are {0} and {1}".format(city_one, city_two))
cities("Nairobi", "Accra")
My arguments are: Nairobi, Accra
Cities I love are Nairobi and Accra
Note : Il est essentiel de veiller à ce que le nombre d'arguments du décorateur (arg1, arg2
dans cet exemple) corresponde au nombre d'arguments de la fonction enveloppée (cities
dans cet exemple). Cet alignement est essentiel pour éviter les erreurs et garantir un fonctionnement correct lors de l'utilisation de décorateurs avec des arguments.
Définition des décorateurs d'usage général
Pour définir un décorateur général pouvant être appliqué à n'importe quelle fonction, nous utilisons args
et **kwargs
. args
et **kwargs
collectent tous les arguments de position et de mot-clé et les stockent dans les variables args et kwargs. args
et kwargs
nous permettent de passer autant d'arguments que nous le souhaitons lors de l'appel d'une fonction.
def a_decorator_passing_arbitrary_arguments(function_to_decorate):
def a_wrapper_accepting_arbitrary_arguments(*args,**kwargs):
print('The positional arguments are', args)
print('The keyword arguments are', kwargs)
function_to_decorate(*args)
return a_wrapper_accepting_arbitrary_arguments
@a_decorator_passing_arbitrary_arguments
def function_with_no_argument():
print("No arguments here.")
function_with_no_argument()
The positional arguments are ()
The keyword arguments are {}
No arguments here.
Voyons comment nous pouvons utiliser le décorateur en utilisant des arguments positionnels.
@a_decorator_passing_arbitrary_arguments
def function_with_arguments(a, b, c):
print(a, b, c)
function_with_arguments(1,2,3)
The positional arguments are (1, 2, 3)
The keyword arguments are {}
1 2 3
Les arguments de type mot-clé sont transmis à l'aide de mots-clés. Vous trouverez ci-dessous une illustration de ce phénomène.
@a_decorator_passing_arbitrary_arguments
def function_with_keyword_arguments():
print("This has shown keyword arguments")
function_with_keyword_arguments(first_name="Derrick", last_name="Mwiti")
The positional arguments are ()
The keyword arguments are {'first_name': 'Derrick', 'last_name': 'Mwiti'}
This has shown keyword arguments
Note : L'utilisation de **kwargs
dans le décorateur lui permet de traiter les arguments de type mot-clé. Cela rend le décorateur polyvalent et lui permet de gérer une grande variété de types d'arguments lors des appels de fonctions.
Passer des arguments au décorateur
Voyons maintenant comment passer des arguments au décorateur lui-même. Pour ce faire, nous définissons un créateur de décorateur qui accepte des arguments, puis nous définissons un décorateur à l'intérieur de celui-ci. Nous définissons ensuite une fonction enveloppante à l'intérieur du décorateur, comme nous l'avons fait précédemment.
def decorator_maker_with_arguments(decorator_arg1, decorator_arg2, decorator_arg3):
def decorator(func):
def wrapper(function_arg1, function_arg2, function_arg3) :
"This is the wrapper function"
print("The wrapper can access all the variables\n"
"\t- from the decorator maker: {0} {1} {2}\n"
"\t- from the function call: {3} {4} {5}\n"
"and pass them to the decorated function"
.format(decorator_arg1, decorator_arg2,decorator_arg3,
function_arg1, function_arg2,function_arg3))
return func(function_arg1, function_arg2,function_arg3)
return wrapper
return decorator
pandas = "Pandas"
@decorator_maker_with_arguments(pandas, "Numpy","Scikit-learn")
def decorated_function_with_arguments(function_arg1, function_arg2,function_arg3):
print("This is the decorated function and it only knows about its arguments: {0}"
" {1}" " {2}".format(function_arg1, function_arg2,function_arg3))
decorated_function_with_arguments(pandas, "Science", "Tools")
The wrapper can access all the variables
- from the decorator maker: Pandas Numpy Scikit-learn
- from the function call: Pandas Science Tools
and pass them to the decorated function
This is the decorated function, and it only knows about its arguments: Pandas Science Tools
Débogage des décorateurs
Comme nous l'avons remarqué, les décorateurs enveloppent les fonctions. Le nom de la fonction d'origine, sa docstring et la liste des paramètres sont tous masqués par la fermeture de l'enveloppe : Par exemple, lorsque nous essayons d'accéder aux métadonnées de decorated_function_with_arguments
, nous voyons les métadonnées de la fermeture du wrapper. Cela pose un problème lors du débogage.
decorated_function_with_arguments.__name__
'wrapper'
decorated_function_with_arguments.__doc__
'This is the wrapper function'
Pour résoudre ce problème, Python propose un décorateur functools.wraps
. Ce décorateur copie les métadonnées perdues de la fonction non décorée vers la fermeture décorée. Voyons comment procéder.
import functools
def uppercase_decorator(func):
@functools.wraps(func)
def wrapper():
return func().upper()
return wrapper
@uppercase_decorator
def say_hi():
"This will say hi"
return 'hello there'
say_hi()
'HELLO THERE'
Lorsque nous vérifions les métadonnées de say_hi
, nous remarquons qu'elles font désormais référence aux métadonnées de la fonction et non à celles du wrapper.
say_hi.__name__
'say_hi'
say_hi.__doc__
'This will say hi'
Il est conseillé et de bonne pratique de toujours utiliser functools.wraps
pour définir les décorateurs. Cela vous évitera bien des maux de tête lors du débogage.
Décorateurs basés sur les classes
Si les décorateurs basés sur des fonctions sont courants, Python vous permet également de créer des décorateurs basés sur des classes, qui offrent une plus grande flexibilité et une meilleure maintenabilité, en particulier pour les cas d'utilisation complexes. Un décorateur basé sur une classe est une classe dotée d'une méthode __call__
qui lui permet de se comporter comme une fonction.
class UppercaseDecorator:
def __init__(self, function):
self.function = function
def __call__(self, *args, **kwargs):
result = self.function(*args, **kwargs)
return result.upper()
@UppercaseDecorator
def greet():
return "hello there"
print(greet())
# Output: HELLO THERE
Comment cela fonctionne-t-il ?
- La méthode
__init__
initialise le décorateur avec la fonction à décorer. - La méthode
__call__
est invoquée lorsque la fonction décorée est appelée, ce qui permet au décorateur de modifier son comportement.
Avantages des décorateurs basés sur les classes :
- Décorateurs d'état: Les décorateurs basés sur les classes peuvent maintenir l'état à l'aide de variables d'instance, contrairement aux décorateurs basés sur les fonctions qui nécessitent des fermetures ou des variables globales.
- Lisibilité: Pour les décorateurs complexes, l'encapsulation de la logique dans une classe peut rendre le code plus organisé et plus facile à comprendre.
Exemple de décorateur avec état :
class CallCounter:
def __init__(self, function):
self.function = function
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"Function {self.function.__name__} has been called {self.count} times.")
return self.function(*args, **kwargs)
@CallCounter
def say_hello():
print("Hello!")
say_hello()
say_hello()
# Output:
# Function say_hello has been called 1 times.
# Hello!
# Function say_hello has been called 2 times.
# Hello!
Cas d'utilisation d'un décorateur dans le monde réel : Mise en cache
Le décorateur lru_cache
est un outil intégré à Python qui met en cache les résultats des appels de fonctions coûteux. Cela permet d'améliorer les performances en évitant les calculs redondants pour les entrées répétées.
Exemple :
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(50)) # Subsequent calls with the same argument are much faster
Autres utilisations courantes des décorateurs :
-
Enregistrement : Cursus des appels de fonction, des arguments et des valeurs de retour à des fins de débogage ou d'audit.
-
Authentification: Renforcez le contrôle d'accès dans les applications web comme Flask ou Django.
-
Délai d'exécution : Mesurez et optimisez le temps d'exécution des fonctions pour les tâches critiques.
-
Mécanisme de réessai : Réessayer automatiquement les appels de fonction qui ont échoué, utile dans les opérations de réseau.
-
Validation des entrées : Validez les arguments de la fonction avant son exécution.
Résumé des décorateurs Python
Les décorateurs modifient dynamiquement la fonctionnalité d'une fonction, d'une méthode ou d'une classe sans avoir à utiliser directement des sous-classes ou à modifier le code source de la fonction décorée. L'utilisation de décorateurs en Python garantit également que votre code est DRY (Don't Repeat Yourself). Les décorateurs peuvent être utilisés dans plusieurs cas, par exemple :
- Autorisation dans les frameworks Python tels que Flask et Django
- Enregistrement
- Mesure du temps d'exécution
- Synchronisation
Pour en savoir plus sur les décorateurs Python, consultez la bibliothèque des décorateurs de Python.
Devenez développeur Python
FAQ
Y a-t-il des considérations de performance à prendre en compte lors de l'utilisation de décorateurs ?
Oui, les décorateurs peuvent ajouter de la surcharge parce qu'ils introduisent des appels de fonction supplémentaires. Lorsque les performances sont critiques, il est important de prendre en compte cette surcharge, en particulier si la fonction décorée est appelée fréquemment dans un contexte sensible aux performances.
Les décorateurs peuvent-ils être utilisés avec des méthodes de classe, et si oui, comment ?
Oui, les décorateurs peuvent être appliqués aux méthodes de classe, tout comme les fonctions ordinaires. Le décorateur reçoit la méthode en argument et renvoie une nouvelle méthode ou une version modifiée de la méthode. Cette fonction est généralement utilisée pour la journalisation, le contrôle d'accès ou l'application de conditions préalables.
Comment les décorateurs peuvent-ils être utilisés à des fins de journalisation ?
Les décorateurs peuvent être utilisés pour enregistrer les appels de fonction, leurs arguments et leurs valeurs de retour en enveloppant l'exécution de la fonction avec du code qui enregistre ces détails dans un système d'enregistrement. Cela facilite le traçage et le débogage.
Quelle est la signification du symbole @ dans les décorateurs ?
Le symbole@
est un sucre syntaxique de Python qui simplifie l'application d'un décorateur à une fonction. Il vous permet d'appliquer un décorateur à une fonction directement au-dessus de sa définition, ce qui rend le code plus propre et plus lisible.
Un décorateur peut-il modifier la valeur de retour d'une fonction, et comment cela fonctionne-t-il ?
Oui, un décorateur peut modifier la valeur de retour d'une fonction en modifiant l'instruction de retour dans la fonction enveloppante. Par exemple, il peut transformer le type de données de sortie, le formater ou ajouter un traitement supplémentaire avant de renvoyer le résultat final.
Comment Python gère-t-il la portée des variables lorsqu'une fonction imbriquée accède à une variable à partir de la fonction qui l'entoure ?
Python utilise une règle de portée LEGB (Local, Enclosing, Global, Built-in). Dans le cas des fonctions imbriquées, la fonction imbriquée peut accéder aux variables de la fonction qui l'entoure, ce qui permet des fermetures où la fonction interne conserve l'accès aux variables de la fonction externe même après que cette dernière a fini de s'exécuter.