#### Rappel sur les décorateurs de Python Comme vous le savez probablement, Python possède une syntaxe permettant de « décorer » des fonctions ou des classes, de manière élégante. Nous l'avons d'ailleurs utilisée dans ce tutoriel. Regardez la méthode `prix` de la classe `Sandwich`. :::python class Sandwich: # ... @property def prix(): # définition de la méthode # ... Le `@property` signifie que l'on a décoré la méthode `prix` de manière à en faire une propriété. Cette syntaxe revient exactement à faire la chose suivante : :::python class Sandwich: # ... def prix(): # définition de la méthode prix = property(prix) # ... Le premier exemple est simplement *beaucoup plus lisible* que le second, et c'est ce qui fait tout l'intérêt de cette syntaxe. Maintenant, cassons tout de suite le mythe : ces décorateurs Python, ce sont le plus souvent de simples fonctions. En fait, le seul pré-requis pour qu'un objet puisse utiliser la syntaxe "`@truc`", c'est qu'il soit « *appelable* » (*callable* en anglais), c'est-à-dire qu'il réagisse à l'opérateur `()`, avec un seul argument, et qu'il retourne une valeur. Cela veut dire que certaines classes peuvent être utilisées avec cette syntaxe. Mais cela dépasse un peu la portée de ce tutoriel. Je ne vais pas vous faire tout un cours sur la décoration de fonctions en Python ([le tutoriel de prolixe et 6prien][tutodeco] le fait déjà très bien), mais je pense qu'un petit rappel, pour faire la connexion avec la suite, s'impose. L'utilisation la plus classique de ces décorateurs consiste à **modifier le comportement** d'une fonction, de manière transparente (c'est-à-dire que le code appelant a vraiment l'impression d'utiliser la fonction qu'il appelle, et pas un décorateur). Voici un exemple d'utilisation typique : nous allons créer un décorateur de fonction qui, à chaque appel de la fonction décorée, va afficher sa valeur de retour dans la console. :::python def log_console(fonction): # On définit une fonction "usurpatrice" qui se fera passer pour # la fonction décorée. def fct_usurpatrice(*args, **kwargs): # on appelle la fonction décorée avec les arguments que # l'utilisateur voulait lui passer à la base # et on stocke le résultat. resultat = fonction(*args, **kwargs) # comportement ajouté : on affiche le résultat print('Appel de {0}: {1}'.format(fonction.__name__, resultat)) # on retourne le résultat : l'utilisateur n'y voit que du feu return resultat # Maintenant, on retourne la fonction "usurpatrice". return fct_usurpatrice Bien, maintenant, appliquons-le, pas à pas. Créons d'abord une fonction toute bête : :::python def addition(a, b): return a+b Maintenant, appliquons-lui notre décorateur, et stockons le résultat dans une autre variable : :::pycon >>> add_decoree = log_console(addition) >>> add_decoree <function fct_usurpatrice at 0x953f96c> Voilà qui est intéressant : `add_decoree` est une fonction, et c'est la fonction que nous avons définie dans le décorateur ! Maintenant, si on l'appelle, cela donne ce qui suit : :::pycon >>> add_decoree(2, 2) Appel de addition: 4 # fct_usurpatrice affiche le résultat de la fonction "addition" 4 # le résultat est ensuite retourné Maintenant, pourquoi avoir appelé la fonction `fct_usurpatrice` ? Eh bien, regardons ce qui se passe lorsque l'on décore la fonction `addition` avec la syntaxe prévue à cet effet. :::python @log_console def addition(a, b): return a+b À quoi correspond maintenant le symbole `addition` ? :::pycon >>> addition <function fct_usurpatrice at 0x885e96c> >>> addition(3, 2) Appel de addition: 5 5 Tout simplement ! Le décorateur a remplacé la fonction de base, il a « usurpé son identité ». Cependant, la fonction usurpatrice reproduit le comportement de la fonction décorée. C'est ce qui fait que tout est « silencieux » dans ce processus. On a simplement ajouté un comportement à cette fonction, sans modifier son code ni altérer son comportement existant. Je pense que vous faites maintenant le rapprochement avec ce que nous avons fait tout le long de ce tutoriel, non ? ;) #### Décoration statique ou dynamique Réfléchissons : quelle est la différence entre la décoration permise par la syntaxe de Python, et la décoration d'objets telle que nous l'avons pratiquée avec nos sandwiches ? Est-ce que c'est que dans un cas, nous avons décoré des fonctions, et dans l'autre des objets ? Pas tout à fait. En soi, une fonction est un objet comme un autre : on peut les assigner à une variable, les passer à d'autres fonctions, et même leur ajouter des attributs si ça nous chante ! Alors c'est quoi, la différence ? Examinons la question sous un autre angle : pourquoi ne pourrait-on pas utiliser la syntaxe en `@truc` dans l'exemple de nos sandwiches ? *Parce que la syntaxe en `@truc` sert à décorer une fonction ou une classe de façon définitive*, toutes les instances de la fonction ou de la classe seront elles aussi décorées. Autrement dit, la syntaxe en `@truc` est une décoration ***statique***. Dans notre étude de cas, nous avions besoin de décorer les objets suivant les goûts du client : ces modifications sont par nature imprévisibles, il faut qu'elles soient faites de manière ***dynamique***, à l'exécution, et sur des objets (des instances) plutôt que sur des classes. La voilà, la différence. ;) Par contre, dans les deux cas, le principe de décoration est exactement le même : il s'agit d'***envelopper*** l'objet (ou la fonction, ou la classe) décoré(e) pour ***modifier légèrement son comportement***, tout ***en se faisant passer pour*** lui(elle). #### Juste pour rigoler : décorons nos décorateurs pour les rendre plus jolis ! Ce paragraphe, totalement facultatif, est là à titre ludique, « pour le fun ». Nous allons tenter d'utiliser les décorateurs en `@truc` de Python pour rendre notre application un peu plus sexy à utiliser (sans en modifier le code, bien évidemment). D'ailleurs, j'en profite pour remercier [ordiclic](http://www.siteduzero.com/membres-294-67116.html) pour m'en avoir inspiré l'idée. ;)  Reprenons donc notre application, et regardons bien la syntaxe utilisée pour définir notre kebab « mayo/harissa, avec pain pita, au poulet, avec supplément fromage et sans oignon » : :::pycon >>> a = sans_oignon(supp_fromage(mod_poulet(mod_pita(Kebab("mayo/harissa"))))) >>> print(a) Kebab sauce mayo/harissa, pain pita, mod viande poulet, supplément fromage, sans oignons 1x mod viande 0.20 € 1x salade 0.20 € 1x frites 0.50 € 1x fromage 0.50 € 1x tomates 0.20 € 1x base 3.00 € 1x mod pain 0.20 € Total 4.80 € Bien que cela fonctionne très bien, il faut quand même avouer qu'en suivant la PEP-8, cette syntaxe n'est pas très jolie à regarder : on est obligé d'écrire les décorateurs dans l'ordre inverse, et toutes les fonctions sont imbriquées les unes dans les autres. Ce qui serait sympa, ce serait que l'on puisse obtenir le même résultat avec la syntaxe suivante : :::pycon >>> a = Kebab("mayo/harissa") << sans_oignon << supp_fromage << mod_poulet << mod_pita Commençons par réfléchir sur un cas « simple ». Essayons de faire en sorte que la syntaxe suivante : :::python a = Kebab("mayo/harissa") << sans_oignon devienne équivalente à cela : :::python a = sans_oignon(Kebab("mayo/harissa")) A priori, il suffit de surcharger l'opérateur `<<` de la classe `Kebab`. Dans [la documentation de Python](http://docs.python.org/reference/datamodel.html) on pourra se rendre compte que la syntaxe `a << b` est interprétée comme `a.__lshift__(b)`. On peut donc essayer de faire notre surcharge d'opérateur en mode « *monkey patching* », comme ceci : :::pycon >>> def applique(objet, fonction): ... return fonction(objet) ... >>> Kebab.__lshift__ = applique # surcharge de l'opérateur dans la classe Kebab >>> a = Kebab("mayo/harissa") << sans_oignon >>> print(a) Kebab sauce mayo/harissa, sans oignons 1x tomates 0.20 € 1x salade 0.20 € 1x base 3.00 € 1x frites 0.50 € Total 3.90 € Eurêka ! Notre Kebab est devenu « joliment décorable » grâce à une simple surcharge de fonction. Étape suivante : plutôt que de faire ce « *monkey patching* » de manière explicite, pourquoi n'essayerions-nous pas de créer un décorateur statique qui ferait la même chose ? A priori, cela devrait être faisable. Il suffit que notre décorateur prenne une classe en argument (comme Kebab), surcharge sa méthode `__lshift__`, puis retourne la classe ainsi modifiée. :::python def decorable(cls): cls.__lshift__ = lambda objet, fonction: fonction(objet) return cls Et puis tant qu'à faire, si on appliquait directement ce décorateur à la classe `Sandwich` ? :::python @decorable class Sandwich: # ... Ah mais attendez, si on a surchargé la classe `Sandwich` ça veut dire que tous nos `DecorateurSandwich`s concrets sont aussi surchargés, puisqu'ils en héritent ! Allez, soyons fous, et faisons directement le test de la mort : :::pycon >>> a = Cheeseburger('ketchup/mayo') << sans_oignon << supp_fromage >>> print(a) Cheeseburger sauce ketchup/mayo, sans oignons, supplément fromage 1x cornichons 0.10 € 1x salade 0.20 € 3x fromage 1.50 € 1x tomates 0.20 € 1x base 2.00 € 1x steak 0.50 € Total 4.50 € Magique ! :D ### Les idées « à emporter » Il est temps maintenant de récapituler tout ce que nous avons appris dans ce tutoriel. * Un *design pattern* est **une solution générique** à un problème de conception donné. Chaque *design pattern* porte **un nom** pour faciliter la communication entre développeurs. * Il ne faut pas utiliser systématiquement l'héritage pour modifier le comportement d'une classe : il existe des cas où cela transforme une application en cauchemard pour les développeurs. * « Si vous **pensez** avoir besoin de l'héritage multiple, alors c'est probablement une mauvaise idée. Si vous **savez** que vous avez besoin de l'héritage multiple, alors c'est sûrement la seule solution ». * Le principe d'ouverture-fermeture : « Une classe doit être **fermée à la modification**, mais **ouverte à l'extension** ». * Le *pattern* Decorator consiste à créer des classes qui **encapsulent et héritent** de la même interface publique que les objets qu'elles enveloppent. * La décoration consiste à **envelopper** un objet pour en **modifier légèrement** le comportement, tout **en se faisant passer** pour lui. * La syntaxe `@truc` de Python permet une décoration **statique**, alors que le *pattern* Decorator décrit une décoration **dynamique**. * La surcharge d'opérateurs *via* un décorateur statique de classe, c'est sexy. ------------------------------------------------------------------------------------------------------------------- [tutodeco]: http://www.siteduzero.com/tutoriel-3-323958-les-decorateurs.html [gof]: http://c2.com/cgi/wiki?GangOfFour "Gang of Four" [dpbook]: http://c2.com/cgi/wiki?DesignPatternsBook "Design Patterns: Elements of Reusable Object-Oriented Software" [pythonprop]: http://docs.python.org/library/functions.html#property Nous arrivons à la fin de ce tutoriel. J'espère qu'il vous aura fait réfléchir et découvrir de nouvelles choses. Je vous remercie d'avoir pris le temps de le lire. À bientôt pour de nouvelles aventures.