Expliqué (avec des exemples et des cas d’utilisation)

Les décorateurs Python sont une construction incroyablement utile en Python. En utilisant des 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 du code plus propre et de partager des fonctionnalités. Cet article est un tutoriel non seulement sur la façon d’utiliser les décorateurs, mais aussi sur la façon de les créer.

Connaissances préalables

Le sujet des décorateurs en Python nécessite quelques connaissances de base. Ci-dessous, j’ai énuméré quelques concepts que vous devriez déjà connaître pour donner un sens à ce didacticiel. J’ai également lié des ressources où vous pouvez rafraîchir les concepts si nécessaire.

Python de base

Ce sujet est un sujet plus intermédiaire/avancé. Par conséquent, avant d’essayer d’apprendre, vous devez déjà être familiarisé 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, les setters et les constructeurs. Si vous n’êtes pas familier avec le langage de programmation Python, voici quelques ressources pour vous aider à démarrer.

Les fonctions sont des citoyens de première classe

En plus de Python de base, vous devez également être conscient de ce concept plus avancé en Python. Les fonctions, et à peu près tout le reste en Python, sont des objets comme int ou string. Parce qu’ils sont des objets, vous pouvez faire plusieurs choses avec eux, à savoir :

  • 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 renvoyées par d’autres fonctions comme vous renverriez d’autres valeurs de chaîne ou 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__().

J’espère 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 comme argument et renvoie une version modifiée de la fonction qui a été transmise. En d’autres termes, la fonction foo est un décorateur si elle prend comme 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 à la barre, baz peut tout faire. C’était une bouchée; voici un code pour illustrer la situation:

# Foo is a decorator, it takes in another function, bar as an argument
def foo(bar):

    # Here we create baz, a modified version of bar
    # baz will call bar but can do anything before and after the function call
    def baz():

        # Before calling bar, we print something
        print("Something")

        # Then we run bar by making a function call
        bar()

        # Then we print something else after running bar
        print("Something else")

    # Lastly, foo returns baz, a modified version of bar
    return baz

Comment créer un décorateur en Python ?

Pour illustrer comment les décorateurs sont créés et utilisés en Python, je vais illustrer cela avec un exemple simple. Dans cet exemple, nous allons créer une fonction de décorateur de journalisation qui consignera le nom de la fonction qu’elle décore à chaque exécution de cette fonction.

Pour commencer, nous avons créé la fonction de décorateur. Le décorateur prend func comme argument. func est la fonction que nous décorons.

def create_logger(func):
    # The function body goes here

Dans la fonction de décorateur, nous allons créer notre fonction modifiée qui enregistrera le nom de func avant d’exécuter func.

# Inside create_logger
def modified_func():
    print("Calling: ", func.__name__)
    func()

Ensuite, la fonction create_logger renverra la fonction modifiée. En conséquence, toute notre fonction create_logger ressemblera à ceci :

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

    return modified_function

Nous avons fini de créer le décorateur. La fonction create_logger est un exemple simple de fonction de décorateur. Il prend 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 ceci :

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

Nous pouvons maintenant appeler say_hello() dans notre script, et le résultat devrait être le texte suivant :

Calling:  say_hello
"Hello, World"

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 immédiatement en dessous de ce paragraphe obtiendrait le même résultat que de mettre @create_logger avant say_hello.

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

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 transmettant la fonction comme nous l’avons fait dans le code ci-dessus. L’autre méthode, plus concise, consiste à utiliser la syntaxe @.

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

Exemples un peu plus compliqués

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

Quand 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 transmettre lorsqu’elle appelle finalement la fonction non modifiée. Si cela semble déroutant, laissez-moi vous expliquer en termes foo-bar.

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

def foo(bar):
    def baz(*args, **kwargs):
        # You can do something here
        ___
        # Then we make the call to bar, passing in args and kwargs
        bar(*args, **kwargs)
        # You can also do something here
        ___

    return baz

Si les *args et **kwargs ne vous semblent pas familiers ; ce sont simplement des pointeurs vers les arguments de position et de mot-clé, respectivement.

Il est important de noter que baz a accès aux arguments et peut donc effectuer une validation des arguments avant d’appeler bar.

Un exemple serait si nous avions une fonction décoratrice, Ensure_string qui garantirait que l’argument passé à une fonction qu’elle décore est une chaîne ; nous l’implémenterions ainsi :

def ensure_string(func):
    def decorated_func(text):
        if type(text) is not str:
             raise TypeError('argument to ' + func.__name__ + ' must be a string.')
        else:
             func(text)

    return decorated_func

Nous pourrions décorer la fonction say_hello comme ceci :

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

Ensuite, nous pourrions tester le code en utilisant ceci :

say_hello('John') # Should run just fine
say_hello(3) # Should throw an exception

Et il devrait produire la sortie suivante :

Hello John
Traceback (most recent call last):
   File "/home/anesu/Documents/python-tutorial/./decorators.py", line 20, in <module> say hello(3) # should throw an 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: argument to say hello must be a string. $0

Comme prévu, le script a réussi à imprimer ‘Hello John’ car ‘John’ est une chaîne. Il a lancé une exception lors de la tentative d’impression de ‘Hello 3’ car ‘3’ n’était pas une chaîne. Le décorateur Ensure_string peut être utilisé pour valider les arguments de toute fonction nécessitant une chaîne.

Décorer une classe

En plus de simplement décorer des fonctions, nous pouvons également décorer des 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 en 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 quoi que ce soit 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
class Bar:
    def __init__(self):
        print("In initiator")

Est équivalent à

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

    return new_func

class Bar:
    def __init__(self):
        print("In initiator")


Bar.__init__ = foo(Bar.__init__)

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

Doing some stuff before instantiation
In initiator

Exemples de décorateurs en Python

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

@méthodestatique

La méthode statique 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 s’exécuter 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.

class Dog:
    @staticmethod
    def bark():
        print('Woof, woof!')

Maintenant, la méthode d’écorce est accessible comme suit :

Dog.bark()

Et l’exécution du code produirait la sortie suivante :

Woof, woof!

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 étant l’une des deux. L’autre méthode consiste à appeler la fonction décorateur, en passant la fonction que nous voulons décorer comme argument. Cela signifie que le code ci-dessus réalise la même chose que le code ci-dessous :

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

Dog.bark = staticmethod(Dog.bark)

Et nous pouvons toujours utiliser la méthode de l’écorce de la même manière

Dog.bark()

Et cela produirait le même résultat

Woof, woof!

Comme vous pouvez le voir, la première méthode est plus propre et il est plus évident que la fonction est une fonction statique avant même que vous n’ayez commencé à lire le code. Par conséquent, pour les exemples restants, j’utiliserai la première méthode. Mais n’oubliez pas que la deuxième 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 que la classe soit instanciée avant de pouvoir être appelées.

Cependant, la principale différence est que les méthodes de classe ont accès aux attributs de classe, contrairement aux méthodes statiques. 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 de méthode de classe.

class Dog:
    @classmethod
    def what_are_you(cls):
        print("I am a " + cls.__name__ + "!")

Pour exécuter le code, nous appelons simplement la méthode sans instancier la classe :

Dog.what_are_you()

Et la sortie est :

I am a Dog!

@propriété

Le décorateur de propriété est utilisé pour étiqueter une méthode en tant que setter de propriété. Pour en revenir à notre exemple Dog, créons une méthode qui récupère le nom du Dog.

class Dog:
    # Creating a constructor method that takes in the dog's name
    def __init__(self, name):

         # Creating a private property name
         # The double underscores make the attribute private
         self.__name = name

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

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

# Creating an instance of the class
foo = Dog('foo')

# Accessing the name property
print("The dog's name is:", foo.name)

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

The dog's name is: foo

@ propriété.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 remplacez 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 :

class Dog:
    # Creating a constructor method that takes in the dog's name
    def __init__(self, name):

         # Creating a private property name
         # The double underscores make the attribute private
         self.__name = name

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

    # Creating a setter for our name property
    @name.setter
    def name(self, new_name):
        self.__name = new_name

Pour tester le setter, on peut utiliser le code suivant :

# Creating a new dog
foo = Dog('foo')

# Changing the dog's name
foo.name="bar"

# Printing the dog's name to the screen
print("The dog's new name is:", foo.name)

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

The dogs's new name is: bar

Importance des décorateurs en Python

Maintenant que nous avons couvert ce que sont les décorateurs et que vous avez vu quelques exemples de décorateurs, nous pouvons expliquer pourquoi les décorateurs sont importants en Python. Les décorateurs sont importants pour plusieurs raisons. Certains d’entre eux, j’ai énuméré ci-dessous:

  • Ils permettent la réutilisation du code : dans l’exemple de journalisation donné ci-dessus, nous pourrions utiliser le @create_logger sur n’importe quelle fonction que nous voulons. Cela nous permet d’ajouter une fonctionnalité de journalisation à toutes nos fonctions sans l’écrire manuellement pour chaque fonction.
  • Ils vous permettent d’écrire du code modulaire : Encore une fois, pour revenir à l’exemple de journalisation, avec des 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 frameworks 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 itinéraires, gérer l’authentification ou appliquer un middleware à des vues spécifiques.

Derniers mots

Les décorateurs sont incroyablement utiles ; vous pouvez les utiliser pour étendre les fonctions sans altérer leur fonctionnalité. Ceci 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 pourrez écrire du code de manière plus propre.

Ensuite, vous voudrez peut-être lire nos articles sur les tuples et l’utilisation de cURL en Python.