• PYTHON > les décorateurs de fonctions

      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’occurrence, 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 fonctionsalut. 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 fonctionsalutque 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écutezsalut, 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 fonctionsalutn’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 quesalut, dans notre exemple ci-dessus, la fonctionsalutaurait 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 fonctionsalut:

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

      >>>

       

       

      Et si vous affichez la fonctionsalutdans 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 dedef salut, c’estsalut(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 fonctionfonction(là encore, il s’agit desalut). À la ligne suivante, on l’exécute effectivement et on renvoie le résultat de son exécution (dans le cas desalut, 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 renvoyerfonction_modifiee.

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

      Vous le voyez bien, d’ailleurs : quand on cherche à affichersalutdans l’interpréteur, on obtientfonction_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 appellesalut, on appelle en fait notre fonction modifiée qui appelle égalementsalutaprè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’abordcontroler_temps, qui définit dans son corps notre décorateurdecorateur, qui définit lui-même dans son corps notre fonction modifiéefonction_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 fonctionfonction_modifieen’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 fonctionfonction_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 quefonction_modifieeremplace 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 notredecorateur, lui-même défini danscontroler_temps(je ne vous remets que le code defonction_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 classessingleton

      Certains reconnaîtront sûrement cette appellation. Pour les autres, sachez qu’une classe ditesingletonest 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 quesingleton:

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

       

      Quand on crée notre premier objet (celui se trouvant dansa), notre constructeur est bien appelé. Quand on souhaite créer un second objet, c’est celui contenu dansaqui est renvoyé. Ainsi,aetbpointent 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 classesingletonet en tant que valeur l’objet créé correspondant. Il renvoie notre fonction interneget_instancequi va remplacer notre classe. Ainsi, quand on voudra créer un nouvel objet, ce seraget_instancequi 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 dessingletonet 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, commeprint, 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écorateurcontroler_typesdoit s’assurer qu’à chaque fois qu’on appelle la fonctionintervalle, ce sont des entiers qui sont passés en paramètres en tant quebase_infetbase_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ètrea_argsqui contient la liste des types des paramètres non nommés attendus, et un second paramètrea_kwargsqui 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 fonctionfonction_modifiee(qui va prendre la place de notrefonction_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.

 

Aucun commentaire

 

Laissez un commentaire