Modele de design Python: pentru un cod elegant și la modă

Publicat: 2022-03-11

Să o spunem din nou: Python este un limbaj de programare de nivel înalt cu tastare dinamică și legare dinamică. L-aș descrie ca un limbaj dinamic puternic, la nivel înalt. Mulți dezvoltatori sunt îndrăgostiți de Python datorită sintaxei sale clare, modulelor și pachetelor bine structurate, precum și pentru flexibilitatea enormă și gama de caracteristici moderne.

În Python, nimic nu vă obligă să scrieți clase și să instanțiați obiecte din ele. Dacă nu aveți nevoie de structuri complexe în proiectul dvs., puteți scrie doar funcții. Și mai bine, puteți scrie un script plat pentru a executa o sarcină simplă și rapidă, fără a structura codul deloc.

În același timp, Python este un limbaj 100% orientat pe obiecte. Cum e? Ei bine, pur și simplu, totul în Python este un obiect. Funcțiile sunt obiecte, obiecte de primă clasă (indiferent ce înseamnă asta). Acest fapt despre funcțiile ca obiecte este important, așa că vă rugăm să rețineți.

Deci, puteți scrie scripturi simple în Python sau pur și simplu deschideți terminalul Python și executați instrucțiuni chiar acolo (este atât de util!). Dar, în același timp, puteți crea cadre complexe, aplicații, biblioteci și așa mai departe. Puteți face atât de multe în Python. Există, desigur, o serie de limitări, dar nu acesta este subiectul acestui articol.

Cu toate acestea, deoarece Python este atât de puternic și flexibil, avem nevoie de anumite reguli (sau modele) atunci când programăm în el. Deci, să vedem ce sunt modelele și cum se leagă ele cu Python. De asemenea, vom continua să implementăm câteva modele de design Python esențiale.

De ce este Python bun pentru modele?

Orice limbaj de programare este bun pentru tipare. De fapt, modelele ar trebui luate în considerare în contextul oricărui limbaj de programare dat. Atât tiparele, sintaxa limbajului, cât și natura impun limitări programării noastre. Limitările care provin din sintaxa limbajului și natura limbajului (dinamic, funcțional, orientat pe obiect și altele asemenea) pot diferi, la fel ca și motivele din spatele existenței lor. Limitările care vin de la tipare sunt acolo cu un motiv, sunt cu scop. Acesta este scopul de bază al tiparelor; să ne spună cum să facem ceva și cum să nu-l facem. Vom vorbi mai târziu despre modele și, în special, despre modele de design Python.

Python este un limbaj dinamic și flexibil. Modelele de design Python sunt o modalitate excelentă de a-și valorifica potențialul vast.

Python este un limbaj dinamic și flexibil. Modelele de design Python sunt o modalitate excelentă de a-și valorifica potențialul vast.
Tweet

Filosofia lui Python este construită pe baza ideii de bune practici bine gândite. Python este un limbaj dinamic (am spus deja asta?) și, ca atare, implementează deja, sau îl face ușor de implementat, o serie de modele de design populare cu câteva linii de cod. Unele modele de design sunt integrate în Python, așa că le folosim chiar și fără să știm. Alte modele nu sunt necesare din cauza naturii limbajului.

De exemplu, Factory este un model de design structural Python care vizează crearea de noi obiecte, ascunzând logica de instanțiere de utilizator. Dar crearea de obiecte în Python este dinamică prin proiectare, astfel încât adăugiri precum Factory nu sunt necesare. Desigur, sunteți liber să îl implementați dacă doriți. Ar putea exista cazuri în care ar fi cu adevărat util, dar sunt o excepție, nu o normă.

Ce este atât de bun la filosofia lui Python? Să începem cu asta (explorați-l în terminalul 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!

Acestea s-ar putea să nu fie modele în sensul tradițional, dar acestea sunt reguli care definesc abordarea „Pythonic” a programării în cel mai elegant și util mod.

Avem, de asemenea, reguli de cod PEP-8 care ajută la structurarea codului nostru. Este o necesitate pentru mine, cu unele excepții adecvate, desigur. Apropo, aceste excepții sunt încurajate de PEP-8 însuși:

Dar cel mai important: știi când să fii inconsecvent – ​​uneori ghidul de stil pur și simplu nu se aplică. Când aveți îndoieli, folosiți cea mai bună judecată. Privește alte exemple și decide ce arată cel mai bine. Și nu ezita să întrebi!

Combinați PEP-8 cu The Zen of Python (de asemenea, un PEP - PEP-20) și veți avea o bază perfectă pentru a crea cod care poate fi citit și întreținut. Adăugați modele de design și sunteți gata să creați orice tip de sistem software cu consistență și evoluție.

Modele de design Python

Ce este un model de design?

Totul începe cu Gang of Four (GOF). Faceți o căutare rapidă online dacă nu sunteți familiarizat cu GOF.

Modelele de proiectare sunt o modalitate obișnuită de a rezolva probleme bine cunoscute. Două principii principale se află în bazele modelelor de design definite de GOF:

  • Program la o interfață nu o implementare.
  • Preferați compoziția obiectului în detrimentul moștenirii.

Să aruncăm o privire mai atentă la aceste două principii din perspectiva programatorilor Python.

Program la o interfață nu o implementare

Gândiți-vă la Duck Typing. În Python nu ne place să definim interfețe și clase de programe în funcție de aceste interfețe, nu-i așa? Dar, ascultă-mă! Asta nu înseamnă că nu ne gândim la interfețe, de fapt cu Duck Typing facem asta tot timpul.

Să spunem câteva cuvinte despre infama abordare Duck Typing pentru a vedea cum se încadrează în această paradigmă: programați la o interfață.

Dacă arată ca o rață și șarlată ca o rață, este o rață!

Dacă arată ca o rață și șarlată ca o rață, este o rață!
Tweet

Nu ne deranjam cu natura obiectului, nu trebuie să ne pese care este obiectul; vrem doar să știm dacă este capabil să facă ceea ce avem nevoie (ne interesează doar interfața obiectului).

Poate obiectul șarlatan? Așa că, lasă-l să târlănească!

 try: bird.quack() except AttributeError: self.lol()

Am definit o interfață pentru rata noastră? Nu! Am programat pe interfață în loc de implementare? Da! Și mie mi se pare atât de drăguț.

După cum subliniază Alex Martelli în binecunoscuta sa prezentare despre Design Patterns în Python, „Învățarea rațelor să tasteze durează ceva timp, dar te economisește multă muncă după aceea!”

Preferați compoziția obiectului în detrimentul moștenirii

Acum, asta numesc un principiu Pythonic ! Am creat mai puține clase/subclase în comparație cu împachetarea unei clase (sau mai des, mai multe clase) într-o altă clasă.

În loc să faci asta:

 class User(DbObject): pass

Putem face ceva de genul acesta:

 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)

Avantajele sunt evidente. Putem restricționa ce metode din clasa wrapped să expunem. Putem injecta instanța persistentă în timpul de execuție! De exemplu, astăzi este o bază de date relațională, dar mâine ar putea fi orice, cu interfața de care avem nevoie (din nou acele rațe plictisitoare).

Compoziția este elegantă și naturală pentru Python.

Tipare comportamentale

Tiparele comportamentale implică comunicarea între obiecte, modul în care obiectele interacționează și îndeplinesc o anumită sarcină. Conform principiilor GOF, există un total de 11 modele de comportament în Python: Lanț de responsabilitate, Comandă, Interpret, Iterator, Mediator, Memento, Observator, Stat, Strategie, Șablon, Vizitator.

Consider că aceste modele sunt foarte utile, dar asta nu înseamnă că celelalte grupuri de modele nu sunt.

Iterator

Iteratoarele sunt încorporate în Python. Aceasta este una dintre cele mai puternice caracteristici ale limbii. Cu ani în urmă, am citit undeva că iteratoarele fac Python minunat și cred că acesta este încă cazul. Aflați suficient despre iteratoarele și generatoarele Python și veți ști tot ce aveți nevoie despre acest model special Python.

Lanț de responsabilitate

Acest model ne oferă o modalitate de a trata o solicitare folosind diferite metode, fiecare adresându-se unei anumite părți a cererii. Știi, unul dintre cele mai bune principii pentru un cod bun este principiul responsabilității unice .

Fiecare bucată de cod trebuie să facă un lucru, și doar unul.

Acest principiu este profund integrat în acest model de design.

De exemplu, dacă dorim să filtram un anumit conținut putem implementa diferite filtre, fiecare făcând un tip de filtrare precis și clar definit. Aceste filtre ar putea fi folosite pentru a filtra cuvintele jignitoare, reclamele, conținutul video nepotrivit și așa mai departe.

 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)

Comanda

Acesta este unul dintre primele modele de design Python pe care le-am implementat ca programator. Asta îmi amintește: modelele nu sunt inventate, ele sunt descoperite . Ele există, trebuie doar să le găsim și să le folosim. L-am descoperit pe acesta pentru un proiect uimitor pe care l-am implementat cu mulți ani în urmă: un editor XML WYSIWYM cu scop special. După ce am folosit intens acest model în cod, am citit mai multe despre el pe unele site-uri.

Modelul de comandă este la îndemână în situațiile în care, dintr-un motiv oarecare, trebuie să începem prin a pregăti ceea ce va fi executat și apoi să-l executăm atunci când este necesar. Avantajul este că încapsularea acțiunilor într-un astfel de mod permite dezvoltatorilor Python să adauge funcționalități suplimentare legate de acțiunile executate, cum ar fi anularea/refacerea sau păstrarea unui istoric al acțiunilor și altele asemenea.

Să vedem cum arată un exemplu simplu și folosit frecvent:

 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()

Modele Creaționale

Să începem prin a sublinia că modelele de creație nu sunt utilizate în mod obișnuit în Python. De ce? Din cauza naturii dinamice a limbii.

Cineva mai înțelept decât mine a spus odată că Factory este integrată în Python. Înseamnă că limbajul în sine ne oferă toată flexibilitatea de care avem nevoie pentru a crea obiecte într-un mod suficient de elegant; rareori trebuie să implementăm ceva deasupra, cum ar fi Singleton sau Factory.

Într-un tutorial Python Design Patterns, am găsit o descriere a modelelor de design creațional care spunea că aceste modele de design oferă o modalitate de a crea obiecte în timp ce ascund logica creării, mai degrabă decât instanțiarea obiectelor direct folosind un nou operator.

Asta rezumă destul de mult problema: nu avem un operator nou în Python!

Cu toate acestea, să vedem cum putem implementa câteva, dacă simțim că am putea obține un avantaj utilizând astfel de modele.

Singleton

Modelul Singleton este folosit atunci când dorim să garantăm că există o singură instanță a unei clase date în timpul rulării. Chiar avem nevoie de acest model în Python? Pe baza experienței mele, este mai ușor să creezi o instanță în mod intenționat și apoi să o folosești în loc să implementezi modelul Singleton.

Dar dacă doriți să o implementați, iată o veste bună: în Python, putem modifica procesul de instanțiere (împreună cu aproape orice altceva). Vă amintiți metoda __new__() pe care am menționat-o mai devreme? Începem:

 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

În acest exemplu, Logger este un Singleton.

Acestea sunt alternativele la utilizarea unui Singleton în Python:

  • Utilizați un modul.
  • Creați o instanță undeva la nivelul superior al aplicației dvs., poate în fișierul de configurare.
  • Transmite instanța fiecărui obiect care are nevoie de ea. Aceasta este o injecție de dependență și este un mecanism puternic și ușor de stăpânit.

Injecție de dependență

Nu intenționez să intru într-o discuție dacă injecția de dependență este un model de proiectare, dar voi spune că este un mecanism foarte bun de implementare a cuplurilor libere și ajută la menținerea și extensia aplicației noastre. Combină-l cu Duck Typing și Forța va fi cu tine. Mereu.

L-am enumerat în secțiunea de model de creație a acestei postări pentru că tratează întrebarea când (sau și mai bine: unde) este creat obiectul. Este creat în exterior. Mai bine să spunem că obiectele nu sunt create deloc acolo unde le folosim, deci dependența nu se creează acolo unde este consumată. Codul consumatorului primește obiectul creat extern și îl folosește. Pentru referințe suplimentare, vă rugăm să citiți răspunsul cel mai votat la această întrebare Stackoverflow.

Este o explicație frumoasă a injectării dependenței și ne oferă o idee bună despre potențialul acestei tehnici specifice. Practic, răspunsul explică problema cu următorul exemplu: Nu obțineți singuri lucruri de băut din frigider, menționați o nevoie. Spune-le părinților tăi că ai nevoie de ceva de băut la prânz.

Python ne oferă tot ce avem nevoie pentru a implementa atât de ușor. Gândiți-vă la posibila sa implementare în alte limbi, cum ar fi Java și C#, și vă veți da seama rapid de frumusețea Python.

Să ne gândim la un exemplu simplu de injectare a dependenței:

 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)

Injectăm metodele de autentificare și autorizare în clasa Command. Tot ce are nevoie clasa Command este să le execute cu succes fără a se deranja cu detaliile implementării. În acest fel, putem folosi clasa Command cu orice mecanisme de autentificare și autorizare pe care decidem să le folosim în timpul rulării.

Am arătat cum să injectăm dependențe prin constructor, dar le putem injecta cu ușurință setând direct proprietățile obiectului, deblocând și mai mult potențial:

 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)

Există mult mai multe de învățat despre injectarea dependenței; oamenii curioși ar căuta IoC, de exemplu.

Dar înainte de a face asta, citiți un alt răspuns Stackoverflow, cel mai votat la această întrebare.

Din nou, tocmai am demonstrat cum implementarea acestui model minunat de design în Python este doar o chestiune de utilizare a funcționalităților încorporate ale limbajului.

Să nu uităm ce înseamnă toate acestea: tehnica de injectare a dependenței permite testarea unitară foarte flexibilă și ușoară. Imaginați-vă o arhitectură în care puteți modifica stocarea datelor din mers. A bate joc de o bază de date devine o sarcină banală, nu-i așa? Pentru mai multe informații, puteți consulta Introducerea lui Toptal la batjocură în Python.

De asemenea, este posibil să doriți să căutați modele de proiectare Prototip , Constructor și Fabrică .

Modele structurale

Faţadă

Acesta poate foarte bine să fie cel mai faimos model de design Python.

Imaginați-vă că aveți un sistem cu un număr considerabil de obiecte. Fiecare obiect oferă un set bogat de metode API. Puteți face o mulțime de lucruri cu acest sistem, dar ce ziceți de simplificarea interfeței? De ce să nu adăugați un obiect de interfață care să expună un subset bine gândit al tuturor metodelor API? O fațadă!

Fațada este un model elegant de design Python. Este o modalitate perfectă de a simplifica interfața.

Fațada este un model elegant de design Python. Este o modalitate perfectă de a simplifica interfața.
Tweet

Exemplu de model de design 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

Nu există nicio surpriză, nici trucuri, clasa Car este o Fațadă și atât.

Adaptor

Dacă fațadele sunt folosite pentru simplificarea interfeței, adaptoarele sunt toate despre modificarea interfeței. Ca și cum ai folosi o vacă când sistemul așteaptă o rață.

Să presupunem că aveți o metodă de lucru pentru înregistrarea informațiilor către o anumită destinație. Metoda dvs. se așteaptă ca destinația să aibă o metodă write() (cum are fiecare obiect fișier, de exemplu).

 def log(message, destination): destination.write('[{}] - {}'.format(datetime.now(), message))

Aș spune că este o metodă bine scrisă cu injecție de dependență, care permite o mare extensibilitate. Să presupunem că doriți să vă conectați la un socket UDP în loc la un fișier, știți cum să deschideți acest socket UDP, dar singura problemă este că obiectul socket nu are o metodă write() . Ai nevoie de un adaptor !

 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)

Dar de ce mi se pare că adaptorul este atât de important? Ei bine, atunci când este combinat eficient cu injecția de dependență, ne oferă o flexibilitate uriașă. De ce să modificăm codul nostru bine testat pentru a suporta interfețe noi când putem implementa doar un adaptor care va traduce noua interfață în cea binecunoscută?

De asemenea, ar trebui să verificați și să stăpâniți modelele de design bridge și proxy , datorită asemănării lor cu adaptorul . Gândiți-vă cât de ușor sunt de implementat în Python și gândiți-vă la diferite moduri în care le puteți utiliza în proiectul dvs.

Decorator

O, ce norocoși suntem! Decoratorii sunt foarte drăguți și îi avem deja integrati în limbaj. Ceea ce îmi place cel mai mult în Python este că folosirea lui ne învață să folosim cele mai bune practici. Nu este că nu trebuie să fim conștienți de cele mai bune practici (și de modele de design, în special), dar cu Python simt că urmez cele mai bune practici, indiferent. Personal, consider că cele mai bune practici Python sunt intuitive și a doua natură, iar acesta este ceva apreciat deopotrivă de dezvoltatorii începători și de elită.

Modelul de decorator se referă la introducerea de funcționalități suplimentare și, în special, de a o face fără a folosi moștenirea.

Deci, haideți să vedem cum decoram o metodă fără a folosi funcționalitatea Python încorporată. Iată un exemplu simplu.

 def execute(user, action): self.authenticate(user) self.authorize(user, action) return action()

Ceea ce nu este atât de bine aici este că funcția execute face mult mai mult decât executarea a ceva. Nu respectăm principiul responsabilitatii unice la litera.

Ar fi bine să scrieți doar următoarele:

 def execute(action): return action()

Putem implementa orice funcționalitate de autorizare și autentificare în alt loc, într-un decorator , astfel:

 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)

Acum metoda execute() este:

  • Simplu de citit
  • Face un singur lucru (cel puțin când se uită la cod)
  • Este decorat cu autentificare
  • Este decorat cu autorizatie

Scriem același lucru folosind sintaxa de decorare integrată a lui 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()

Este important să rețineți că nu vă limitați la funcțiile de decoratori. Un decorator poate implica clase întregi. Singura cerință este ca acestea să fie apelabile . Dar nu avem nicio problemă cu asta; trebuie doar să definim metoda __call__(self) .

De asemenea, poate doriți să aruncați o privire mai atentă la modulul functools al lui Python. Sunt multe de descoperit acolo!

Concluzie

Am arătat cât de natural și ușor este să utilizați modelele de design Python, dar am arătat și cum programarea în Python ar trebui să fie de asemenea ușoară.

„Simplu este mai bine decât complex”, îți amintești asta? Poate ați observat că niciunul dintre modelele de design nu este descris complet și formal. Nu au fost prezentate implementări complexe la scară completă. Trebuie să le „simți” și să le implementezi în modul care se potrivește cel mai bine stilului și nevoilor tale. Python este un limbaj grozav și vă oferă toată puterea de care aveți nevoie pentru a produce cod flexibil și reutilizabil.

Cu toate acestea, vă oferă mai mult decât atât. Vă oferă „libertatea” de a scrie cod foarte prost . Nu o face! Nu vă repetați (DRY) și nu scrieți niciodată linii de cod mai lungi de 80 de caractere. Și nu uitați să folosiți modele de design acolo unde este cazul; este una dintre cele mai bune modalități de a învăța de la ceilalți și de a câștiga din bogata lor experiență în mod gratuit.