Apprenez à programmer en Python
Last updated on Monday, September 8, 2014
  • 4 semaines
  • Facile

Ce cours est visible gratuitement en ligne.

Paperback available in this course

Ce cours existe en eBook.

Certificate of achievement available at the end this course

Got it!

Les décorateurs

Nous allons ici nous intéresser à un concept fascinant de Python, un concept de programmation assez avancé. Vous n'êtes pas obligés de lire ce chapitre pour la suite de ce livre, ni même connaître cette fonctionnalité pour coder en Python. Il s'agit d'un plus que j'ai voulu détailler mais qui n'est certainement pas indispensable.

Les décorateurs sont un moyen simple de modifier le comportement « par défaut » de fonctions. C'est un exemple assez flagrant de ce qu'on appelle la métaprogrammation, que je vais décrire assez brièvement comme l'écriture de programmes manipulant… d'autres programmes. Cela donne faim, non ?

Qu'est-ce que c'est ?

Les décorateurs sont des fonctions de Python dont le rôle est de modifier le comportement par défaut d'autres fonctions ou classes. Pour schématiser, une fonction modifiée par un décorateur ne s'exécutera pas elle-même mais appellera le décorateur. C'est au décorateur de décider s'il veut exécuter la fonction et dans quelles conditions.

Mais quel est l'intérêt ? Si on veut juste qu'une fonction fasse quelque chose de différent, il suffit de la modifier, non ? Pourquoi s'encombrer la tête avec une nouvelle fonctionnalité plus complexe ?

Il peut y avoir de nombreux cas dans lesquels les décorateurs sont un choix intéressant. Pour comprendre l'idée, je vais prendre un unique exemple.

On souhaite tester les performances de certaines de nos fonctions, en l'occurence, calculer combien de temps elles mettent pour s'exécuter.

Une possibilité, effectivement, consiste à modifier chacune des fonctions devant intégrer ce test. Mais ce n'est pas très élégant, ni très pratique, ni très sûr… bref ce n'est pas la meilleure solution.

Une autre possibilité consiste à utiliser un décorateur. Ce décorateur se chargera d'exécuter notre fonction en calculant le temps qu'elle met et pourra, par exemple, afficher une alerte si cette durée est trop élevée.

Pour indiquer qu'une fonction doit intégrer ce test, il suffira d'ajouter une simple ligne avant sa définition. C'est bien plus simple, clair et adapté à la situation.

Et ce n'est qu'un exemple d'application.

Les décorateurs sont des fonctions standard de Python mais leur construction est parfois complexe. Quand il s'agit de décorateurs prenant des arguments en paramètres ou devant tenir compte des paramètres de la fonction, le code est plus complexe, moins intuitif.

Je vais faire mon possible pour que vous compreniez bien le principe. N'hésitez pas à y revenir à tête reposée, une, deux, trois fois pour que cela soit bien clair.

En théorie

Une fois n'est pas coutume, je vais vous montrer les différentes constructions possibles en théorie avec quelques exemples, mais je vais aussi consacrer une section entière à des exemples d'utilisations pour expliciter cette partie théorique indispensable.

Format le plus simple

Comme je l'ai dit, les décorateurs sont des fonctions « classiques » de Python, dans leur définition. Ils ont une petite subtilité en ce qu'ils prennent en paramètre une fonction et renvoient une fonction.

On déclare qu'une fonction doit être modifiée par un (ou plusieurs) décorateurs grâce à une (ou plusieurs) lignes au-dessus de la définition de fonction, comme ceci :

@nom_du_decorateur
def ma_fonction(...)

Le décorateur s'exécute au moment de la définition de fonction et non lors de l'appel. Ceci est important. Il prend en paramètre, comme je l'ai dit, une fonction (celle qu'il modifie) et renvoie une fonction (qui peut être la même).

Voyez plutôt :

>>> def mon_decorateur(fonction):
...     """Premier exemple de décorateur"""
...     print("Notre décorateur est appelé avec en paramètre la fonction {0}".format(fonction))
...     return fonction
...
>>> @mon_decorateur
... def salut():
...     """Fonction modifiée par notre décorateur"""
...     print("Salut !")
...
Notre décorateur est appelé avec en paramètre la fonction <function salut at 0x00BA5198>
>>>

Euh… qu'est-ce qu'on a fait là ?

  • D'abord, on crée le décorateur. Il prend en paramètre, comme je vous l'ai dit, la fonction qu'il modifie. Dans notre exemple, il se contente d'afficher cette fonction puis de la renvoyer.

  • On crée ensuite la fonction salut. Comme vous le voyez, on indique avant la définition la ligne @mon_decorateur, qui précise à Python que cette fonction doit être modifiée par notre décorateur. Notre fonction est très utile : elle affiche « Salut ! » et c'est tout.

  • À la fin de la définition de notre fonction, on peut voir que le décorateur est appelé. Si vous regardez plus attentivement la ligne affichée, vous vous rendez compte qu'il est appelé avec, en paramètre, la fonction salut que nous venons de définir.

Intéressons-nous un peu plus à la structure de notre décorateur. Il prend en paramètre la fonction à modifier (celle que l'on définit sous la ligne du @), je pense que vous avez pu le constater. Mais il renvoie également cette fonction et cela, c'est un peu moins évident !

En fait, la fonction renvoyée remplace la fonction définie. Ici, on renvoie la fonction définie, c'est donc la même. Mais on peut demander à Python d'exécuter une autre fonction à la place, pour modifier son comportement. Nous allons voir cela un peu plus loin.

Pour l'heure, souvenez-vous que les deux codes ci-dessous sont identiques :

# Exemple avec décorateur
@decorateur
def fonction(...):
    ...
# Exemple équivalent, sans décorateur
def fonction(...):
    ...

fonction = decorateur(fonction)

Relisez bien ces deux codes, ils font la même chose. Le second est là pour que vous compreniez ce que fait Python quand il manipule des fonctions modifiées par un (ou plusieurs) décorateur(s).

Quand vous exécutez salut, vous ne voyez aucun changement. Et c'est normal puisque nous renvoyons la même fonction. Le seul moment où notre décorateur est appelé, c'est lors de la définition de notre fonction. Notre fonction salut n'a pas été modifiée par notre décorateur, on s'est contenté de la renvoyer telle quelle.

Modifier le comportement de notre fonction

Vous l'aurez deviné, un décorateur comme nous l'avons créé plus haut n'est pas bien utile. Les décorateurs servent surtout à modifier le comportement d'une fonction. Je vous montre cependant pas à pas comment cela fonctionne, sinon vous risquez de vite vous perdre.

Comment faire pour modifier le comportement de notre fonction ?

En fait, vous avez un élément de réponse un peu plus haut. J'ai dit que notre décorateur prenait en paramètre la fonction définie et renvoyait une fonction (peut-être la même, peut-être une autre). C'est cette fonction renvoyée qui sera directement affectée à notre fonction définie. Si vous aviez renvoyé une autre fonction que salut, dans notre exemple ci-dessus, la fonction salut aurait redirigé vers cette fonction renvoyée.

Mais alors… il faut définir encore une fonction ?

Eh oui ! Je vous avais prévenus (et ce n'est que le début), notre construction se complexifie au fur et à mesure : on va devoir créer une nouvelle fonction qui sera chargée de modifier le comportement de la fonction définie. Et, parce que notre décorateur sera le seul à utiliser cette fonction, on va la définir directement dans le corps de notre décorateur.

Je suis perdu. Comment cela marche-t-il, concrètement ?

Je vais vous mettre le code, cela vaudra mieux que des tonnes d'explications. Je le commente un peu plus bas, ne vous inquiétez pas :

def mon_decorateur(fonction):
    """Notre décorateur : il va afficher un message avant l'appel de la
    fonction définie"""
    
    def fonction_modifiee():
        """Fonction que l'on va renvoyer. Il s'agit en fait d'une version
        un peu modifiée de notre fonction originellement définie. On se
        contente d'afficher un avertissement avant d'exécuter notre fonction
        originellement définie"""
        
        print("Attention ! On appelle {0}".format(fonction))
        return fonction()
    return fonction_modifiee

@mon_decorateur
def salut():
    print("Salut !")

Voyons l'effet, avant les explications. Aucun message ne s'affiche en exécutant ce code. Par contre, si vous exécutez votre fonction salut :

>>> salut()
Attention ! On appelle <function salut at 0x00BA54F8>
Salut !
>>>

Et si vous affichez la fonction salut dans l'interpréteur, vous obtenez quelque chose de surprenant :

>>> salut
<function fonction_modifiee at 0x00BA54B0>
>>>

Pour comprendre, revenons sur le code de notre décorateur :

  • Comme toujours, il prend en paramètre une fonction. Cette fonction, quand on place l'appel au décorateur au-dessus de def salut, c'est salut (la fonction définie à l'origine).

  • Dans le corps même de notre décorateur, vous pouvez voir qu'on a défini une nouvelle fonction, fonction_modifiee. Elle ne prend aucun paramètre, elle n'en a pas besoin. Dans son corps, on affiche une ligne avertissant qu'on va exécuter la fonction fonction (là encore, il s'agit de salut). À la ligne suivante, on l'exécute effectivement et on renvoie le résultat de son exécution (dans le cas de salut, il n'y en a pas mais d'autres fonctions pourraient renvoyer des informations).

  • De retour dans notre décorateur, on indique qu'il faut renvoyer fonction_modifiee.

Lors de la définition de notre fonction salut, on appelle notre décorateur. Python lui passe en paramètre la fonction salut. Cette fois, notre décorateur ne renvoie pas salut mais fonction_modifiee. Et notre fonction salut, que nous venons de définir, sera donc remplacée par notre fonction fonction_modifiee, définie dans notre décorateur.

Vous le voyez bien, d'ailleurs : quand on cherche à afficher salut dans l'interpréteur, on obtient fonction_modifiee.

Souvenez-vous bien que le code :

@mon_decorateur
def salut():
    ...

revient au même, pour Python, que le code :

def salut():
    ...

salut = mon_decorateur(salut)

Ce n'est peut-être pas plus clair. Prenez le temps de lire et de bien comprendre l'exemple. Ce n'est pas simple, la logique est bel et bien là mais il faut passer un certain temps à tester avant de bien intégrer cette notion.

Pour résumer, notre décorateur renvoie une fonction de substitution. Quand on appelle salut, on appelle en fait notre fonction modifiée qui appelle également salut après avoir affiché un petit message d'avertissement.

Autre exemple : un décorateur chargé tout simplement d'empêcher l'exécution de la fonction. Au lieu d'exécuter la fonction d'origine, on lève une exception pour avertir l'utilisateur qu'il utilise une fonctionnalité obsolète.

def obsolete(fonction_origine):
    """Décorateur levant une exception pour noter que la fonction_origine
    est obsolète"""
    
    def fonction_modifiee():
        raise RuntimeError("la fonction {0} est obsolète !".format(fonction_origine))
    return fonction_modifiee

Là encore, faites quelques essais : tout deviendra limpide après quelques manipulations.

Un décorateur avec des paramètres

Toujours plus dur ! On voudrait maintenant passer des paramètres à notre décorateur. Nous allons essayer de coder un décorateur chargé d'exécuter une fonction en contrôlant le temps qu'elle met à s'exécuter. Si elle met un temps supérieur à la durée passée en paramètre du décorateur, on affiche une alerte.

La ligne appelant notre décorateur, au-dessus de la définition de notre fonction, sera donc sous la forme :

@controler_temps(2.5) # 2,5 secondes maximum pour la fonction ci-dessous

Jusqu'ici, nos décorateurs ne comportaient aucune parenthèse après leur appel. Ces deux parenthèses sont très importantes : notre fonction de décorateur prendra en paramètres non pas une fonction, mais les paramètres du décorateur (ici, le temps maximum autorisé pour la fonction). Elle ne renverra pas une fonction de substitution, mais un décorateur.

Encore et toujours perdu. Pourquoi est-ce si compliqué de passer des paramètres à notre décorateur ?

En fait… ce n'est pas si compliqué que cela mais c'est dur à saisir au début. Pour mieux comprendre, essayez encore une fois de vous souvenir que ces deux codes reviennent au même :

@decorateur
def fonction(...):
    ...
def fonction(...):
    ...

fonction = decorateur(fonction)

C'est la dernière ligne du second exemple que vous devez retenir et essayer de comprendre : fonction = decorateur(fonction).

On remplace la fonction que nous avons définie au-dessus par la fonction que renvoie notre décorateur.

C'est le mécanisme qui se cache derrière notre @decorateur.

Maintenant, si notre décorateur attend des paramètres, on se retrouve avec une ligne comme celle-ci :

@decorateur(parametre)
def fonction(...):
    ...

Et si vous avez compris l'exemple ci-dessus, ce code revient au même que :

def fonction(...):
    ...

fonction = decorateur(parametre)(fonction)

Je vous avais prévenus, ce n'est pas très intuitif ! Mais relisez bien ces exemples, le déclic devrait se faire tôt ou tard.

Comme vous le voyez, on doit définir comme décorateur une fonction qui prend en arguments les paramètres du décorateur (ici, le temps attendu) et qui renvoie un décorateur. Autrement dit, on se retrouve encore une fois avec un niveau supplémentaire dans notre fonction.

Je vous donne le code sans trop insister. Si vous arrivez à comprendre la logique qui se trouve derrière, c'est tant mieux, sinon n'hésitez pas à y revenir plus tard :

"""Pour gérer le temps, on importe le module time
On va utiliser surtout la fonction time() de ce module qui renvoie le nombre
de secondes écoulées depuis le premier janvier 1970 (habituellement).
On va s'en servir pour calculer le temps mis par notre fonction pour
s'exécuter"""

import time

def controler_temps(nb_secs):
    """Contrôle le temps mis par une fonction pour s'exécuter.
    Si le temps d'exécution est supérieur à nb_secs, on affiche une alerte"""
    
    def decorateur(fonction_a_executer):
        """Notre décorateur. C'est lui qui est appelé directement LORS
        DE LA DEFINITION de notre fonction (fonction_a_executer)"""
        
        def fonction_modifiee():
            """Fonction renvoyée par notre décorateur. Elle se charge
            de calculer le temps mis par la fonction à s'exécuter"""
            
            tps_avant = time.time() # Avant d'exécuter la fonction
            valeur_renvoyee = fonction_a_executer() # On exécute la fonction
            tps_apres = time.time()
            tps_execution = tps_apres - tps_avant
            if tps_execution >= nb_secs:
                print("La fonction {0} a mis {1} pour s'exécuter".format( \
                        fonction_a_executer, tps_execution))
            return valeur_renvoyee
        return fonction_modifiee
    return decorateur

Ouf ! Trois niveaux dans notre fonction ! D'abord controler_temps, qui définit dans son corps notre décorateur decorateur, qui définit lui-même dans son corps notre fonction modifiée fonction_modifiee.

J'espère que vous n'êtes pas trop embrouillés. Je le répète, il s'agit d'une fonctionnalité très puissante mais qui n'est pas très intuitive quand on n'y est pas habitué. Jetez un coup d'œil du côté des exemples au-dessus si vous êtes un peu perdus.

Nous pouvons maintenant utiliser notre décorateur. J'ai fait une petite fonction pour tester qu'un message s'affiche bien si notre fonction met du temps à s'exécuter. Voyez plutôt :

>>> @controler_temps(4)
... def attendre():
...     input("Appuyez sur Entrée...")
...
>>> attendre() # Je vais appuyer sur Entrée presque tout de suite
Appuyez sur Entrée...
>>> attendre() # Cette fois, j'attends plus longtemps
Appuyez sur Entrée...
La fonction <function attendre at 0x00BA5810> a mis 4.14100003242 pour s'exécuter
>>>

Ça marche ! Et même si vous devez passer un peu de temps sur votre décorateur, vu ses différents niveaux, vous êtes obligés de reconnaître qu'il s'utilise assez simplement.

Il est quand même plus intuitif d'écrire :

@controler_temps(4)
def attendre(...)
    ...

que :

def attendre(...):
    ...

attendre = controler_temps(4)(attendre)

Tenir compte des paramètres de notre fonction

Jusqu'ici, nous n'avons travaillé qu'avec des fonctions ne prenant aucun paramètre. C'est pourquoi notre fonction fonction_modifiee n'en prenait pas non plus.

Oui mais… tenir compte des paramètres, cela peut être utile. Sans quoi on ne pourrait construire que des décorateurs s'appliquant à des fonctions sans paramètre.

Il faut, pour tenir compte des paramètres de la fonction, modifier ceux de notre fonction fonction_modifiee. Là encore, je vous invite à regarder les exemples ci-dessus, explicitant ce que Python fait réellement lorsqu'on définit un décorateur avant une fonction. Vous pourrez vous rendre compte que fonction_modifiee remplace notre fonction et que, par conséquent, elle doit prendre des paramètres si notre fonction définie prend également des paramètres.

C'est dans ce cas en particulier que nous allons pouvoir réutiliser la notation spéciale pour nos fonctions attendant un nombre variable d'arguments. En effet, le décorateur que nous avons créé un peu plus haut devrait pouvoir s'appliquer à des fonctions ne prenant aucun paramètre, ou en prenant un, ou plusieurs… au fond, notre décorateur ne doit ni savoir combien de paramètres sont fournis à notre fonction, ni même s'en soucier.

Là encore, je vous donne le code adapté de notre fonction modifiée. Souvenez-vous qu'elle est définie dans notre decorateur, lui-même défini dans controler_temps (je ne vous remets que le code de fonction_modifiee).

...
        def fonction_modifiee(*parametres_non_nommes, **parametres_nommes):
            """Fonction renvoyée par notre décorateur. Elle se charge
            de calculer le temps mis par la fonction à s'exécuter"""
            
            tps_avant = time.time() # avant d'exécuter la fonction
            ret = fonction_a_executer(*parametres_non_nommes, **parametres_nommes)
            tps_apres = time.time()
            tps_execution = tps_apres - tps_avant
            if tps_execution >= nb_secs:
                print("La fonction {0} a mis {1} pour s'exécuter".format( \
                        fonction_a_executer, tps_execution))
            return ret

À présent, vous pouvez appliquer ce décorateur à des fonctions ne prenant aucun paramètre, ou en prenant un certain nombre, nommés ou non. Pratique, non ?

Des décorateurs s'appliquant aux définitions de classes

Vous pouvez également appliquer des décorateurs aux définitions de classes. Nous verrons un exemple d'application dans la section suivante. Au lieu de recevoir en paramètre la fonction, vous allez recevoir la classe.

>>> def decorateur(classe):
...     print("Définition de la classe {0}".format(classe))
...     return classe
...
>>> @decorateur
... class Test:
...     pass
...
Définition de la classe <class '__main__.Test'>
>>>

Voilà. Vous verrez dans la section suivante quel peut être l'intérêt de manipuler nos définitions de classes à travers des décorateurs. Il existe d'autres exemples que celui que je vais vous montrer, bien entendu.

Chaîner nos décorateurs

Vous pouvez modifier une fonction ou une définition de classe par le biais de plusieurs décorateurs, sous la forme :

@decorateur1
@decorateur2
def fonction():

Ce n'est pas plus compliqué que ce que vous venez de faire. Je vous le montre pour qu'il ne subsiste aucun doute dans votre esprit, vous pouvez tester à loisir cette possibilité, par vous-mêmes.

Je vais à présent vous présenter quelques applications possibles des décorateurs, inspirées en grande partie de la PEP 318.

Exemples d'applications

Nous allons voir deux exemples d'applications des décorateurs dans cette section. Vous en avez également vu quelques-uns dans la section précédente mais, maintenant que vous maîtrisez la syntaxe, nous allons nous pencher sur des exemples plus parlants !

Les classes singleton

Certains reconnaîtront sûrement cette appellation. Pour les autres, sachez qu'une classe dite singleton est une classe qui ne peut être instanciée qu'une fois.

Autrement dit, on ne peut créer qu'un seul objet de cette classe.

Cela peut-être utile quand vous voulez être absolument certains qu'une classe ne produira qu'un seul objet, qu'il est inutile (voire dangereux) d'avoir plusieurs objets de cette classe. La première fois que vous appelez le constructeur de ce type de classe, vous obtenez le premier et l'unique objet nouvellement instancié. Tout appel ultérieur à ce constructeur renvoie le même objet (le premier créé).

Ceci est très facile à modéliser grâce à des décorateurs.

Code de l'exemple
def singleton(classe_definie):
    instances = {} # Dictionnaire de nos instances singletons
    def get_instance():
        if classe_definie not in instances:
            # On crée notre premier objet de classe_definie
            instances[classe_definie] = classe_definie()
        return instances[classe_definie]
    return get_instance
Explications

D'abord, pour utiliser notre décorateur, c'est très simple : il suffit de mettre l'appel à notre décorateur avant la définition des classes que nous souhaitons utiliser en tant que singleton :

>>> @singleton
... class Test:
...     pass
...
>>> a = Test()
>>> b = Test()
>>> a is b
True
>>>

Quand on crée notre premier objet (celui se trouvant dans a), notre constructeur est bien appelé. Quand on souhaite créer un second objet, c'est celui contenu dans a qui est renvoyé. Ainsi, a et b pointent vers le même objet.

Intéressons-nous maintenant à notre décorateur. Il définit dans son corps un dictionnaire. Ce dictionnaire contient en guise de clé la classe singleton et en tant que valeur l'objet créé correspondant. Il renvoie notre fonction interne get_instance qui va remplacer notre classe. Ainsi, quand on voudra créer un nouvel objet, ce sera get_instance qui sera appelée. Cette fonction vérifie si notre classe se trouve dans le dictionnaire. Si ce n'est pas le cas, on crée notre premier objet correspondant et on l'insère dans le dictionnaire. Dans tous les cas, on renvoie l'objet correspondant dans le dictionnaire (soit il vient d'être créé, soit c'est notre objet créé au premier appel du constructeur).

Grâce à ce système, on peut avoir plusieurs classes déclarées comme des singleton et on est sûr que, pour chacune de ces classes, un seul objet sera créé.

Contrôler les types passés à notre fonction

Vous l'avez déjà observé dans Python : aucun contrôle n'est fait sur le type des données passées en paramètres de nos fonctions. Certaines, comme print, acceptent n'importe quel type. D'autres lèvent des exceptions quand un paramètre d'un type incorrect leur est fourni.

Il pourrait être utile de coder un décorateur qui vérifie les types passés en paramètres à notre fonction et qui lève une exception si les types attendus ne correspondent pas à ceux reçus lors de l'appel à la fonction.

Voici notre définition de fonction, pour vous donner une idée :

@controler_types(int, int)
def intervalle(base_inf, base_sup):

Notre décorateur controler_types doit s'assurer qu'à chaque fois qu'on appelle la fonction intervalle, ce sont des entiers qui sont passés en paramètres en tant que base_inf et base_sup.

Ce décorateur est plus complexe, bien que j'aie simplifié au maximum l'exemple de la PEP 318.

Encore une fois, s'il est un peu long à écrire, il est d'une simplicité enfantine à utiliser.

Code de l'exemple
def controler_types(*a_args, **a_kwargs):
    """On attend en paramètres du décorateur les types souhaités. On accepte
    une liste de paramètres indéterminés, étant donné que notre fonction
    définie pourra être appelée avec un nombre variable de paramètres et que
    chacun doit être contrôlé"""
    
    def decorateur(fonction_a_executer):
        """Notre décorateur. Il doit renvoyer fonction_modifiee"""
        def fonction_modifiee(*args, **kwargs):
            """Notre fonction modifiée. Elle se charge de contrôler
            les types qu'on lui passe en paramètres"""
            
            # La liste des paramètres attendus (a_args) doit être de même
            # Longueur que celle reçue (args)
            if len(a_args) != len(args):
                raise TypeError("le nombre d'arguments attendu n'est pas égal " \
                        "au nombre reçu")
            # On parcourt la liste des arguments reçus et non nommés
            for i, arg in enumerate(args):
                if a_args[i] is not type(args[i]):
                    raise TypeError("l'argument {0} n'est pas du type " \
                            "{1}".format(i, a_args[i]))
            
            # On parcourt à présent la liste des paramètres reçus et nommés
            for cle in kwargs:
                if cle not in a_kwargs:
                    raise TypeError("l'argument {0} n'a aucun type " \
                            "précisé".format(repr(cle)))
                if a_kwargs[cle] is not type(kwargs[cle]):
                    raise TypeError("l'argument {0} n'est pas de type" \
                            "{1}".format(repr(cle), a_kwargs[cle]))
            return fonction_a_executer(*args, **kwargs)
        return fonction_modifiee
    return decorateur
Explications

C'est un décorateur assez complexe (et pourtant, croyez-moi, je l'ai simplifié autant que possible). Nous allons d'abord voir comment l'utiliser :

>>> @controler_types(int, int)
... def intervalle(base_inf, base_sup):
...     print("Intervalle de {0} à {1}".format(base_inf, base_sup))
...
>>> intervalle(1, 8)
Intervalle de 1 à 8
>>> intervalle(5, "oups!")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 24, in fonction_modifiee
TypeError: l'argument 1 n'est pas du type <class 'int'>
>>>

Là encore, l'utilisation est des plus simples. Intéressons-nous au décorateur proprement dit, c'est déjà un peu plus complexe.

Notre décorateur doit prendre des paramètres (une liste de paramètres indéterminés d'ailleurs, car notre fonction doit elle aussi prendre une liste de paramètres indéterminés et l'on doit contrôler chacun d'eux). On définit donc un paramètre a_args qui contient la liste des types des paramètres non nommés attendus, et un second paramètre a_kwargs qui contient le dictionnaire des types des paramètres nommés attendus.

Vous suivez toujours ?

Vous devriez comprendre la construction d'ensemble, nous l'avons vue un peu plus haut. Elle comprend trois niveaux, puisque nous devons influer sur le comportement de la fonction et que notre décorateur prend des paramètres. Notre code de contrôle se trouve, comme il se doit, dans notre fonction fonction_modifiee (qui va prendre la place de notre fonction_a_executer).

On commence par vérifier que la liste des paramètres non nommés attendus est bien égale en taille à la liste des paramètres non nommés reçus. On vérifie ensuite individuellement chaque paramètre reçu, en contrôlant son type. Si le type reçu est égal au type attendu, tout va bien. Sinon, on lève une exception. On répète l'opération sur les paramètres nommés (avec une petite différence, puisqu'il s'agit de paramètres nommés : ils sont contenus dans un dictionnaire, pas une liste).

Si tout va bien (aucune exception n'a été levée), on exécute notre fonction en renvoyant son résultat.

Voilà nos exemples d'applications. Il y en a bien d'autres, vous pouvez en retrouver plusieurs sur la PEP 318 consacrée aux décorateurs, ainsi que des informations supplémentaires : n'hésitez pas à y faire un petit tour.

En résumé

  • Les décorateurs permettent de modifier le comportement d'une fonction.

  • Ce sont eux-mêmes des fonctions, prenant en paramètre une fonction et renvoyant une fonction (qui peut être la même).

  • On peut déclarer une fonction comme décorée en plaçant, au-dessus de la ligne de sa définition, la ligne @nom_decorateur.

  • Au moment de la définition de la fonction, le décorateur est appelé et la fonction qu'il renvoie sera celle utilisée.

  • Les décorateurs peuvent également prendre des paramètres pour influer sur le comportement de la fonction décorée.

Example of certificate of achievement
Example of certificate of achievement