Machine Learning
2020-10-29
4 min
Équipe Blent

Les décorateurs de fonctions sous Python

Tu as peut-être déjà remarqué le symbole @ à proximité des fonctions avec Python. Il s'agit de décorateurs de fonctions. Mais à quoi peuvent-ils servir ? En quelques lignes de code, nous allons voir comment créer des décorateurs de fonctions Python qui peuvent nous faciliter la vie.

Les décorateurs de fonctions sous Python

Tu as peut-être déjà remarqué le symbole @ à proximité des fonctions avec Python. Il s'agit de décorateurs de fonctions.

Mais à quoi peuvent-ils servir ? En quelques lignes de code, nous allons voir comment créer des décorateurs de fonctions Python qui peuvent nous faciliter la vie.

C'est quoi un décorateur de fonction ?

Prenons un cas concret : on souhaite mesurer la durée d'exécution d'une fonction. Pour cela, nous importons la librairie time et avec le code suivant, nous allons mesurer le temps d'exécution de la fonction pause.

import time

def pause():
    print("Début ...")
    time.sleep(2)  # Pause de 2 secondes
    print("Fin !")

start_time = time.time()  # Temps avant exécution
pause()
end_time = time.time()  # Temps après exécution
print("Durée d'exécution : {:1.3}s".format(end_time - start_time))

Le problème, c'est qu'à chaque fois que l'on souhaitera mesurer la durée d'exécution d'une fonction, il faudra copier-coller l'affectation aux variables start_time et end_time, puis afficher la durée avec un print.

À lire aussi : découvrez notre formation MLOps

C'est ici que les décorateurs de fonctions vont nous être utile. Elles viennent englober une fonction et vont permettrent, par exemple, d'exécuter des instructions avant ou après l'exécution de la fonction.

def timing(func):
    """
    Mesure le temps d'exécution d'une fonction.
    """
    def wrapper():
        start_time = time.time()
        func()
        end_time = time.time()
        print("Durée d'exécution : {:1.3}s".format(end_time - start_time))

    return wrapper

Analysons ce code.

  • La fonction timing attends un paramètre, la fonction dont nous allons calculer la durée d'exécution.
  • À l'intérieur de timing, nous avons crée une fonction wrapper qui sera renvoyée par la fonction timing.
  • Cette fonction wrapper va calculer le temps avant puis après l'exécution de la fonction et afficher la durée avec un print.

Maintenant, pour mesurer la durée d'exécution de la fonction pause, il suffit d'appeler la fonction timing avec, comme paramètre, la fonction pause.

timing(pause)()

En écrivant timing(pause), j'obtiens la fonction wrapper qui va exécuter à l'intérieur la fonction pause.

⚠️ timing(pause) est une fonction Python, d'où la présence des parenthèses pour l'exécuter.

Mais plutôt que d'utiliser cette écriture à chaque exécution de la fonction pause, un décorateur de fonction va effectuer la même opération.

@timing  # La fonction timing(pause) sera exécuté à chaque appel de la fonction pause
def pause():
    print("Début ...")
    time.sleep(2)  # Pause de 2 secondes
    print("Fin !")

Dorénavant, dès que l'on appelera la fonction pause, la durée d'exécution sera automatiquement calculée et affichée.

pause()

Un décorateur avec des arguments dans la fonction

Une situation qui arrive souvent, c'est lorsque la fonction décorée, en l'occurrence ici la fonction pause, s'attend à avoir des paramètres.

@timing
def pause(t):
    print("Début ...")
    time.sleep(t)  # Pause de t secondes
    print("Fin !")

Le paramètre t indique ici la durée de la pause. Si maintenant, on appelle la fonction pause avec comme argument t=2.

pause(t=2)

Une erreur apparaît ! 😨

Pourquoi y a-t-il une erreur ? Pour rappel, appeler la fonction pause revient à appeler timing(pause). Or, le paramètre spécifié ici n'est pas envoyé à pause mais à timing(pause) (c'est-à-dire au wrapper).

❓ Comment faire pour que les paramètres ne soient pas envoyés au wrapper mais à la fonction pause ?

Il suffit de rajouter des arguments dans la fonction wrapper :

def timing(func):
    """
    Mesure le temps d'exécution d'une fonction.
    """
    def wrapper(*args, **kwargs):
        start_time = time.time()
        func(*args, **kwargs)
        end_time = time.time()
        print("Durée d'exécution : {:1.3}s".format(end_time - start_time))

    return wrapper

Nous avons défini deux arguments dans le wrapper.

À lire aussi : découvrez notre formation MLOps

  • L'argument *args permet de spécifier des paramètres positionnels (selon leur position lors de l'appel de la fonction).
  • L'argument **kwargs permet de spécifier des paramètres nommés (de la forme param=valeur).

Ces arguments sont ré-utilisés lors de l'appel de la fonction func.

Ainsi, en exécutant pause(2), le paramètre 2 sera envoyé directement à la pause pause par l'intermédiaire du wrapper.

@timing
def pause(t):
    print("Début ...")
    time.sleep(t)  # Pause de t secondes
    print("Fin !")

pause(t=2)

Plus aucune difficulté maintenant pour passer des arguments en paramètres avec les décorateurs de fonctions. 🙂