Design Patterns en Python

Tout ceci appartient à la "couche méthodologique" de l'apprentissage de programmation, et votre cours de Java en L3 prévoit une raisonnable introduction aux Patterns en général. Mon cours est orienté techniquement vers le Python avancé, et l'essentiel pour moi ce sont des exemples concrets, donc je toucherai un pourcentage infinitésimal du domaine, et de plus, spécifiquement orienté Python... De toute façon, ce domaine est né dans le cadre de la programmation orientée-objets, et est ciblé sur des langages comme Java ou C++ (éventuellement PHP ; un peu Smalltalk). Concernant Python, sa popularité est visiblent inférieure.

Qu'est-ce que un Design Pattern?

En français : "patron de conception", mais le terme anglais est utilisé plus souvent, avec "patron" décliné comme "modèle", "motif", "type", "schéma", etc., parfois même : "style". Tout, d'abord, c'est une "abstraction", une manière commune de parler de plusieurs choses disparates, un modèle conceptuel commun à plusieurs solutions d'un problème, ou une méthode commune, reflétant typiquement quelques bonnes pratiques de création, qui se répètent, inspirent les autres, et peuvent être réutilisées. Le nom vient (années '70) de Christopher Alexander, un architecte et anthropologue anglais, qui a analysé le phénomène de répétition des schémas dans l'ingénierie et dans les arts. En Informatique, le terme et les notions de DP ont été popularisés dans le livre "Design Patterns – Elements of Reusable Object-Oriented Software" de 1995.

Dans l'IA, le parcours des arbres est une exemplification de pattern, de genre "Strategie". Dans la simulation, l'observation des parties d'un système par d'autres (et qui réagissent en conséquence), est un pattern, "Observateur", populaire dans une catégorie d'interfaces visuelles. Meme les schémas classes-instances, ou client-serveur – ce sont des patterns. Dans la POO, les notions comme "Singleton" (classe avec une seule instance possible), "Itérateur", "Adaptateur" (ou "Façade" - un moyen de voir un objet de manière commode, indépendante de sa structure), ce sont des patterns.

Le monde de patterns évolue, certains sont oubliés, d'autres apparaissent, souvent c'est une question de mode (ou de paresse...) Il n'y a pas de vraie standardisation. Quelques solutions "Javaistes" ne sont guère adaptables au Python, et vice-versa. Il y a aussi des patterns sociologiques, médicaux, architecturaux, culturels et politiques... L'idée est de pouvoir maîtriser la complexité de ce monde avec quelques idées/protocoles généraux. Dans l'informatique, c'est une question de rendre la programmation plus efficace (codage, débogage, analyse). Pour l'analyse ça va, et permet souvent de mieux comprendre quelques algorithmes. Pour le codage, le succès est mitigé, car raconter des généralités ne suffit pas pour rendre une implémentation efficace...

Opinion personnelle (pourquoi je n'aime pas les patterns...)

(Je parle de patterns dans le contexte de ce cours...)

D'abord il faut bien maîtriser les langages de programmation (avec leurs libraries de support) et les algorithmes. Si vous trouvez un cours/tutoriel qui décrit des patterns de manière généraliste, sans donner les détails d'implémentation correcte, efficace, et réutilisable, simplement fuyez.

Pattern Observateur

Le premier exemple est en Smalltalk et concerne le fonctionnement d'un naviga­teur-système, permettant de rédiger les méthodes d'une classe. En sélectionnant la classe, le navigateur affiche les méthodes ; en sélection­nant une, on accède à sa source.

L'interface contient plusieurs éléments visuels (fenêtres) attachées à ses "contrôleurs".

La description est approximative. Une sélection (click) d'une classe ou méthode, lance une méthode "standard" changed, qui en standard envoie le message updated(self) à tous les objets spécifiés comme les observateurs de cette fenêtre source. La mise à jour et la notification, peut déclencher la mise à jour des observateurs, et l'action se répète.
Tout ce que l'utilisateur code – dans ce contexte – est de spécifier l'arbre de dépendances, de déclarer que les volets X Y dépendent de Z. Alors, ils deviennent les observateurs de Z.

(Pour votre culture : le schéma ici applique les observateurs à l'implémentation du pattern plus global : Modèle - Vue - Contrôleur, dont vous allez entendre parler ; le système se divise en fragments qui ne se mélangent pas, l'un gère les données internes, un autre les affiche, un troisième interagit avec l'utilisateur).

Dans le modèle de système planétaire discuté en TP, les noms sont différents, mais la méthode move, qui déplace un astre, contient la notification de la tortue attachée à cette planète, et celle-là visualise le déplacement.

En principe on aurait pu notifier d'autres planètes, mais il est plus efficace d'opérer avec les paramètres (positions) accessibles directement. La notification des observateurs souvent ajoute des indirections qui consomment le temps et la mémoire (pas trop). L'usage des patterns parfois peut ralentir un peu le programme, mais ceci n'est pas grave. Ici les astres constituent le modèle, et la Tortue (l'observateur) - la Vue.

Pattern Singleton

Ce pattern est fréquement présenté comme le premier, car il est trivial à comprendre : la classe ne permet qu'une seule instance. En Python on peut le réaliser en plaçant dans la classe une variable statique (variable de classe) qui contient la référence de l'instance unique, et la modification de la méthode __new__ : si et seulement si c'est le premier appel, on créé l'instance, et on la sauvegarde. Toute "création" ultérieure, retourne l'instance unique. L'usage possible de ce pattern est de créer ainsi des gestionnaires des ressources partagés, par ex. un seul "écran" attaché à l'écran physique de l'ordinateur. Un module de log. Un gestionnaire de la boucle événementielle qui pilote la totalité de l'interfaçage d'une application. Un "servlet" qui centralise l'accès de plusieurs clients à une base de données, etc. Voici un codage primitif.

Depuis un bon moment, le Singleton a une mauvaise presse (et tous ses adversaires répètent les mêmes arguments, avec beaucoup de préjugés...)
Pas la peine d'avoir une classe pour cela. On peut profiter des structures de données globales, et si on veut quand même les encapsuler, on met tout dans un module Python (qui par définition est unique). Mais les objets globaux dans la philosophie OO sont considérés mauvais en général. Il faut connaître ce pattern, mais son usage n'est pas la meilleure idée. Cet objet-singleton devra centraliser trop de dépendances entre les éléments du système... etc. Souvent les arguments contre les singletons sont abstraits et généralistes.

Nous sommes le(s) Borg, toute résistance serait futile...

Dans la saga TV : "Star Trek" il y a une race d'extraterrestres de constitution cyborg, qui "assimilent" d'autres races ; tout individu après l'injection des nano-implants, devient l'un d'eux, mais "eux" c'est un mauvais terme, ils sont psychiquement identiques, le même "état psychique", aucune individualité.

[À titre anecdotique : cette idée est, elle-même, un pattern culturel (Hive-Mind, une "ruche" psychique) dans le monde Sci-Fi, on le trouve dans le livre "The Puppet Masters" de Robert Heinlein (1951), et plusieurs autres ouvrages, que l'on peut tracer jusqu'à "Last and First Men" de Olaf Stapledon, (1930)...].

Ainsi, plus sérieusement, une application composite, multi-utilisateur, etc. peut avoir plusieurs instances d'objets-interfaces vers les ressources partagées, chaque instance a son identité, mais leurs attributs sont identiques (verrouillage critique des BD ou des imprimantes, etc.).

Selon certains, ce pattern est meilleur que Singleton, selon d'autres : peu importe. Pour les débutants, l'important est de comprendre ce qui se passe dans vos programmes, et ne pas trop écouter les pseudo-philosophes de programmation.

Regardons la définition :

class Borg(object):
    _etat = {}
    def __new__(cls,*a):
        inst = object.__new__(cls)
        inst.__dict__ = cls._etat
        return inst
    def __init__(self,a):  # inutilisé
        self.a=a
class JeSuisUnique(Borg):
    def __init__(self, a,b):
        self.a = a; self.b=b
    def __repr__(self): return str(self.__dict__)

x=JeSuisUnique("Klingon","Hollande"); 
y=JeSuisUnique("Romulien","Sarkozy")
Les deux objets sont différents (Borg.__repr__ donne des adresses différentes), mais la modification de l'un d'eux change l'état de l'autre.

Bien sûr, on peut protéger la classe contre la modification, ou, au contraire : une tentative de modification d'une instance (dans des conditions précises) dé-borgifie cette instance, créé un état (__dict__) local.

Injection des dépendances, et inversion de contrôle

La programmation à votre niveau ressemble la programmation en général de ma jeunesse : on écrit le code et on y incorpore des librairies extérieures, dont les modules (procédures, classes ...) sont appelées par le programme utilisateur, et remplissent leur rôles. On peut penser que c'est tout, dont on a besoin.

Cependant avec les frameworks partout, on voit l'émergence d'un autre style, l'inversion du contrôle. Le framework contrôle tout ; le client "enregistre" ses propres fonctions et classes (typiquement : sous-classes de quelques classes standardisées) auprès du framework, et celui-là les utilise. [Parfois on appelle cela "le principe de Hollywood", la façon de se débarrasser des candidat(e)s aspirant à une brillante et rapide carrière : "ne nous appelez pas, on vous contactera"...]. Ceci est devenu standard dans le monde de serveurs applicatifs, et les systèmes de management des documents. Ceci facilite la création des extensions modulaires, mais demande une réflexion sur la question : quelle entité dépend de quoi?, et souvent de déclarer ces dépendances...

Dans toutes circonstances si une entité (classe, module) X a besoin de Y : elle créé une instance d'Y, ou utilise une méthode d'Y, etc., X dépend d'Y. Mais si ce n'est pas la procédure utilisateur qui dépend d'une classe librairie, mais au contraire, alors comment une entité prédéfinie dans cette libraire peut appeler notre procédure / classe / méthode? Dans le modèle "Hollywood", les gestionnaires des ressources humaines doivent connaître votre téléphone, et autres données utiles à eux (dans quel contexte vous pourrez être utile), il faut leur donner cela : injecter les dépéndances.

Voici la surface d'un exemple très simple, l'usage du serveur Tornado. Un serveur est une fonction qui "écoute" son port d'entrée, et si elle reçoit un message selon un protocole établie, construit en envoie la réponse. Ces services en principes peuvent être lancés par l'utilisateur, mais la réalité est organisée autrement. Le programme principal se réduit à

import tornado.ioloop as TI
import tornado.web as TW
class MonHandler(TW.RequestHandler):
    def get(self):
        self.write("Hello, Schtroumpfs")
def go():
    app = TW.Application([(r"/", MonHandler)])
    app.listen(8888)
    TI.IOLoop.current().start()
Mon gestionnaire ne connaît pas les procédures de lecture et écriture réseau, seulement il sait que si le client émet la requête HTTP : GET, c'est la méthode get() qui sera lancée, et fournira la réponse.

"Décorateur"

Nous avons vu les décorateurs comme une technique concrète de programmation, une abréviation syntaxique permettant de transformer les fonctions ou classes, de manière structurée. Mais quel rapport avec les patterns? Les décorateurs classiques

@decor
def fun(...  ): ...
ou class ... ou un autre objet appellable – c'est une variante particulière de ce pattern, un décorateur statique, figé par sa forme syntaxique. L'essentiel sémantique, le sens de l'opération, c'est la transformation : fun = decor(fun), qui fait que la fonction se comporte comme quelque chose de différent. Par exemple, comme une fonction tracée, ou mémoisée, ou recompilée et optimisée comme avec @jit de numba. Mais nous avons introduit le modificateur "memo" beaucoup avant les décorateurs ! Au lieu d'avoir la fonction d'origine, le nom a été attribué à un autre objet. (En général, on pourrait décorer d'autres entités que les fonctions, ou classes).

Le pattern "Décorateur" est un 'emballage', permettant de modifier des objets structurés, non pas leurs états (attributs), mais leur comportement. Parfois ceci remplace l'héritage. Une particularité importante, qui doit être satisfaite, est la possibilité d'empiler plusieurs décorateurs, et d'obtenir une modification composite de l'original. La syntaxe avec l'arrobase, ou : f=qqchose(f) n'épuise pas les variantes syntaxiques des décorations. Python ici est considérablement plus universel que Java, grâce à la surcharge des opérateurs. Voici un exemple assez artificiel, mais inspirant (appelé parfois "monkey patching"...

Supposons que notre programme contient (ou appelle depuis un module extérieur) une classe d'objets compliqués, de nature géométrique (figures, caméras, lumières, textures... ; toute similitude avec vos lanceurs de rayons est purement accidentelle). Comme exemple, définissons :
class Thing(object):
    def __init__(self,*val): self.v=numpy.array(val)
    def __repr__(self): return str(self.v)
et Thing(2,1,3,0) affiche [2 1 3 0]. Nous ne voulons pas sous-classer Thing, afin d'avoir des fonctionnalités supplémentaires, mais nous voulons avoir la possibilité d'écrire, disons :
a=Thing(1,2,0) << ('t',10) << ('s',3)
ce qui signifie : ajoute 10 à la valeur interne v (translate) et ensuite multiplie le résultat (scale) par 3. Le résultat doit être [33 36 30]. L'idée est d'écrire une simple function transform, applicable comme : Thing=transform(Thing), et la syntaxe ci-dessus devrait fonctionner. Le "monkey-patching" ici signifie l'ajout d'une nouvelle méthode, __lshift__ qui en Python implémente l'opérateur (<<). Voici le transformateur :
def transform(cls):
    def trf(slf,op):
        cod,val=op
        if cod=='t': slf.v+=val
        elif cod=='s': slf.v*=val
        return slf
    cls.__lshift__=trf; return cls
Ainsi on peut ajouter des rotations et d'autres transformations, ou construire une autre variante syntaxique.

La transformation des données est l'essence de la programmation. Le traitement des éléments du programme comme des données se trouve au coeur de la méta-programmation. Si le programme est exécuté sous le contrôle d'une machine virtuelle, une telle fonctionnalité n'est pas difficile à implémenter.

Ensemble vous avez le principe de décoration. Le reste, c'est l'usage.

Autres patterns...

Voici une pseudo-présentation, incomplète, superficielle, et peu objective, de quelques autres patterns considérés populaires. Puisqu'il n'y aura pas de détails, et de conseils d'usage pour vous, ma recommandation : "fuyez" est valide. Mais le but de cette section est de vous donner un peu de terminologie ; c'est un survol qualitatif pour votre culture informatique. Tout d'abord : les patterns au sens intuitif existent dans d'autres catégories de langages que OO, seulement les slogans populaires, et > 90% d'exemples se concentrent sur Java et langages similaires. Pour vous l'essentiel est de comprendre le sens de quelques constructions.

Dans le monde de programmation fonctionnelle, très "pure", il a des patterns comme continuations – abstractions des "futurs" d'un calcul ; on applique une fonction à une donnée, et ensuite quoi? L'enchaînement des continuations construit un style particulier de programmation, connu de tous connaisseurs de programmation fonctionnelle.

[Mais votre cours d'introduction à Haskell en 2nd semestre, ne pourra toucher ces questions. Il n'abordera pas non plus les Monades – une abstraction de "données généralisées", accompagnées de comportement non-standard : les données dont l'usage est automatiquement tracé, ou les données non-déterministes : plusieurs réponses à une question, essentiel en IA. Il y a peu de chances que vous verrez cela à Caen.] Dans les langages logiques, comme Prolog, vous avez des patterns (simplistes, on ne les appelle pas "patterns"...) comme le backtracking, le retour en arrière, et la recherche d'une autre solution d'une spécification logique (par ex.: une autre combinaison de p éléments parmi n). On a des notions de contraintes : équations, inégalités, appartenance, etc. des données qui ne sont pas encore connues – qui ont évolué vers une catégorie de langages à part [CLP, CHR ... Aussi des langages de création graphique : Metapost, Asymptote]. On a des "structures incomplètes" et "listes différentielles", utilisées (implicitement ; j'ai caché les détails) par tous mes étudiants du groupe TAL : c'est l'essence des Direct Clause Grammars en Prolog, permettant de coder les analyseurs syntaxiques de manière très simple, lisible et puissante.

On a quelques autres patterns universels, comme les paradigmes de programmation par flots de données, utiles dans la constructions des langages "visuels", comme Simulink [ou XCOS] ou Prograph, dans les langages de traitement des signaux et des sons (Esterel, MAX), etc. On a donc les concepts de synchronisation des signaux, comme un pattern, et plusieurs autres. Tout ceci appartient à la culture de programmation, qui hélas est bien couverte par moins de 20% d'Universités dans le monde, loin de chez nous.

Revenons aux patterns "classiques" de POO. Répétons : ces patterns d'habitude ne sont pas bien adaptés au "style Pythonien", et correspondent aux tentatives de solution de problèmes qui souvent ne se posent vraiment pas en Python, grâce à sa souplesse et dynamisme. Ce qui doit vous guider est le bon sens, non pas la recherche des patterns.

Patterns : "Adaptateur", "Proxy", "Façade", etc.

Si votre système contient une multitude d'objets hétérogènes, à de facettes multiples, il est souvent difficile de les faire communiquer, et implique parfois une duplication gênante du code. Par ex., votre programme, "très Objet" communique avec une base de données, et votre code devient pollué par des structures SQL. Une philosophie rationnelle serait l'usage de la passerelle ORM (object-relational mapping) : la BD prétend être une collection d'objets Python, avec des attributs et méthodes d'accès assez standard. C'est un exemple d'adaptation ; on peut considérer aussi votre passerelle comme un "proxy" : grâce à la surcharge de __getattr__, __getitem__, etc., l'accès à "votre" objet, déclenche une communication à travers le réseau, et sollicite un serveur d'une BD distante.

Votre lanceur de rayons dispose d'une caméra qui projète des rayons, des "choses", des lumières, textures, champs de déformation, etc. Mais pour les "choses" (surfaces) l'essentiel est la recherche de l'intersection avec un rayon. Les détails de construction ICI ne sont pas importants. Votre "façade" se réduit ICI à la super-classe, disons Surface avec la méthode appropriée. Les sphères, plans, cônes, etc. héritent (et modifient) cette méthode, héritent le calcul des normales, etc.
Parfois l'héritage ou super-classes ne suffit pas. Quelques heures après le début du codage, vous vous rendez compte, que TOUT point sur la surface d'une "chose" pourrait projeter des rayons, pour la visibilité / réflexion / éclairage-ombrage... Pas de classe commune. Mais un objet "projeteur des rayons" peut être séparé, et injecté dans la caméra, et dans les surfaces (et dans les lumières) comme une composante. Vous pourrez définir un "chapeau commun", la Scène, qui dirige le comportement des objets individuels de manière plus simple. (D'ailleurs, normalement c'est un exemple d'un autre pattern, le singleton, mais rien de rigide dedans, on peut simultanément opérer sur plusieurs scènes).

Dans cette niche on trouve d'autres patterns, comme les "médiateurs", ou "ponts" (bridges) Il n'est pas infréquent de trouver cette terminologie incohérente.

"Fabriques" (Factories)

Ce pattern est assez populaire en Java: si le programme peut construire, selon les circonstances (par ex., lecture des paramètres, résultats d'autres programmes, etc.), plusieurs objets - modèles de types divers, par ex. des objets géométriques (cercles, rectangles, etc., sous-classes d'une classe abstraite Shape), l'architecture se complique. Le programme peut avoir trop trop de choses à gérer, si des nouveaux modèles arrivent, quelques uns deviennent obsolètes.

Il est préférable de déléguer tout à une classe spéciale qui construit les objets, et les rend à l'utilisateur. Les variantes spécifiques sont construites par des sous-classes de la Fabrique. Tous ces problèmes sont liés assez fort avec la structure de Java. En Python la classe abstraite elle-même peut servir de "Factory", si on y insiste, au lieu de lancer Triangle(...), l'utilisateur appelle Shape.forme('triangle',...). Pas besoin d'une classe spéciale, on définit une méthode créatrice (statique par ex.) forme(...), et on gère facilement par ex., les couleurs par défaut, etc. D'ailleurs, les classes sont des objets dynamiques, qui peuvent être créés pendant l'exécution de programme par type ou autres méta-classes, donc l'utilisateur peut créer des nouveaux modèles (formes) à volonté. Je n'ai jamais vu un exemple utile de Factory en Python.


Vous serez obligés d'apprendre les patterns en étudiant Java, c'et la raison principale de cette section. Mais je répète : dans ce domaine il y a trop de doctrinalisme, et si vous avez un peu de temps à consacrer à l'étude de Python vraiment avancé afin d'écrire des programmes élégants et efficaces, la voie des "patterns classiques" n'est pas naturelle.

Apprenez la structure des classes et métaclasses, quelques détails du code niveau machine virtuelle, et surtout apprenez tout possible sur les descripteurs, qui établissent la relation entre les données (les variables et leurs valeurs) et leur comportement, de manière considérablement plus profonde que la constatation "une variable est l'accès à envirDict['variable']". (Lire aussi cela).