Modèles de conception Python : pour un code élégant et à la mode
Publié: 2022-03-11Répétons-le : Python est un langage de programmation de haut niveau avec typage dynamique et liaison dynamique. Je le décrirais comme un langage dynamique puissant et de haut niveau. De nombreux développeurs sont amoureux de Python en raison de sa syntaxe claire, de ses modules et packages bien structurés, ainsi que de son énorme flexibilité et de sa gamme de fonctionnalités modernes.
En Python, rien ne vous oblige à écrire des classes et à en instancier des objets. Si vous n'avez pas besoin de structures complexes dans votre projet, vous pouvez simplement écrire des fonctions. Mieux encore, vous pouvez écrire un script plat pour exécuter une tâche simple et rapide sans structurer le code du tout.
En même temps, Python est un langage 100 % orienté objet. Comment est-ce? Eh bien, tout simplement, tout en Python est un objet. Les fonctions sont des objets, des objets de première classe (quoi que cela signifie). Ce fait que les fonctions sont des objets est important, alors souvenez-vous-en.
Ainsi, vous pouvez écrire des scripts simples en Python, ou simplement ouvrir le terminal Python et exécuter des instructions directement là (c'est tellement utile !). Mais en même temps, vous pouvez créer des frameworks complexes, des applications, des bibliothèques, etc. Vous pouvez faire tellement de choses en Python. Il y a bien sûr un certain nombre de limitations, mais ce n'est pas le sujet de cet article.
Cependant, comme Python est si puissant et flexible, nous avons besoin de règles (ou modèles) lors de la programmation. Voyons donc quels sont les modèles et comment ils se rapportent à Python. Nous procéderons également à l'implémentation de quelques modèles de conception Python essentiels.
Pourquoi Python est-il bon pour les modèles ?
Tout langage de programmation est bon pour les modèles. En fait, les modèles doivent être considérés dans le contexte d'un langage de programmation donné. Les modèles, la syntaxe du langage et la nature imposent des limites à notre programmation. Les limitations qui proviennent de la syntaxe du langage et de la nature du langage (dynamique, fonctionnel, orienté objet, etc.) peuvent différer, tout comme les raisons de leur existence. Les limitations provenant des modèles sont là pour une raison, elles ont un but. C'est l'objectif fondamental des motifs ; pour nous dire comment faire quelque chose et comment ne pas le faire. Nous parlerons des modèles, et en particulier des modèles de conception Python, plus tard.
La philosophie de Python repose sur l'idée de meilleures pratiques bien pensées. Python est un langage dynamique (l'ai-je déjà dit ?) et, en tant que tel, implémente déjà, ou facilite l'implémentation, un certain nombre de modèles de conception populaires avec quelques lignes de code. Certains modèles de conception sont intégrés à Python, nous les utilisons donc même sans le savoir. D'autres modèles ne sont pas nécessaires en raison de la nature de la langue.
Par exemple, Factory est un modèle de conception Python structurel visant à créer de nouveaux objets, en cachant la logique d'instanciation à l'utilisateur. Mais la création d'objets en Python est dynamique par conception, donc des ajouts comme Factory ne sont pas nécessaires. Bien sûr, vous êtes libre de l'implémenter si vous le souhaitez. Il pourrait y avoir des cas où cela serait vraiment utile, mais ils sont une exception, pas la norme.
Qu'y a-t-il de si bon dans la philosophie de Python ? Commençons par ceci (explorez-le dans le terminal Python):
> >> import this The Zen of Python, by Tim Peters Beautiful is better than ugly. Explicit is better than implicit. Simple is better than complex. Complex is better than complicated. Flat is better than nested. Sparse is better than dense. Readability counts. Special cases aren't special enough to break the rules. Although practicality beats purity. Errors should never pass silently. Unless explicitly silenced. In the face of ambiguity, refuse the temptation to guess. There should be one-- and preferably only one --obvious way to do it. Although that way may not be obvious at first unless you're Dutch. Now is better than never. Although never is often better than *right* now. If the implementation is hard to explain, it's a bad idea. If the implementation is easy to explain, it may be a good idea. Namespaces are one honking great idea -- let's do more of those!
Ce ne sont peut-être pas des modèles au sens traditionnel, mais ce sont des règles qui définissent l'approche "Pythonic" de la programmation de la manière la plus élégante et la plus utile.
Nous avons également des directives de code PEP-8 qui aident à structurer notre code. C'est un must pour moi, avec quelques exceptions appropriées, bien sûr. Soit dit en passant, ces exceptions sont encouragées par PEP-8 lui-même :
Mais le plus important : sachez quand être incohérent – parfois, le guide de style ne s'applique tout simplement pas. En cas de doute, utilisez votre meilleur jugement. Regardez d'autres exemples et décidez ce qui vous convient le mieux. Et n'hésitez pas à demander !
Combinez PEP-8 avec The Zen of Python (également un PEP - PEP-20), et vous aurez une base parfaite pour créer du code lisible et maintenable. Ajoutez des modèles de conception et vous êtes prêt à créer tout type de système logiciel avec cohérence et évolutivité.
Modèles de conception Python
Qu'est-ce qu'un modèle de conception ?
Tout commence avec le Gang of Four (GOF). Effectuez une recherche rapide en ligne si vous n'êtes pas familier avec le GOF.
Les modèles de conception sont un moyen courant de résoudre des problèmes bien connus. Deux grands principes sont à la base des design patterns définis par le GOF :
- Programmez une interface et non une implémentation.
- Privilégiez la composition d'objets à l'héritage.
Examinons de plus près ces deux principes du point de vue des programmeurs Python.
Programmer sur une interface et non sur une implémentation
Pensez à Duck Typing. En Python, nous n'aimons pas définir des interfaces et des classes de programme en fonction de ces interfaces, n'est-ce pas ? Mais, écoutez-moi ! Cela ne signifie pas que nous ne pensons pas aux interfaces, en fait avec Duck Typing nous le faisons tout le temps.
Disons quelques mots sur la tristement célèbre approche Duck Typing pour voir comment elle s'inscrit dans ce paradigme : du programme à une interface.
Nous ne nous soucions pas de la nature de l'objet, nous n'avons pas à nous soucier de ce qu'est l'objet ; nous voulons juste savoir s'il est capable de faire ce dont nous avons besoin (nous ne sommes intéressés que par l'interface de l'objet).
L'objet peut-il charlataniser ? Alors, laissez-le coincer!
try: bird.quack() except AttributeError: self.lol()
Avons-nous défini une interface pour notre canard ? Non! Avons-nous programmé l'interface au lieu de l'implémentation ? Oui! Et, je trouve cela tellement agréable.
Comme le souligne Alex Martelli dans sa présentation bien connue sur les Design Patterns en Python, « Apprendre aux canards à taper prend du temps, mais vous fait économiser beaucoup de travail par la suite !
Privilégier la composition d'objets à l'héritage
C'est ce que j'appelle un principe Pythonique ! J'ai créé moins de classes/sous-classes par rapport à l'emballage d'une classe (ou plus souvent, plusieurs classes) dans une autre classe.
Au lieu de faire ceci :
class User(DbObject): pass
Nous pouvons faire quelque chose comme ceci :
class User: _persist_methods = ['get', 'save', 'delete'] def __init__(self, persister): self._persister = persister def __getattr__(self, attribute): if attribute in self._persist_methods: return getattr(self._persister, attribute)
Les avantages sont évidents. Nous pouvons restreindre les méthodes de la classe enveloppée à exposer. Nous pouvons injecter l'instance de persister dans le runtime ! Par exemple, aujourd'hui c'est une base de données relationnelle, mais demain ce pourrait être n'importe quoi, avec l'interface dont nous avons besoin (encore ces fichus canards).
La composition est élégante et naturelle pour Python.
Modèles comportementaux
Les modèles comportementaux impliquent la communication entre les objets, la façon dont les objets interagissent et accomplissent une tâche donnée. Selon les principes du GOF, il existe au total 11 modèles de comportement en Python : chaîne de responsabilité, commande, interprète, itérateur, médiateur, mémento, observateur, état, stratégie, modèle, visiteur.
Je trouve ces modèles très utiles, mais cela ne signifie pas que les autres groupes de modèles ne le sont pas.
Itérateur
Les itérateurs sont intégrés à Python. C'est l'une des caractéristiques les plus puissantes de la langue. Il y a des années, j'ai lu quelque part que les itérateurs rendaient Python génial, et je pense que c'est toujours le cas. Apprenez-en suffisamment sur les itérateurs et les générateurs Python et vous saurez tout ce dont vous avez besoin sur ce modèle Python particulier.
Chaîne de responsabilité
Ce modèle nous donne un moyen de traiter une demande en utilisant différentes méthodes, chacune traitant une partie spécifique de la demande. Vous savez, l'un des meilleurs principes pour un bon code est le principe de responsabilité unique .
Chaque morceau de code doit faire une, et une seule, chose.
Ce principe est profondément intégré dans ce modèle de conception.
Par exemple, si nous voulons filtrer du contenu, nous pouvons implémenter différents filtres, chacun effectuant un type de filtrage précis et clairement défini. Ces filtres peuvent être utilisés pour filtrer les mots offensants, les publicités, le contenu vidéo inapproprié, etc.
class ContentFilter(object): def __init__(self, filters=None): self._filters = list() if filters is not None: self._filters += filters def filter(self, content): for filter in self._filters: content = filter(content) return content filter = ContentFilter([ offensive_filter, ads_filter, porno_video_filter]) filtered_content = filter.filter(content)
Commander
C'est l'un des premiers modèles de conception Python que j'ai implémenté en tant que programmeur. Cela me rappelle : les motifs ne s'inventent pas, ils se découvrent . Ils existent, il suffit de les trouver et de les utiliser. J'ai découvert celui-ci pour un projet incroyable que nous avons mis en œuvre il y a de nombreuses années : un éditeur XML WYSIWYM à usage spécial. Après avoir utilisé ce modèle de manière intensive dans le code, j'ai lu plus à ce sujet sur certains sites.
Le modèle de commande est pratique dans les situations où, pour une raison quelconque, nous devons commencer par préparer ce qui sera exécuté, puis l'exécuter si nécessaire. L'avantage est que l'encapsulation d'actions de cette manière permet aux développeurs Python d'ajouter des fonctionnalités supplémentaires liées aux actions exécutées, telles que défaire/rétablir, ou de conserver un historique des actions, etc.
Voyons à quoi ressemble un exemple simple et fréquemment utilisé :
class RenameFileCommand(object): def __init__(self, from_name, to_name): self._from = from_name self._to = to_name def execute(self): os.rename(self._from, self._to) def undo(self): os.rename(self._to, self._from) class History(object): def __init__(self): self._commands = list() def execute(self, command): self._commands.append(command) command.execute() def undo(self): self._commands.pop().undo() history = History() history.execute(RenameFileCommand('docs/cv.doc', 'docs/cv-en.doc')) history.execute(RenameFileCommand('docs/cv1.doc', 'docs/cv-bg.doc')) history.undo() history.undo()
Modèles de création
Commençons par souligner que les modèles de création ne sont pas couramment utilisés en Python. Pourquoi? En raison de la nature dynamique de la langue.
Quelqu'un de plus sage que moi a dit un jour que Factory est intégré à Python. Cela signifie que le langage lui-même nous offre toute la flexibilité dont nous avons besoin pour créer des objets d'une manière suffisamment élégante ; nous avons rarement besoin d'implémenter quoi que ce soit en haut, comme Singleton ou Factory.
Dans un didacticiel sur les modèles de conception Python, j'ai trouvé une description des modèles de conception de création qui indiquait que ces modèles de conception "fournissent un moyen de créer des objets tout en masquant la logique de création, plutôt que d'instancier des objets directement à l'aide d'un nouvel opérateur".

Cela résume à peu près le problème : nous n'avons pas de nouvel opérateur en Python !
Néanmoins, voyons comment nous pouvons en implémenter quelques-uns, si nous pensons que nous pourrions gagner un avantage en utilisant de tels modèles.
Singleton
Le modèle Singleton est utilisé lorsque nous voulons garantir qu'une seule instance d'une classe donnée existe pendant l'exécution. Avons-nous vraiment besoin de ce modèle en Python ? D'après mon expérience, il est plus simple de créer intentionnellement une instance, puis de l'utiliser au lieu d'implémenter le modèle Singleton.
Mais si vous souhaitez l'implémenter, voici quelques bonnes nouvelles : en Python, nous pouvons modifier le processus d'instanciation (ainsi que pratiquement tout le reste). Vous souvenez-vous de la __new__()
que j'ai mentionnée plus tôt ? Nous y voilà:
class Logger(object): def __new__(cls, *args, **kwargs): if not hasattr(cls, '_logger'): cls._logger = super(Logger, cls ).__new__(cls, *args, **kwargs) return cls._logger
Dans cet exemple, Logger est un Singleton.
Voici les alternatives à l'utilisation d'un Singleton en Python :
- Utilisez un module.
- Créez une instance quelque part au niveau supérieur de votre application, peut-être dans le fichier de configuration.
- Passez l'instance à chaque objet qui en a besoin. C'est une injection de dépendance et c'est un mécanisme puissant et facilement maîtrisé.
Injection de dépendance
Je n'ai pas l'intention d'entrer dans une discussion sur la question de savoir si l'injection de dépendances est un modèle de conception, mais je dirai que c'est un très bon mécanisme d'implémentation de couplages lâches, et cela aide à rendre notre application maintenable et extensible. Combinez-le avec Duck Typing et la Force sera avec vous. Toujours.
Je l'ai répertorié dans la section des modèles de création de cet article car il traite de la question de savoir quand (ou mieux encore : où) l'objet est créé. Il est créé à l'extérieur. Mieux vaut dire que les objets ne sont pas du tout créés là où on les utilise, donc la dépendance n'est pas créée là où elle est consommée. Le code consommateur reçoit l'objet créé en externe et l'utilise. Pour plus de référence, veuillez lire la réponse la plus votée à cette question Stackoverflow.
C'est une belle explication de l'injection de dépendance et nous donne une bonne idée du potentiel de cette technique particulière. Fondamentalement, la réponse explique le problème avec l'exemple suivant : ne prenez pas vous-même des choses à boire dans le réfrigérateur, indiquez plutôt un besoin. Dites à vos parents que vous avez besoin de quelque chose à boire pendant le déjeuner.
Python nous offre tout ce dont nous avons besoin pour implémenter cela facilement. Pensez à son implémentation possible dans d'autres langages tels que Java et C#, et vous réaliserez rapidement la beauté de Python.
Prenons un exemple simple d'injection de dépendance :
class Command: def __init__(self, authenticate=None, authorize=None): self.authenticate = authenticate or self._not_authenticated self.authorize = authorize or self._not_autorized def execute(self, user, action): self.authenticate(user) self.authorize(user, action) return action() if in_sudo_mode: command = Command(always_authenticated, always_authorized) else: command = Command(config.authenticate, config.authorize) command.execute(current_user, delete_user_action)
Nous injectons les méthodes d' authentification et d' autorisation dans la classe Command. Tout ce dont la classe Command a besoin est de les exécuter avec succès sans se soucier des détails d'implémentation. De cette façon, nous pouvons utiliser la classe Command avec tous les mécanismes d'authentification et d'autorisation que nous décidons d'utiliser lors de l'exécution.
Nous avons montré comment injecter des dépendances via le constructeur, mais nous pouvons facilement les injecter en définissant directement les propriétés de l'objet, débloquant encore plus de potentiel :
command = Command() if in_sudo_mode: command.authenticate = always_authenticated command.authorize = always_authorized else: command.authenticate = config.authenticate command.authorize = config.authorize command.execute(current_user, delete_user_action)
Il y a beaucoup plus à apprendre sur l'injection de dépendances ; les personnes curieuses chercheraient IoC, par exemple.
Mais avant de faire cela, lisez une autre réponse Stackoverflow, la plus votée à cette question.
Encore une fois, nous venons de démontrer comment la mise en œuvre de ce merveilleux modèle de conception en Python consiste simplement à utiliser les fonctionnalités intégrées du langage.
N'oublions pas ce que tout cela signifie : la technique d'injection de dépendances permet des tests unitaires très flexibles et faciles. Imaginez une architecture où vous pouvez modifier le stockage des données à la volée. Se moquer d'une base de données devient une tâche triviale, n'est-ce pas ? Pour plus d'informations, vous pouvez consulter l'introduction de Toptal à Mocking en Python.
Vous pouvez également rechercher des modèles de conception Prototype , Builder et Factory .
Modèles structurels
Façade
Cela pourrait très bien être le modèle de conception Python le plus célèbre.
Imaginez que vous ayez un système avec un nombre considérable d'objets. Chaque objet offre un riche ensemble de méthodes API. Vous pouvez faire beaucoup de choses avec ce système, mais que diriez-vous de simplifier l'interface ? Pourquoi ne pas ajouter un objet d'interface exposant un sous-ensemble bien pensé de toutes les méthodes d'API ? Une Façade !
Exemple de modèle de conception Python Facade :
class Car(object): def __init__(self): self._tyres = [Tyre('front_left'), Tyre('front_right'), Tyre('rear_left'), Tyre('rear_right'), ] self._tank = Tank(70) def tyres_pressure(self): return [tyre.pressure for tyre in self._tyres] def fuel_level(self): return self._tank.level
Il n'y a pas de surprise, pas d'astuces, la classe Car
est une Facade , et c'est tout.
Adaptateur
Si les façades sont utilisées pour simplifier l'interface, les adaptateurs consistent uniquement à modifier l'interface. Comme utiliser une vache quand le système attend un canard.
Disons que vous avez une méthode de travail pour enregistrer des informations vers une destination donnée. Votre méthode s'attend à ce que la destination ait une méthode write()
(comme chaque objet fichier, par exemple).
def log(message, destination): destination.write('[{}] - {}'.format(datetime.now(), message))
Je dirais que c'est une méthode bien écrite avec injection de dépendances, ce qui permet une grande extensibilité. Supposons que vous souhaitiez vous connecter à un socket UDP plutôt qu'à un fichier, vous savez comment ouvrir ce socket UDP, mais le seul problème est que l'objet socket
n'a pas de méthode write()
. Vous avez besoin d'un adaptateur !
import socket class SocketWriter(object): def __init__(self, ip, port): self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self._ip = ip self._port = port def write(self, message): self._socket.send(message, (self._ip, self._port)) def log(message, destination): destination.write('[{}] - {}'.format(datetime.now(), message)) upd_logger = SocketWriter('1.2.3.4', '9999') log('Something happened', udp_destination)
Mais pourquoi est-ce que je trouve l'adaptateur si important ? Eh bien, lorsqu'il est efficacement combiné avec l'injection de dépendances, cela nous donne une grande flexibilité. Pourquoi modifier notre code bien testé pour prendre en charge de nouvelles interfaces alors que nous pouvons simplement implémenter un adaptateur qui traduira la nouvelle interface en une interface bien connue ?
Vous devez également vérifier et maîtriser les modèles de conception de pont et de proxy , en raison de leur similitude avec adapter . Pensez à leur facilité d'implémentation en Python et réfléchissez aux différentes manières de les utiliser dans votre projet.
Décorateur
Oh quelle chance nous avons ! Les décorateurs sont vraiment sympas, et nous les avons déjà intégrés dans le langage. Ce que j'aime le plus en Python, c'est que son utilisation nous apprend à utiliser les meilleures pratiques. Ce n'est pas que nous n'avons pas à être conscients des meilleures pratiques (et des modèles de conception, en particulier), mais avec Python, j'ai l'impression de suivre les meilleures pratiques, malgré tout. Personnellement, je trouve que les meilleures pratiques Python sont intuitives et une seconde nature, et c'est quelque chose d'apprécié par les développeurs novices et d'élite.
Le modèle de décorateur consiste à introduire des fonctionnalités supplémentaires et, en particulier, à le faire sans utiliser l'héritage.
Voyons donc comment nous décorons une méthode sans utiliser la fonctionnalité Python intégrée. Voici un exemple simple.
def execute(user, action): self.authenticate(user) self.authorize(user, action) return action()
Ce qui n'est pas si bon ici, c'est que la fonction d' execute
fait bien plus que d'exécuter quelque chose. Nous ne suivons pas le principe de responsabilité unique à la lettre.
Ce serait bien d'écrire simplement ce qui suit :
def execute(action): return action()
Nous pouvons implémenter n'importe quelle fonctionnalité d'autorisation et d'authentification à un autre endroit, dans un décorateur , comme ceci :
def execute(action, *args, **kwargs): return action() def autheticated_only(method): def decorated(*args, **kwargs): if check_authenticated(kwargs['user']): return method(*args, **kwargs) else: raise UnauthenticatedError return decorated def authorized_only(method): def decorated(*args, **kwargs): if check_authorized(kwargs['user'], kwargs['action']): return method(*args, **kwargs) else: raise UnauthorizeddError return decorated execute = authenticated_only(execute) execute = authorized_only(execute)
Maintenant la méthode execute()
est :
- Simple à lire
- Ne fait qu'une chose (au moins en regardant le code)
- Est décoré d'authentification
- Est décoré avec autorisation
Nous écrivons la même chose en utilisant la syntaxe du décorateur intégré de Python :
def autheticated_only(method): def decorated(*args, **kwargs): if check_authenticated(kwargs['user']): return method(*args, **kwargs ) else: raise UnauthenticatedError return decorated def authorized_only(method): def decorated(*args, **kwargs): if check_authorized(kwargs['user'], kwargs['action']): return method(*args, **kwargs) else: raise UnauthorizedError return decorated @authorized_only @authenticated_only def execute(action, *args, **kwargs): return action()
Il est important de noter que vous n'êtes pas limité aux fonctions de décorateurs. Un décorateur peut impliquer des classes entières. La seule exigence est qu'ils doivent être callables . Mais nous n'avons aucun problème avec cela; nous avons juste besoin de définir la __call__(self)
.
Vous voudrez peut-être aussi regarder de plus près le module functools de Python. Il y a beaucoup à y découvrir !
Conclusion
J'ai montré à quel point il est naturel et facile d'utiliser les modèles de conception de Python, mais j'ai également montré à quel point la programmation en Python devrait également être facile.
« Simple vaut mieux que complexe », vous en souvenez-vous ? Peut-être avez-vous remarqué qu'aucun des modèles de conception n'est entièrement et formellement décrit. Aucune implémentation complexe à grande échelle n'a été présentée. Vous devez les « sentir » et les mettre en œuvre de la manière qui correspond le mieux à votre style et à vos besoins. Python est un excellent langage et il vous donne toute la puissance dont vous avez besoin pour produire du code flexible et réutilisable.
Cependant, cela vous donne plus que cela. Cela vous donne la "liberté" d'écrire du code vraiment mauvais . Ne le faites pas ! Ne vous répétez pas (DRY) et n'écrivez jamais de lignes de code de plus de 80 caractères. Et n'oubliez pas d'utiliser des modèles de conception le cas échéant ; c'est l'un des meilleurs moyens d'apprendre des autres et de bénéficier gratuitement de leur riche expérience.