Les décorateurs Python sont une construction incroyablement utile en Python. En utilisant les décorateurs en Python, nous pouvons modifier le comportement d’une fonction en l’enveloppant dans une autre fonction. Les décorateurs nous permettent d’écrire un code plus propre et de partager des fonctionnalités. Cet article est un tutoriel qui explique non seulement comment utiliser les décorateurs, mais aussi comment les créer.

Connaissances préalables

Le sujet des décorateurs en Python nécessite quelques connaissances de base. Ci-dessous, j’ai listé quelques concepts avec lesquels vous devriez déjà être familier pour que ce tutoriel ait un sens. J’ai également indiqué des liens vers des ressources qui vous permettront d’approfondir ces concepts si nécessaire.

Python de base

Ce sujet est de niveau intermédiaire/avancé. Par conséquent, avant d’essayer d’apprendre, vous devriez déjà être familier avec les bases de Python, telles que les types de données, les fonctions, les objets et les classes.

Vous devez également comprendre certains concepts orientés objet tels que les getters, setters et constructors. Si vous ne connaissez pas le langage de programmation Python, voici quelques ressources pour vous aider à démarrer.

Les fonctions sont des citoyens de première classe

Outre les notions de base de Python, vous devez également connaître ce concept plus avancé de Python. Les fonctions, et à peu près tout le reste en Python, sont des objets comme int ou string. Parce que ce sont des objets, vous pouvez faire un certain nombre de choses avec eux :

  • Vous pouvez passer une fonction comme argument à une autre fonction de la même manière que vous passez une chaîne ou un int comme argument de fonction.
  • Les fonctions peuvent également être retournées par d’autres fonctions, comme vous le feriez pour d’autres valeurs de chaînes ou d’int.
  • Les fonctions peuvent être stockées dans des variables

En fait, la seule différence entre les objets fonctionnels et les autres objets est que les objets fonctionnels contiennent la méthode magique __call__().

Nous espérons qu’à ce stade, vous êtes à l’aise avec les connaissances préalables. Nous pouvons commencer à discuter du sujet principal.

Qu’est-ce qu’un décorateur Python ?

Un décorateur Python est simplement une fonction qui prend une fonction en argument et renvoie une version modifiée de la fonction qui lui a été transmise. En d’autres termes, la fonction foo est un décorateur si elle prend en argument la fonction bar et renvoie une autre fonction baz.

La fonction baz est une modification de bar dans le sens où, dans le corps de baz, il y a un appel à la fonction bar. Cependant, avant et après l’appel à bar, baz peut faire n’importe quoi. C’était un peu long ; voici un peu de code pour illustrer la situation :

# Foo est un décorateur, il prend une autre fonction, bar comme argument
def foo(bar) :

    # Ici, nous créons baz, une version modifiée de bar
    # baz appellera bar mais peut faire n'importe quoi avant et après l'appel de la fonction
    def baz() :

        # Avant d'appeler bar, nous imprimons quelque chose
        print("Quelque chose")

        # Ensuite, nous exécutons bar en faisant un appel de fonction
        bar()

        # Puis nous imprimons quelque chose d'autre après l'exécution de bar
        print("Autre chose")

    # Enfin, foo renvoie baz, une version modifiée de bar
    return baz

Comment créer un décorateur en Python ?

Pour illustrer la façon dont les décorateurs sont créés et utilisés en Python, je vais vous donner un exemple simple. Dans cet exemple, nous allons créer une fonction décoratrice logger qui enregistrera le nom de la fonction qu’elle décore à chaque fois que cette fonction s’exécute.

Pour commencer, nous avons créé la fonction décoratrice. Le décorateur prend en argument la fonction func, qui est la fonction que nous décorons.

def create_logger(func) :
    # Le corps de la fonction va ici

A l’intérieur de la fonction décoratrice, nous allons créer notre fonction modifiée qui enregistrera le nom de func avant d’exécuter func.

# A l'intérieur de create_logger
def modified_func() :
    print("Calling : ", func.__name__)
    func()

Ensuite, la fonction create_logger renverra la fonction modifiée. Par conséquent, l’ensemble de notre fonction create_logger ressemblera à ceci :

def create_logger(func) :
    def modified_func() :
        print("Appel : ", func.__name__)
        func()

    return fonction_modifiée

Nous avons terminé la création du décorateur. La fonction create_logger est un exemple simple de fonction décoratrice. Elle prend en entrée func, qui est la fonction que nous décorons, et renvoie une autre fonction, modified_func. modified_func enregistre d’abord le nom de func, avant d’exécuter func.

Comment utiliser les décorateurs en Python

Pour utiliser notre décorateur, nous utilisons la syntaxe @ comme suit :

@create_logger
def say_hello() :
    print("Hello, World !")

Nous pouvons maintenant appeler say_hello() dans notre script, et la sortie devrait être le texte suivant :

Appel : say_hello
"Hello, World
Screenshot of a program that uses python decorators

Mais que fait le @create_logger? Eh bien, il applique le décorateur à notre fonction say_hello. Pour mieux comprendre ce qui se passe, le code situé juste en dessous de ce paragraphe aboutirait au même résultat que si l’on plaçait @create_logger avant say_hello.

def say_hello() :
    print("Bonjour, le monde !")

say_hello = create_logger(say_hello)

En d’autres termes, une façon d’utiliser les décorateurs en Python est d’appeler explicitement le décorateur en passant par la fonction, comme nous l’avons fait dans le code ci-dessus. L’autre façon, plus concise, consiste à utiliser la syntaxe @.

Dans cette section, nous avons vu comment créer des décorateurs en Python.

Exemples un peu plus compliqués

L’exemple ci-dessus était un cas simple. Il existe des exemples un peu plus complexes, par exemple lorsque la fonction que nous décorons prend des arguments. Une autre situation plus compliquée est celle où vous voulez décorer une classe entière. Je vais couvrir ces deux situations ici.

Lorsque la fonction prend des arguments

Lorsque la fonction que vous décorez prend des arguments, la fonction modifiée doit recevoir les arguments et les passer lorsqu’elle fait l’appel à la fonction non modifiée. Si cela vous semble confus, laissez-moi vous expliquer en termes de foo-bar.

Rappelez-vous que foo est la fonction décoratrice, bar est la fonction que nous décorons et baz est la fonction décorée. Dans ce cas, bar prend les arguments et les passe à baz lors de l’appel à baz. Voici un exemple de code pour consolider le concept :

def foo(bar) :
    def baz(*args, **kwargs) :
        # Vous pouvez faire quelque chose ici
        ___
        # Ensuite, nous appelons bar, en lui passant les args et les kwargs
        bar(*args, **kwargs)
        # Vous pouvez aussi faire quelque chose ici
        ___

    return baz

Si les *args et **kwargs ne vous semblent pas familiers, il s’agit simplement de pointeurs vers les arguments positionnels et les mots-clés, respectivement.

Il est important de noter que baz a accès aux arguments et peut donc les valider avant d’appeler bar.

Par exemple, si nous avions une fonction décoratrice, ensure_string, qui s’assurerait que l’argument passé à une fonction qu’elle décore est une chaîne de caractères, nous l’implémenterions de la manière suivante :

def ensure_string(func) :
    def decorated_func(text) :
        si type(text) n'est pas str :
             raise TypeError('argument to ' func.__name__ ' must be a string.')
        else :
             func(texte)

    return decorated_func

Nous pourrions décorer la fonction say_hello comme suit :

@ensure_string
def say_hello(name) :
    print('Hello', name)

Ensuite, nous pourrions tester le code en utilisant ceci :

say_hello('John') # Devrait fonctionner correctement
say_hello(3) # Devrait lever une exception

Et il devrait produire la sortie suivante :

Bonjour Jean
Traceback (dernier appel le plus récent) :
   File "/home/anesu/Documents/python-tutorial/./decorators.py", line 20, in <module> say hello(3) # devrait lever une exception
   File "/home/anesu/Documents/python-tu$ ./decorators.pytorial/./decorators.py", line 7, in decorated_func raise TypeError('argument to func._name_ must be a string.')
TypeError : l'argument de say hello doit être une chaîne de caractères. $0
Screenshot-from-2023-05-23-02-33-18

Comme prévu, le script a réussi à imprimer “Hello John” parce que “John” est une chaîne de caractères. Il a levé une exception en essayant d’imprimer ‘Hello 3’ parce que ‘3’ n’est pas une chaîne de caractères. Le décorateur ensure_string peut être utilisé pour valider les arguments de toute fonction nécessitant une chaîne de caractères.

Décoration d’une classe

Outre la décoration des fonctions, nous pouvons également décorer les classes. Lorsque vous ajoutez un décorateur à une classe, la méthode décorée remplace la méthode constructeur/initiateur de la classe (__init__).

Pour revenir à foo-bar, supposons que foo soit notre décorateur et que Bar soit la classe que nous décorons, alors foo décorera Bar.__init__. Cela sera utile si nous voulons faire quelque chose avant que les objets de type Bar ne soient instanciés.

Cela signifie que le code suivant

def foo(func) :
    def new_func(*args, **kwargs) :
        print('Doing some stuff before instantiation')
        func(*args, **kwargs)

    return new_func

@foo
classe Bar :
    def __init__(self) :
        print("Dans l'initiateur")

Est équivalent à

def foo(func) :
    def new_func(*args, **kwargs) :
        print('Faire quelques trucs avant l'instanciation')
        func(*args, **kwargs)

    return new_func

classe Bar :
    def __init__(self) :
        print("Dans l'initiateur")


Bar.__init__ = foo(Bar.__init__)

En fait, l’instanciation d’un objet de la classe Bar, défini à l’aide de l’une ou l’autre des deux méthodes, devrait donner le même résultat :

Faire des choses avant l'instanciation
Dans l'initiateur
Screenshot-from-2023-05-23-02-20-38

Exemples de décorateurs en Python

Vous pouvez définir vos propres décorateurs, mais certains sont déjà intégrés à Python. Voici quelques-uns des décorateurs les plus courants que vous pouvez rencontrer en Python :

@staticmethod

La méthode static est utilisée sur une classe pour indiquer que la méthode qu’elle décore est une méthode statique. Les méthodes statiques sont des méthodes qui peuvent être exécutées sans qu’il soit nécessaire d’instancier la classe. Dans l’exemple de code suivant, nous créons une classe Dog avec une méthode statique bark.

classe Chien :
    @staticmethod
    def bark() :
        print('Woof, woof!')

La méthode bark est maintenant accessible de la manière suivante :

Dog.bark()

L’exécution du code produirait le résultat suivant :

Woof, woof !
Screenshot-from-2023-05-23-02-02-07

Comme je l’ai mentionné dans la section “Comment utiliser les décorateurs”, les décorateurs peuvent être utilisés de deux manières. La syntaxe @ étant la plus concise, c’est l’une des deux. L’autre méthode consiste à appeler la fonction du décorateur, en passant en argument la fonction que l’on veut décorer. Cela signifie que le code ci-dessus réalise la même chose que le code ci-dessous :

classe Dog :
    def bark() :
        print('Woof, woof!')

Dog.bark = staticmethod(Dog.bark)

Et nous pouvons toujours utiliser la méthode bark de la même manière

Chien.bark()

Et cela produirait le même résultat

Woof, woof !
Screenshot-from-2023-05-23-02-02-07-1

Comme vous pouvez le constater, la première méthode est plus propre et il est plus évident que la fonction est une fonction statique avant même d’avoir commencé à lire le code. Par conséquent, pour les autres exemples, j’utiliserai la première méthode. Mais n’oubliez pas que la seconde méthode est une alternative.

@classmethod

Ce décorateur est utilisé pour indiquer que la méthode qu’il décore est une méthode de classe. Les méthodes de classe sont similaires aux méthodes statiques en ce sens qu’elles ne nécessitent pas l’instanciation de la classe avant d’être appelées.

Cependant, la principale différence est que les méthodes de classe ont accès aux attributs de la classe, alors que les méthodes statiques n’y ont pas accès. En effet, Python transmet automatiquement la classe comme premier argument à une méthode de classe chaque fois qu’elle est appelée. Pour créer une méthode de classe en Python, nous pouvons utiliser le décorateur classmethod.

classe Chien :
    @classmethod
    def what_are_you(cls) :
        print("Je suis un " cls.__name__ " !")

Pour exécuter le code, il suffit d’appeler la méthode sans instancier la classe :

Dog.what_are_you()

Et le résultat est le suivant :

Je suis un chien !
Screenshot-from-2023-05-23-02-07-18

@property

Le décorateur de propriété est utilisé pour étiqueter une méthode en tant que fixateur de propriété. Revenons à notre exemple du chien. Créons une méthode qui récupère le nom du chien.

classe Dog :
    # Création d'une méthode constructeur qui récupère le nom du chien
    def __init__(self, name) :

         # Création d'une propriété privée name
         # Les doubles traits de soulignement rendent l'attribut privé
         self.__name = nom

    
    @property
    def name(self) :
        return self.__name

Maintenant, nous pouvons accéder au nom du chien comme une propriété normale,

# Création d'une instance de la classe
foo = Dog('foo')

# Accès à la propriété name
print("Le nom du chien est :", foo.name)

Le résultat de l’exécution du code serait le suivant

Le nom du chien est : foo
Screenshot-from-2023-05-23-02-09-42

décorateur @property.setter

Le décorateur property.setter est utilisé pour créer une méthode setter pour nos propriétés. Pour utiliser le décorateur @property.setter, vous devez remplacer property par le nom de la propriété pour laquelle vous créez un setter. Par exemple, si vous créez un setter pour la méthode de la propriété foo, votre décorateur sera @foo.setter. Voici un exemple de chien pour illustrer ce propos :

classe Dog :
    # Création d'une méthode de construction qui prend en compte le nom du chien
    def __init__(self, name) :

         # Création d'une propriété privée name
         # Les doubles traits de soulignement rendent l'attribut privé
         self.__name = nom

    
    @property
    def name(self) :
        return self.__name

    # Création d'un setter pour notre propriété name
    @name.setter
    def name(self, new_name) :
        self.__name = new_name

Pour tester le setter, nous pouvons utiliser le code suivant :

# Création d'un nouveau chien
foo = Chien('foo')

# Changement du nom du chien
foo.name = 'bar'

# Impression du nom du chien à l'écran
print("Le nouveau nom du chien est :", foo.name)

L’exécution du code produira la sortie suivante :

Le nouveau nom du chien est : bar
Screenshot-from-2023-05-23-11-46-25

Importance des décorateurs en Python

Maintenant que nous avons expliqué ce que sont les décorateurs et que vous avez vu quelques exemples de décorateurs, nous pouvons discuter de l’importance des décorateurs en Python. Les décorateurs sont importants pour plusieurs raisons. J’en ai énuméré quelques-unes ci-dessous :

  • Ils permettent de réutiliser le code : Dans l’exemple de la journalisation donné ci-dessus, nous pourrions utiliser @create_logger sur n’importe quelle fonction. Cela nous permet d’ajouter la fonctionnalité de journalisation à toutes nos fonctions sans avoir à l’écrire manuellement pour chaque fonction.
  • Cela vous permet d’écrire du code modulaire : Pour revenir à l’exemple de la journalisation, avec les décorateurs, vous pouvez séparer la fonction principale, dans ce cas say_hello, des autres fonctionnalités dont vous avez besoin, dans ce cas, la journalisation.
  • Ils améliorent les cadres et les bibliothèques : Les décorateurs sont largement utilisés dans les frameworks et les bibliothèques Python pour fournir des fonctionnalités supplémentaires. Par exemple, dans les frameworks web comme Flask ou Django, les décorateurs sont utilisés pour définir des routes, gérer l’authentification ou appliquer des intergiciels à des vues spécifiques.

Le mot de la fin

Les décorateurs sont incroyablement utiles ; vous pouvez les utiliser pour étendre des fonctions sans altérer leur fonctionnalité. C’est utile lorsque vous souhaitez chronométrer les performances des fonctions, enregistrer chaque fois qu’une fonction est appelée, valider les arguments avant d’appeler une fonction ou vérifier les autorisations avant l’exécution d’une fonction. Une fois que vous aurez compris les décorateurs, vous serez en mesure d’écrire du code de manière plus propre.

Ensuite, vous pouvez lire nos articles sur les tuples et l’utilisation de cURL en Python.