Définir des fonctions de décorateur simples

PythonPythonBeginner
Pratiquer maintenant

This tutorial is from open-source community. Access the source code

💡 Ce tutoriel est traduit par l'IA à partir de la version anglaise. Pour voir la version originale, vous pouvez cliquer ici

Introduction

Dans ce laboratoire (lab), vous allez apprendre ce qu'est un décorateur (decorator) en Python et comment il fonctionne. Les décorateurs sont une fonctionnalité puissante qui vous permet de modifier le comportement d'une fonction sans modifier le code source. Ils sont largement utilisés dans les frameworks et les bibliothèques Python.

Vous allez également apprendre à créer un simple décorateur de journalisation (logging decorator) et à implémenter un décorateur plus complexe pour la validation de fonction. Les fichiers utilisés dans ce laboratoire sont logcall.py, sample.py et validate.py, le fichier validate.py étant modifié.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/BasicConceptsGroup(["Basic Concepts"]) python(("Python")) -.-> python/FunctionsGroup(["Functions"]) python(("Python")) -.-> python/AdvancedTopicsGroup(["Advanced Topics"]) python/BasicConceptsGroup -.-> python/type_conversion("Type Conversion") python/FunctionsGroup -.-> python/function_definition("Function Definition") python/FunctionsGroup -.-> python/scope("Scope") python/AdvancedTopicsGroup -.-> python/decorators("Decorators") subgraph Lab Skills python/type_conversion -.-> lab-132514{{"Définir des fonctions de décorateur simples"}} python/function_definition -.-> lab-132514{{"Définir des fonctions de décorateur simples"}} python/scope -.-> lab-132514{{"Définir des fonctions de décorateur simples"}} python/decorators -.-> lab-132514{{"Définir des fonctions de décorateur simples"}} end

Créer votre premier décorateur

Qu'est-ce qu'un décorateur ?

En Python, les décorateurs (decorators) sont une syntaxe spéciale qui peut être très utile pour les débutants. Ils vous permettent de modifier le comportement des fonctions ou des méthodes. Imaginez un décorateur comme une fonction qui prend une autre fonction en entrée. Elle retourne ensuite une nouvelle fonction. Cette nouvelle fonction étend souvent ou modifie le comportement de la fonction originale.

Les décorateurs sont appliqués en utilisant le symbole @. Vous placez ce symbole suivi du nom du décorateur directement au-dessus de la définition d'une fonction. C'est un moyen simple de dire à Python que vous voulez utiliser le décorateur sur cette fonction particulière.

Créer un simple décorateur de journalisation

Créons un simple décorateur qui enregistre des informations lorsqu'une fonction est appelée. La journalisation (logging) est une tâche courante dans les applications du monde réel, et utiliser un décorateur pour cela est un excellent moyen de comprendre le fonctionnement des décorateurs.

  1. Tout d'abord, ouvrez l'éditeur VSCode. Dans le répertoire /home/labex/project, créez un nouveau fichier nommé logcall.py. Ce fichier contiendra notre fonction de décorateur.

  2. Ajoutez le code suivant à logcall.py :

## logcall.py

def logged(func):
    print('Adding logging to', func.__name__)
    def wrapper(*args, **kwargs):
        print('Calling', func.__name__)
        return func(*args, **kwargs)
    return wrapper

Analysons ce que fait ce code :

  • La fonction logged est notre décorateur. Elle prend une autre fonction, que nous appelons func, en argument. Cette fonction func est celle à laquelle nous voulons ajouter la journalisation.
  • Lorsque le décorateur est appliqué à une fonction, il affiche un message. Ce message nous indique que la journalisation est ajoutée à la fonction du nom donné.
  • À l'intérieur de la fonction logged, nous définissons une fonction interne appelée wrapper. Cette fonction wrapper remplacera la fonction originale.
    • Lorsque la fonction décorée est appelée, la fonction wrapper affiche un message indiquant que la fonction est en cours d'appel.
    • Elle appelle ensuite la fonction originale (func) avec tous les arguments qui lui ont été passés. Les *args et **kwargs sont utilisés pour accepter n'importe quel nombre d'arguments positionnels et de mots-clés.
    • Enfin, elle retourne le résultat de la fonction originale.
  • La fonction logged retourne la fonction wrapper. Cette fonction wrapper sera maintenant utilisée à la place de la fonction originale, ajoutant ainsi la fonctionnalité de journalisation.

Utilisation du décorateur

  1. Maintenant, dans le même répertoire (/home/labex/project), créez un autre fichier nommé sample.py avec le code suivant :
## sample.py

from logcall import logged

@logged
def add(x, y):
    return x + y

@logged
def sub(x, y):
    return x - y

La syntaxe @logged est très importante ici. Elle indique à Python d'appliquer le décorateur logged aux fonctions add et sub. Ainsi, chaque fois que ces fonctions sont appelées, la fonctionnalité de journalisation ajoutée par le décorateur sera exécutée.

Test du décorateur

  1. Pour tester votre décorateur, ouvrez un terminal dans VSCode. Tout d'abord, changez de répertoire pour le répertoire du projet en utilisant la commande suivante :
cd /home/labex/project

Ensuite, lancez l'interpréteur Python :

python3
  1. Dans l'interpréteur Python, importez le module sample et testez les fonctions décorées :
>>> import sample
Adding logging to add
Adding logging to sub
>>> sample.add(3, 4)
Calling add
7
>>> sample.sub(2, 3)
Calling sub
-1
>>> exit()

Notez que lorsque vous importez le module sample, les messages "Adding logging to..." sont affichés. Cela est dû au fait que le décorateur est appliqué lorsque le module est importé. Chaque fois que vous appelez l'une des fonctions décorées, le message "Calling..." est affiché. Cela montre que le décorateur fonctionne comme prévu.

Ce simple décorateur démontre le concept de base des décorateurs. Il enveloppe la fonction originale avec une fonctionnalité supplémentaire (la journalisation dans ce cas) sans modifier le code de la fonction originale. C'est une fonctionnalité puissante en Python que vous pouvez utiliser dans de nombreux scénarios différents.

✨ Vérifier la solution et pratiquer

Créer un décorateur de validation

Dans cette étape, nous allons créer un décorateur (decorator) plus pratique. En Python, un décorateur est un type spécial de fonction qui peut modifier le comportement d'une autre fonction. Le décorateur que nous allons créer va valider les arguments d'une fonction en fonction des annotations de type (type annotations). Les annotations de type sont un moyen de spécifier les types de données attendus pour les arguments d'une fonction et sa valeur de retour. C'est un cas d'utilisation courant dans les applications du monde réel car cela permet de s'assurer que les fonctions reçoivent les bons types d'entrée, ce qui peut prévenir de nombreux bugs.

Comprendre les classes de validation

Nous avons déjà créé un fichier appelé validate.py pour vous, et il contient quelques classes de validation. Les classes de validation sont utilisées pour vérifier si une valeur répond à certains critères. Pour voir ce qu'il y a dans ce fichier, vous devez l'ouvrir dans l'éditeur VSCode. Vous pouvez le faire en exécutant les commandes suivantes dans le terminal :

cd /home/labex/project
code validate.py

Le fichier contient trois classes :

  1. Validator - C'est une classe de base. Une classe de base fournit un cadre général ou une structure que d'autres classes peuvent hériter. Dans ce cas, elle fournit la structure de base pour la validation.
  2. Integer - Cette classe de validateur est utilisée pour s'assurer qu'une valeur est un entier. Si vous passez une valeur non entière à une fonction qui utilise ce validateur, elle lèvera une erreur.
  3. PositiveInteger - Cette classe de validateur s'assure qu'une valeur est un entier positif. Donc, si vous passez un entier négatif ou zéro, elle lèvera également une erreur.

Ajouter le décorateur de validation

Maintenant, nous allons ajouter une fonction décorateur nommée validated au fichier validate.py. Ce décorateur effectuera plusieurs tâches importantes :

  1. Il inspectera les annotations de type d'une fonction. Les annotations de type sont comme de petites notes qui nous indiquent quel type de données la fonction attend.
  2. Il validera les arguments passés à la fonction par rapport à ces annotations de type. Cela signifie qu'il vérifiera si les valeurs passées à la fonction sont du bon type.
  3. Il validera également la valeur de retour de la fonction par rapport à son annotation. Ainsi, il s'assurera que la fonction retourne le type de données qu'elle est censée retourner.
  4. Si la validation échoue, il lèvera des messages d'erreur informatifs. Ces messages vous diront exactement ce qui a mal fonctionné, comme quel argument avait le mauvais type.

Ajoutez le code suivant à la fin du fichier validate.py :

## Add to validate.py

import inspect
import functools

def validated(func):
    sig = inspect.signature(func)

    print(f'Validating {func.__name__} {sig}')

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        ## Bind arguments to the signature
        bound = sig.bind(*args, **kwargs)
        errors = []

        ## Validate each argument
        for name, value in bound.arguments.items():
            if name in sig.parameters:
                param = sig.parameters[name]
                if param.annotation != inspect.Parameter.empty:
                    try:
                        ## Create an instance of the validator and validate the value
                        if isinstance(param.annotation, type) and issubclass(param.annotation, Validator):
                            validator = param.annotation()
                            bound.arguments[name] = validator.validate(value)
                    except Exception as e:
                        errors.append(f'    {name}: {e}')

        ## If validation errors, raise an exception
        if errors:
            raise TypeError('Bad Arguments\n' + '\n'.join(errors))

        ## Call the function
        result = func(*bound.args, **bound.kwargs)

        ## Validate the return value
        if sig.return_annotation != inspect.Signature.empty:
            try:
                if isinstance(sig.return_annotation, type) and issubclass(sig.return_annotation, Validator):
                    validator = sig.return_annotation()
                    result = validator.validate(result)
            except Exception as e:
                raise TypeError(f'Bad return: {e}') from None

        return result

    return wrapper

Ce code utilise le module inspect de Python. Le module inspect nous permet d'obtenir des informations sur des objets en temps réel, comme des fonctions. Ici, nous l'utilisons pour examiner la signature de la fonction et valider les arguments en fonction des annotations de type. Nous utilisons également functools.wraps. C'est une fonction auxiliaire qui conserve les métadonnées de la fonction originale, telles que son nom et sa docstring. Les métadonnées sont comme des informations supplémentaires sur la fonction qui nous aident à comprendre ce qu'elle fait.

Tester le décorateur de validation

Créons un fichier pour tester notre décorateur de validation. Nous allons créer un nouveau fichier appelé test_validate.py et y ajouter le code suivant :

## test_validate.py

from validate import Integer, PositiveInteger, validated

@validated
def add(x: Integer, y: Integer) -> Integer:
    return x + y

@validated
def pow(x: Integer, y: Integer) -> Integer:
    return x ** y

## Test with a class
class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    @property
    def cost(self):
        return self.shares * self.price

    @validated
    def sell(self, nshares: PositiveInteger):
        self.shares -= nshares

Maintenant, nous allons tester notre décorateur dans l'interpréteur Python. Tout d'abord, accédez au répertoire du projet et lancez l'interpréteur Python en exécutant ces commandes dans le terminal :

cd /home/labex/project
python3

Ensuite, dans l'interpréteur Python, nous pouvons exécuter le code suivant pour tester notre décorateur :

>>> from test_validate import add, pow, Stock
Validating add (x: validate.Integer, y: validate.Integer) -> validate.Integer
Validating pow (x: validate.Integer, y: validate.Integer) -> validate.Integer
Validating sell (self, nshares: validate.PositiveInteger) -> <class 'inspect._empty'>
>>>
>>> ## Test with valid inputs
>>> add(2, 3)
5
>>>
>>> ## Test with invalid inputs
>>> add('2', '3')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/labex/project/validate.py", line 75, in wrapper
    raise TypeError('Bad Arguments\n' + '\n'.join(errors))
TypeError: Bad Arguments
    x: Expected <class 'int'>
    y: Expected <class 'int'>
>>>
>>> ## Test valid power
>>> pow(2, 3)
8
>>>
>>> ## Test with negative exponent (produces non - integer result)
>>> pow(2, -1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/labex/project/validate.py", line 83, in wrapper
    raise TypeError(f'Bad return: {e}') from None
TypeError: Bad return: Expected <class 'int'>
>>>
>>> ## Test with a class
>>> s = Stock("GOOG", 100, 490.1)
>>> s.sell(50)
>>> s.shares
50
>>>
>>> ## Test with invalid shares
>>> s.sell(-10)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/labex/project/validate.py", line 75, in wrapper
    raise TypeError('Bad Arguments\n' + '\n'.join(errors))
TypeError: Bad Arguments
    nshares: Expected value > 0
>>> exit()

Comme vous pouvez le voir, notre décorateur validated a réussi à appliquer la vérification de type sur les arguments et les valeurs de retour des fonctions. Cela est très utile car cela rend notre code plus robuste. Au lieu de laisser les erreurs de type se propager plus profondément dans le code et causer des bugs difficiles à trouver, nous les capturons aux limites des fonctions.

✨ Vérifier la solution et pratiquer

Résumé

Dans ce laboratoire (lab), vous avez appris ce que sont les décorateurs (decorators) en Python et comment ils fonctionnent. Vous avez également maîtrisé la création d'un simple décorateur de journalisation (logging decorator) pour ajouter un comportement aux fonctions et construit un décorateur plus complexe pour valider les arguments des fonctions en fonction des annotations de type (type annotations). De plus, vous avez appris à utiliser le module inspect pour analyser les signatures des fonctions et functools.wraps pour conserver les métadonnées des fonctions.

Les décorateurs sont une fonctionnalité puissante de Python qui permet d'écrire un code plus maintenable et réutilisable. Ils sont couramment utilisés dans les frameworks et les bibliothèques Python pour des préoccupations transversales telles que la journalisation, le contrôle d'accès et la mise en cache. Vous pouvez maintenant appliquer ces techniques dans vos propres projets Python pour obtenir un code plus propre et plus maintenable.