• 8 hours
  • Easy

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 3/19/24

Créez des fonctions flexibles avec le design pattern Décorateur

Créez des fonctions flexible avec le design pattern Décorateur

Imaginez un instant : en tant qu’ingénieur de renom, vous devez coder une application qui servira à voyager à travers les étoiles, dans le but de trouver une planète habitable… 💫 🌎

Vous avez déjà créé deux fonctions très complexes, qui gèrent le décollage et l'atterrissage de la navette en toutes circonstances.

Il reste cependant une partie du code qui devra s’adapter au trajet et aux événements : le voyage d’une planète à l’autre !

Voici donc les différentes parties de votre code :

  • Monter dans le vaisseau.

  • Décoller d’une planète.

  • Voyager d’une planète à l’autre.

  • Atterrir sur la nouvelle planète.

  • Sortir du vaisseau.

  • Ouvrir la soute.

La partie en gras de votre code est la partie qui peut souvent changer. Vous remarquerez qu’il serait inutile de réécrire à chaque fois le code pour décoller et atterrir : ce serait long et redondant ! Et puis, s’il faut un jour ajouter des modifications à l'atterrissage, il faudrait l’ajouter sur toutes les autres copies du code. 😱

Il vous faut donc un moyen de séparer la responsabilité de la partie qui change (voyager à travers les étoiles) des parties qui ne changent pas (décoller et atterrir).

Ça m’a l’air tout simple ! S’il s’agissait de code Python, pourrions-nous simplement placer les tâches qui se répètent dans des fonctions séparées, et les appeler uniquement lorsque c’est nécessaire ?

En effet, cela nous serait utile  ! Ainsi, nous ne nous retrouverions pas à micromanager exactement la façon dont la fusée décolle, et la façon dont elle atterrit.

En revanche, notre code peut vite devenir redondant en répétant toujours les mêmes actions avant et après la partie dynamique :

def travel_from_to_earth_to_ganymede():
    """Commence l'aventure, de la terre à Ganymède."""
    get_on_the_ship()
    take_off_ship()
 
    # code pour voyager dans l'espace
 
    land_ship()
    exit_the_ship()
    open_the_hold()
 
def travel_from_ganymede_to_kepler():
    """Voyage de Ganymède à Kepler 438 b..."""
    get_on_the_ship()
    take_off_ship()
 
    # code pour voyager dans l'espace
 
    land_ship()
    exit_the_ship()
    open_the_hold()

Mais quel est le problème du code qui se répète ? Au moins, on comprend clairement ce qu’il se passe…

C’est vrai – mais imaginez que nous devions ajouter une étape supplémentaire à chaque voyage, comme  warm_up_the_engine()  (faire chauffer le moteur). On devrait alors l’ajouter à chacune des fonctions de voyage (et les voyages risquent de se multiplier !). Si nous oublions de le faire quelque part, alors un voyage pourrait se retrouver à décoller sans cette procédure, pourtant importante ! 📛

Comment utiliser le pattern Décorateur

Vous l’avez peut-être deviné, il existe un modèle nommé le design pattern Décorateur, qui simplifie ce type de code et nous aide à écrire du code maintenable. En Python, ce pattern est même très bien intégré au langage. 🎉

Il fonctionne ainsi :

Étape 1

Créez une fonction décorateur qui :

  • attend une autre fonction en paramètre,

  • et retourne une variation décorée de cette fonction en retour.

Le décorateur n’est qu’un modificateur de fonction. Il va la transformer pour rajouter des choses avant, et après.

Attendez une seconde – on peut utiliser des fonctions comme arguments d’autres fonctions, en Python ?!

Cela se fait rarement, mais oui ! En Python, tout est objet, et nous pouvons passer des fonctions, des classes et globalement tout ce qui peut être contenu dans une variable, en paramètre de fonction. Nous verrons cela en action sous peu.

Pour notre code, la fonction décorateur prendra nos fonctions de voyages pour les agrémenter automatiquement des procédures de décollage et d’atterrissage.

Étape 2

Chaque changement fait dans le décorateur se répercute donc directement dans les fonctions décorées !

Voici un exemple simple de décorateur écrit en Python :

def decorate_function(function):
    """Cette fonction va générer le décorateur."""
 
    def wrapper():
        """Voici le "vrai" décorateur.
 
        C'est ici que l'on change la fonction de base
        en rajoutant des choses avant et après.
        """
        print("Do something at the start")
 
        result = function()
 
        print("Do something at the end")
 
        return result
 
    return wrapper
 
 
def travelling_through_the_stars():
    """Voyage à travers les étoiles."""
    print("C'est parti pour un long voyage !")
 
 
# ici, nous allons récupérer le retour de "decorate_function",
# qui n'est autre que la fonction "wrapper" !
# Notez que nous pouvons très bien renommer une fonction en
# l'assignant dans une nouvelle variable (ici "wrapper" devient "decorated").
decorated = decorate_function(travelling_through_the_stars)
decorated()  # nous executons la fonction "wrapper"

Donc, dans notre exemple, notre fonction de voyage ne contiendra plus que le code propre au voyage, mais les procédures de décollage et d’atterrissage seront reléguées dans la fonction “wrapper” du décorateur.

Essayons d’appliquer le pattern décorateur à nos voyages  !

La syntaxe Décorateur en Python

L’une des conséquences regrettables des décorateurs, c’est que vous devez créer une fonction, puis la redéfinir avec le décorateur. Il serait préférable de pouvoir tout faire en une seule étape.

Heureusement, c’est possible, grâce à une syntaxe spécifique qui nous simplifie un peu la vie (on parle de sucre syntaxique) :

def decorate_function(function):
    """Cette fonction va générer le décorateur."""
 
    def wrapper():
        """Le "vrai" décorateur."""
        print("Do something at the start")
        result = function()
        print("Do something at the end")
        return result
 
    return wrapper
 
 
@decorate_function  # c'est ici que ça se passe !
def travelling_through_the_stars():
    """Voyage à travers les étoiles."""
    print("C'est parti pour un long voyage !")
 
 
# la fonction est directement décorée, et s'utilise comme telle, comme si rien
# comme si rien n'avait changé ! ;)
travelling_through_the_stars()

La syntaxe  @decorate_function  dit à l’interpréteur Python que cette fonction doit être décorée par la fonction décorateur  decorate_function  . Très concrètement, l’interpréteur Python va exécuter la fonction décorateur lors du lancement du code, et passer la fonction décorée en paramètre de son appel. Exactement comme pour la première méthode. Ici la fonction garde le même nom, mais représente en fait le “wrapper” retourné par le décorateur. 

Améliorons nos voyages grâce à cette idée !

Lien vers la solution

À quoi d’autre un décorateur pourrait-il me servir ?

Imaginons que votre code soit lent à s’exécuter, et que vous vouliez ajouter des fonctionnalités de chronométrage et de logging pour vous aider à trouver quelle partie s’exécute lentement.

Néanmoins, vous ne souhaitez pas modifier directement la fonction pour ce genre de test ! C’est là que les décorateurs entrent en jeu : ils permettent de garder le code de la fonction propre, et spécifique à une tâche seulement.

Vous pourriez donc écrire un décorateur qui démarre et arrête un chronomètre, et logue le résultat à l’écran ou dans un fichier.

Puis, pour chaque fonction que vous voudrez tester, vous pourriez simplement écrire  @my_timing_decorator  sur la ligne qui la précède. Je vous propose ce petit exercice pour finir : saurez-vous implémenter un tel décorateur ? 🕒

from time import time, sleep
 

def calculate_time_spent(function):
    """calcule le temps que met une fonction à s'executer."""
 
    # notez *args et **kwargs. Ce sont des paramètres dynamiques
    # qui permet au décorateur de s'adapter à tout type de fonction !
    # N'hésitez pas à vous documenter sur l'unpacking pour en apprendre
    # davantage.
    def wrapper(*args, **kwargs):
        """Décore la fonction avec un calcul du temps."""
        # retourne le temps en secondes depuis le 01/01/1970.
        # On appelle cela le temps "epoch".
        start = time()

        result = function()

        # mettez ici votre code. Il s'agit de faire la différence entre
        # 2 temps "epochs", celui qui est gardé dans "start", et celui qui
        # sera gardé dans votre variable 'end'. ;)
        end = # ...
        time_spent = # …
        print(f"secondes passées: {time_spent:.2f}")
 
        return result
 
    return wrapper
 

# n'oubliez pas de décorer la fonction !
def calculate_the_trajectory():
    """Calcule la trajectoire du vaisseau."""
    print("Calcul en cours...")
    sleep(3)  # on met le programme en pause pendant 3 secondes !
    print("Calcul terminé !")
 

calculate_the_trajectory()

Lien de la solution

En résumé

  • En Python, les fonctions sont des objets comme les autres : elles peuvent donc être passées à d’autres fonctions et en sortir comme n’importe quelle autre valeur.

  • Le design pattern Décorateur fournit une façon de modifier une fonction, souvent en ajoutant des fonctionnalités avant et après son exécution.

  • Il peut être utile lorsque plusieurs fonctions similaires ont des fonctionnalités centrales différentes, mais des fonctionnalités partagées significatives.

  • Il peut aussi être utile lorsque vous ne souhaitez pas modifier le code interne d’une fonction, pour pouvoir la réutiliser de différentes manières.

  • La syntaxe  @decorate_function  simplifie l’écriture de code impliquant des décorateurs.

Maintenant que nous avons vu des design patterns assez petits, retrouvez-moi au chapitre suivant pour explorer comment structurer une application entière avec le pattern d’architecture Model-View-Controller, ou Modèle-Vue-Contrôleur.

Example of certificate of achievement
Example of certificate of achievement