Kako primijeniti SOLID principe u Pythonu korak po korak

  • SOLID principi pružaju jasnu osnovu za dizajniranje čitljivijeg, održivijeg i skalabilnijeg objektno orijentisanog Python koda.
  • Svaki princip (SRP, OCP, LSP, ISP i DIP) adresira specifičnu vrstu dizajnerskog problema, od loše razdvojenih odgovornosti do krutih zavisnosti.
  • Primjena SOLID-a s klasama, apstrakcijama i ubrizgavanjem zavisnosti u Pythonu smanjuje spajanje, poboljšava testabilnost i olakšava evoluciju sistema.

Čvrsto u Pythonu

Kada počnete raditi na velikim Python projektima, jedna od prvih stvari koje primijetite je da Kod postaje teško razumjeti, testirati i proširiti. Ako ne slijedite nekoliko osnovnih pravila dizajna, tu dolaze do izražaja poznati SOLID principi: skup najboljih praksi osmišljenih da znatno olakšaju život timu.

Ovi principi su nastali u oblasti klasično objektno orijentisano programiranje (Java, C++, C#, itd.)Ali oni se savršeno uklapaju u Python sve dok koristite klase i objekte na manje-više ozbiljan način. Pogledajmo detaljno šta su, odakle dolaze, zašto su važni i, prije svega, kako Primijenite SOLID s jasnim primjerima u Pythonu kako bi vaš kod bio održiviji, skalabilniji i ugodniji za rad.

Šta je SOLID i odakle sve to dolazi?

Termin SOLID je akronim koji je popularizirao Michael Feathers. grupirati pet principa dizajna koje je prvobitno predložio Robert C. Martin, poznatiji kao Ujak Bob. Ovaj američki softverski inženjer, jedan od potpisnika Agilnog manifesta, objavio je članak "Principi objektno orijentisanog dizajna" sredinom 90-ih, a kasnije "Principi dizajna i obrasci dizajna", gdje je postavio mnoge temelje modernog objektno orijentisanog dizajna.

Vremenom su i drugi autori, kao što su Barbara Liskov i Bertrand Meyer Također su doprinijeli idejama koje su integrirane u ovaj skup principa. Michael Feathers je jednostavno imao (vrlo pronicljivu) ideju da ih preuredi tako da inicijali formiraju riječ SOLID, što im je pomoglo da se šire poput požara u razvojnoj zajednici.

Pet slova u SOLID-u odgovaraju ovim principima objektno orijentisanog dizajna, koji se mogu primijeniti i na Python:

  • S – Princip jedinstvene odgovornosti (Princip jedinstvene odgovornosti)
  • O – Princip otvoreno/zatvoreno (Princip otvorenog/zatvorenog)
  • L – Liskovljev princip supstitucije (Liskovljev princip supstitucije)
  • I – Princip segregacije interfejsa (Princip segregacije interfejsa)
  • D – Princip inverzije zavisnosti (Princip obrnutog odnosa zavisnosti)

Opšta ideja je da ovih pet principa, korištenih zajedno, Pomažu vam da napišete fleksibilan, jednostavan za testiranje i održiv softverTo se prevodi u brže implementacije, manje misterioznih grešaka, bolju ponovnu upotrebu koda i manje glavobolja kada je projekat u produkciji nekoliko godina.

Za šta se koriste SOLID principi u Pythonu?

Primjena SOLID principa u Pythonu nije samo akademska vježba; ona ima direktan utjecaj na svakodnevni rad tima. Kada se pridržavate ovih principa, Oni smanjuju "špageti" kod, smanjuju miris koda i sprečavaju da vaša kodna baza "miriše trulo".koristeći poznatu analogiju, „ako nešto loše miriše, nešto je loše dizajnirano.“ U Windowsu, mnogi programeri se odlučuju za Instalirajte i konfigurirajte WSL2 imati Linux okruženje bliže produkcijskom.

U kolaborativnim okruženjima (timovi za razvoj pozadinskih sistema, inženjering podataka, proizvodi s dugim ciklusima itd.) ovi principi su ključni za Više ljudi može raditi na istoj kodnoj bazi bez prekoračenja svojih granica ili prekida bilo čega pri najmanjem dodiru.Nadalje, Python, iako fleksibilan i dinamičan, omogućava besprijekornu primjenu tipičnih OOP apstrakcija: apstraktnih klasa, hijerarhija nasljeđivanja, kompozicije i interfejsa putem... abc, Itd

Ukratko, SOLID vam pomaže da postignete:

  • Čistiji, čitljiviji kodčak i godinama nakon što ga je napisao.
  • Poboljšana mogućnost testiranjajer su odgovornosti jasno podijeljene.
  • Visoka mogućnost ponovne upotrebe i skalabilnost zahvaljujući manjem broju krutih zavisnosti između modula.
  • Manje kolateralnih grešakaKada nešto promijenite u jednom modulu, nećete slučajno pokvariti pet drugih stvari.

S – Princip jedinstvene odgovornosti

Prvi princip kaže da Razred treba imati samo jedan razlog za promjenu.Drugim riječima, mora preuzeti jednu, dobro definiranu odgovornost. To ne znači imati samo jednu metodu, već da sva njena logika treba da ukazuje na jedan, koherentan cilj.

Zamislite Python klasu koja predstavlja korisnika i, pored pohranjivanja njegovih podataka, također se bavi pristupom bazi podataka i generiranjem izvještaja:

class User:
    def __init__(self, name: str):
        self.name = name

    def get_user_from_database(self, user_id: int) -> dict:
        # Recupera datos desde la base de datos
        # ...
        pass

    def save_user_to_database(self) -> None:
        # Persiste el usuario en la base de datos
        # ...
        pass

    def generate_user_report(self) -> str:
        # Genera un informe del usuario
        # ...
        pass

Evo je razred miješa tri različite odgovornostiPredstavljanje korisnika, upravljanje perzistencijom i izrada izvještaja. Promjene u bazi podataka, formatu izvještaja ili atributima korisnika zahtijevaju modifikaciju iste klase, što povećava rizik od uvođenja unakrsnih grešaka.

Ako odvojimo ove probleme, dizajn se značajno poboljšava:

class User:
    def __init__(self, name: str):
        self.name = name


class UserDB:
    @staticmethod
    def get_user(user_id: int) -> User:
        # Lógica para obtener usuarios de la base de datos
        # ...
        return User("John Doe")

    @staticmethod
    def save_user(user: User) -> None:
        # Lógica para guardar el usuario
        # ...
        pass


class UserReportGenerator:
    @staticmethod
    def generate_report(user: User) -> str:
        # Lógica para generar informes de usuario
        # ...
        return f"Report for user: {user.name}"

Sada razred Korisnik predstavlja korisnika samo kao entitetAko se način generiranja izvještaja promijeni, samo dodirnite UserReportGeneratorAko promijenite bazu podataka, samo dodirnete UserDBSvaka klasa ima jedan razlog za promjenu, što pojednostavljuje otklanjanje grešaka i evoluciju sistema.

SRP primijenjen na realističniji primjer: patke i komunikacija

Pogledajmo adaptirani klasični scenario: razred patka Kojem se, u početku, postepeno dodaju odgovornosti sve dok ne postane čudovište koje je teško održavati. Zamislite naivnu implementaciju:

class Duck:
    def __init__(self, name: str):
        self.name = name

    def fly(self) -> None:
        print(f"{self.name} is flying not very high")

    def swim(self) -> None:
        print(f"{self.name} swims in the lake and quacks")

    def do_sound(self) -> str:
        return "Quack"

    def greet(self, other_duck: "Duck") -> None:
        print(f"{self.name}: {self.do_sound()}, hello {other_duck.name}")

Klasa Trebalo bi ga jednostavno definirati kao "patka"Ali također upravlja načinom na koji međusobno komuniciraju. Ako sutra promijenite logiku razgovora (više fraza, drugi jezici, različiti kanali), morate modificirati klasu duck, koja već dobro funkcionira kao entitet.

Rješenje koje poštuje SRP je izdvajanje te druge odgovornosti iz druge klase specijalizirane za komunikaciju:

class Duck:
    def __init__(self, name: str):
        self.name = name

    def fly(self) -> None:
        print(f"{self.name} is flying not very high")

    def swim(self) -> None:
        print(f"{self.name} swims in the lake and quacks")

    def do_sound(self) -> str:
        return "Quack"


class Communicator:
    def __init__(self, channel: str):
        self.channel = channel

    def communicate(self, duck1: Duck, duck2: Duck) -> None:
        sentence1 = f"{duck1.name}: {duck1.do_sound()}, hello {duck2.name}"
        sentence2 = f"{duck2.name}: {duck2.do_sound()}, hello {duck1.name}"
        conversation = 
        print(*conversation, f"(via {self.channel})", sep="\n")

Zahvaljujući ovom odvajanju, Možete razviti logiku komunikacije bez dodirivanja definicije patkeNadalje, kod je lakše testirati: testirate ponašanje Duck a s druge strane, onaj od Communicatorbez miješanja odgovornosti.

O – Princip otvoreno/zatvoreno

Princip OCP-a kaže da Softverski entiteti trebaju biti otvoreni za proširenje svog ponašanja, ali zatvoreni za direktne modifikacije.Drugim riječima, kada želite dodati novu funkcionalnost, idealno bi bilo da ne morate prepisivati ​​klase koje već rade i koriste ih drugi moduli.

Klasičan primjer je izračunavanje površina geometrijskih figura. Prvo ćemo pogledati verziju koja ne poštuje OCP:

class Rectangle:
    def __init__(self, width: float, height: float):
        self.width = width
        self.height = height


class Circle:
    def __init__(self, radius: float):
        self.radius = radius


class AreaCalculator:
    def calculate_area(self, shape) -> float:
        if isinstance(shape, Rectangle):
            return shape.width * shape.height
        elif isinstance(shape, Circle):
            return 3.14159 * shape.radius * shape.radius
        else:
            raise ValueError("Forma no soportada")

Ako sutra želite dodati trokut, bit ćete prisiljeni modificirati kod AreaCalculatordodavanje još jednog elifOvo krši OCP, jer klasa više nije "zatvorena" za promjene.

Ispravna verzija uključuje uvođenje apstrakcije Shape sa metodom area() koje svaka figura implementira na svoj način:

from abc import ABC, abstractmethod


class Shape(ABC):
    @abstractmethod
    def area(self) -> float:
        pass


class Rectangle(Shape):
    def __init__(self, width: float, height: float):
        self.width = width
        self.height = height

    def area(self) -> float:
        return self.width * self.height


class Circle(Shape):
    def __init__(self, radius: float):
        self.radius = radius

    def area(self) -> float:
        return 3.14159 * self.radius * self.radius


class AreaCalculator:
    def calculate_area(self, shape: Shape) -> float:
        return shape.area()

Zahvaljujući ovom dizajnu, za dodajte trokut koji ne dodirujete AreaCalculatorJednostavno kreirate novu podklasu:

class Triangle(Shape):
    def __init__(self, base: float, height: float):
        self.base = base
        self.height = height

    def area(self) -> float:
        return 0.5 * self.base * self.height

Princip otvoreno/zatvoreno se veoma dobro uklapa u ideju definirati jasne tačke proširenja putem apstrakcije: interfejsi, apstraktne klase, hookovi, itd. U Pythonu, modul abc Omogućava vam da ovo eksplicitno izrazite, čak i ako je jezik dinamičan.

OCP primijenjen na primjer komunikatora

Ako se vratimo na primjer CommunicatorMožemo ići korak dalje i pripremiti dizajn da podržava različite vrste razgovora bez prepisivanja komunikatora svaki put. Da bismo to uradili, definišemo apstrakciju razgovora i omogućavamo komunikatoru da koristi samo nju:

from typing import final
from abc import ABC, abstractmethod


class AbstractConversation(ABC):
    @abstractmethod
    def do_conversation(self) -> list:
        pass


class SimpleConversation(AbstractConversation):
    def __init__(self, duck1: Duck, duck2: Duck):
        self.duck1 = duck1
        self.duck2 = duck2

    def do_conversation(self) -> list:
        sentence1 = f"{self.duck1.name}: {self.duck1.do_sound()}, hello {self.duck2.name}"
        sentence2 = f"{self.duck2.name}: {self.duck2.do_sound()}, hello {self.duck1.name}"
        return 


class Communicator:
    def __init__(self, channel: str):
        self.channel = channel

    @final
    def communicate(self, conversation: AbstractConversation) -> None:
        print(*conversation.do_conversation(), f"(via {self.channel})", sep="\n")

U ovoj verziji, Ako želite dodati novi način razgovora (na primjer, agresivan razgovor, razgovor u kojem se naizmjenično odlučuje, itd.), samo stvarate još jednu podklasu AbstractConversation. Metoda communicate() de Communicator Ne mijenja se, u potpunosti se pridržava OCP-a.

L – Liskovljev princip supstitucije

Liskovljev princip supstitucije, koji je formulisala Barbara Liskov, tvrdi da Podklase bi trebale biti u stanju zamijeniti svoje osnovne klase bez promjene očekivanog ponašanja programa.U praksi, to znači da ako kod radi s jednom instancom osnovne klase, trebao bi jednako dobro raditi i s bilo kojom instancom podklase.

Tipičan primjer kršenja LSP-a je modeliranje svih ptica jednom metodom. fly()uključujući nojeve:

class Bird:
    def fly(self) -> None:
        pass


class Duck(Bird):
    def fly(self) -> None:
        print("¡El pato está volando!")


class Ostrich(Bird):
    def fly(self) -> None:
        # Las avestruces no vuelan
        raise NotImplementedError("Las avestruces no pueden volar")

Bilo koji kod koji pretpostavlja da Svaka ptica koja može letjeti propast će kada dobije noja.. To znači Ostrich Nije valjana zamjena za Bird, čime krši LSP.

Rješenje je prilagoditi hijerarhiju kako bi bolje odražavala stvarnost: ne lete sve ptice, tako da Samo dio ptica bi trebao imati ovu metodu fly():

class Bird:
    pass


class FlyingBird(Bird):
    def fly(self) -> None:
        pass


class Duck(FlyingBird):
    def fly(self) -> None:
        print("¡El pato está volando!")


class Ostrich(Bird):
    # No vuela, así que no implementa fly()
    pass

Ovim dizajnom, Bilo koja funkcija koja zahtijeva leteću pticu će izjaviti da joj je potrebna jedna. FlyingBirdi nikada neće dobiti noja. Na ovaj način se poštuje LSP i izbjegavaju se neočekivani izuzeci tokom izvršavanja.

LSP i razgovori o pticama

Vraćajući se na primjer razgovora, uobičajeno je da se kodiranje započne razmišljajući samo o patkama, a zatim se žele dodati vrane ili druge ptice. Ako klasa razgovora zavisi od Duck, Nećete ga moći ponovo koristiti s drugim vrstama ptica. bez diranja koda:

class Crow:
    # Implementación específica del cuervo
    ...

Si SimpleConversation Tipizirano je samo za patke; ne možete samo propustiti vranu bez da je modificirate. Ispravan pristup je kreiranje zajedničke apstrakcije. Bird i učinite da razgovor zavisi od te apstrakcije:

from abc import ABC, abstractmethod


class Bird(ABC):
    def __init__(self, name: str):
        self.name = name

    @abstractmethod
    def do_sound(self) -> str:
        pass


class Crow(Bird):
    def do_sound(self) -> str:
        return "Caw"


class Duck(Bird):
    def do_sound(self) -> str:
        return "Quack"


class SimpleConversation(AbstractConversation):
    def __init__(self, bird1: Bird, bird2: Bird):
        self.bird1 = bird1
        self.bird2 = bird2

    def do_conversation(self) -> list:
        sentence1 = f"{self.bird1.name}: {self.bird1.do_sound()}, hello {self.bird2.name}"
        sentence2 = f"{self.bird2.name}: {self.bird2.do_sound()}, hello {self.bird1.name}"
        return 

Na ovaj način, bilo koja podklasa Bird koji poštuje ugovor (do_sound()(ime, itd.) je valjana zamjena i neće poremetiti očekivano ponašanje SimpleConversation.

I – Princip segregacije interfejsa

Princip ISP-a tvrdi da Nijedan klijent ne bi trebao biti prisiljen oslanjati se na metode koje ne koristi.Prevedeno u apstraktne klase ili interfejse, ovo znači da je bolje imati nekoliko malih, specifičnih interfejsa nego jedan ogroman, generički interfejs.

Obratite pažnju na ovaj dizajn u kojem interfejs Worker To zahtijeva od svih koji ga primjenjuju da imaju specifične metode rada i prehrane:

from abc import ABC, abstractmethod


class Worker(ABC):
    @abstractmethod
    def work(self) -> None:
        pass

    @abstractmethod
    def eat(self) -> None:
        pass


class Human(Worker):
    def work(self) -> None:
        print("El humano está trabajando")

    def eat(self) -> None:
        print("El humano está comiendo")


class Robot(Worker):
    def work(self) -> None:
        print("El robot está trabajando")

    def eat(self) -> None:
        # El robot no come, pero está obligado a declarar este método
        pass

Klasa Robot se oslanja na metodu eat() to ne trebaBilo kakva promjena vezana za hranu uticaće na robota, čak i ako nema nikakve veze sa tim ponašanjem.

Primjenom ISP-a, podijelili smo interfejs na dva manja, specifičnija:

class Workable(ABC):
    @abstractmethod
    def work(self) -> None:
        pass


class Eatable(ABC):
    @abstractmethod
    def eat(self) -> None:
        pass


class Human(Workable, Eatable):
    def work(self) -> None:
        print("El humano está trabajando")

    def eat(self) -> None:
        print("El humano está comiendo")


class Robot(Workable):
    def work(self) -> None:
        print("El robot está trabajando")

Sada, Svaka klasa implementira samo one metode koje su joj zaista potrebne.Ovo smanjuje spajanje, olakšava evoluciju dizajna i čini kod izražajnijim: postaje vrlo jasno ko šta može da uradi.

ISP u modeliranju ptica: letenje i plivanje

Nešto slično se dešava pri modeliranju ptica koje lete i plivaju. Ako osnovna apstrakcija Bird To zahtijeva implementaciju oba fly() como swim()Završit ćete s predavanjima poput Crow koji se moraju pretvarati da znaju plivati:

class Bird(ABC):
    def __init__(self, name: str):
        self.name = name

    @abstractmethod
    def fly(self) -> None:
        pass

    @abstractmethod
    def swim(self) -> None:
        pass

    @abstractmethod
    def do_sound(self) -> str:
        pass

Rješenje prema ISP-u je odvojite interfejs na specifičnije mogućnosti:

class Bird(ABC):
    def __init__(self, name: str):
        self.name = name

    @abstractmethod
    def do_sound(self) -> str:
        pass


class FlyingBird(Bird):
    @abstractmethod
    def fly(self) -> None:
        pass


class SwimmingBird(Bird):
    @abstractmethod
    def swim(self) -> None:
        pass


class Crow(FlyingBird):
    def fly(self) -> None:
        print(f"{self.name} is flying high and fast!")

    def do_sound(self) -> str:
        return "Caw"


class Duck(SwimmingBird, FlyingBird):
    def fly(self) -> None:
        print(f"{self.name} is flying not very high")

    def swim(self) -> None:
        print(f"{self.name} swims in the lake and quacks")

    def do_sound(self) -> str:
        return "Quack"

Ako ikada odlučite da budete model pingvina, jednostavno Natjeraš ga da naslijedi od SwimmingBird ali ne iz FlyingBirdI nećete morati implementirati prazne metode ili bacati vještačke izuzetke.

D – Princip inverzije zavisnosti

Posljednji princip, DIP, može se sažeti u dvije ključne ideje: Moduli visokog nivoa ne bi trebali zavisiti od modula niskog nivoa; oba bi trebala zavisiti od apstrakcija.I apstrakcije ne bi trebale zavisiti od detalja, već detalji bi trebali zavisiti od apstrakcija.

U praksi, to znači da vaša poslovna logika ne bi trebala biti vezana za specifične detalje poput "Korostim MySQL", "Pišem u lokalnu datoteku" ili "Šaljem SMS poruke s ovim provajderom". Umjesto toga, definirate apstraktni interfejsi (npr. Database, Channel, NotificationService) i natjerate svoj visokonivoski kod da komunicira samo s njima.

Dizajn koji prekid DIP-a Ovo bi bio korisnički repozitorij koji direktno instancira MySQL bazu podataka:

class MySQLDatabase:
    def connect(self) -> None:
        # Conectar a MySQL
        pass

    def query(self, sql: str) -> list:
        # Ejecutar consulta
        return []


class UserRepository:
    def __init__(self) -> None:
        self.database = MySQLDatabase()  # Dependencia directa

    def get_users(self) -> list:
        return self.database.query("SELECT * FROM users")

Ako odlučite da sutra koristite PostgreSQL, morate modificirati klasu visokog nivoa UserRepositoryVezani ste za određeni detalj implementacije.

Primjenom DIP-a, prvo definiramo apstrakciju baze podataka, a zatim nasljeđujemo konkretne implementacije od nje:

from abc import ABC, abstractmethod


class Database(ABC):
    @abstractmethod
    def connect(self) -> None:
        pass

    @abstractmethod
    def query(self, sql: str) -> list:
        pass


class MySQLDatabase(Database):
    def connect(self) -> None:
        # Conexión a MySQL
        pass

    def query(self, sql: str) -> list:
        # Consulta en MySQL
        return []


class PostgreSQLDatabase(Database):
    def connect(self) -> None:
        # Conexión a PostgreSQL
        pass

    def query(self, sql: str) -> list:
        # Consulta en PostgreSQL
        return []


class UserRepository:
    def __init__(self, database: Database) -> None:
        self.database = database  # Depende de una abstracción

    def get_users(self) -> list:
        return self.database.query("SELECT * FROM users")

Na ovaj način, Možete ubrizgati bilo koju implementaciju Database prilikom kreiranja repozitorija, bez diranja njegovog internog koda:

mysql_db = MySQLDatabase()
user_repo = UserRepository(mysql_db)

postgres_db = PostgreSQLDatabase()
user_repo = UserRepository(postgres_db)

Ovaj obrazac je poznat kao Injekcija zavisnosti I to je najčešći način primjene DIP-a: klase ne kreiraju vlastite zavisnosti, već ih primaju izvana (kroz konstruktor ili putem specifičnih metoda), uvijek koristeći apstrakcije kao tip.

DIP primijenjen na kanale i komunikatore

U primjeru razgovora ptica, možemo poboljšati i upravljanje kanalima primjenom DIP-a. Pretpostavimo da definirate jednu apstrakciju za kanal, a drugu za komunikator:

class AbstractChannel(ABC):
    @abstractmethod
    def get_channel_message(self) -> str:
        pass


class AbstractCommunicator(ABC):
    @abstractmethod
    def get_channel(self) -> AbstractChannel:
        pass

    @final
    def communicate(self, conversation: AbstractConversation) -> None:
        print(*conversation.do_conversation(),
              self.get_channel().get_channel_message(),
              sep="\n")

Prva, naivna implementacija bi mogla biti:

class SMSChannel(AbstractChannel):
    def get_channel_message(self) -> str:
        return "(via SMS)"


class SMSCommunicator(AbstractCommunicator):
    def __init__(self) -> None:
        self._channel = SMSChannel()  # Depende de detalle concreto

    def get_channel(self) -> AbstractChannel:
        return self._channel

Iako se čini tačnim, Ovaj komunikator je i dalje direktno povezan sa SMSChannelPoboljšali smo dizajn tako što je komunikator primao kanal izvana (ubrizgavanje zavisnosti), te stoga zavisio samo od apstrakcije:

class SimpleCommunicator(AbstractCommunicator):
    def __init__(self, channel: AbstractChannel) -> None:
        self._channel = channel

    def get_channel(self) -> AbstractChannel:
        return self._channel

S ovim pristupom, svaki novi kanal (e-pošta, push obavještenja itd.) implementira AbstractChannel y Može se koristiti bez promjene koda komunikatora.Opet, klase visokog nivoa zavise od apstrakcija, a ne od detalja.

Šta se dešava kada ignorišete SOLID?

Ako se ovi principi ne uzmu u obzir, kod obično pati od problema kao što su Miris koda, trulež koda i spojnice koje je nemoguće raspetljatiTo jest, ogromne klase sa hiljadu odgovornosti, podklase koje krše ugovore, ciklične zavisnosti i metode koje se mijenjaju svaki drugi dan jer rade previše stvari.

Posljedice su jasne i prilično bolne za bilo koji tim: Više ranjivosti, više grešaka, stalno refaktorisanje i, u najgorem slučaju, kod koji na kraju postane praktično neupotrebljiv.To je ono što se obično naziva "špageti kod": teško ga je pratiti, pun je zakrpa i gotovo ga je nemoguće proširiti bez oštećenja nečeg važnog.

SOLID principi nisu uklesani u kamenu i nije uvijek isplativo primjenjivati ​​ih sve kruto, posebno kod brze izrade prototipa ili vrlo malih projekata. Uprkos tome, Imajte ih na umu i primijenite ih na većinu vašeg objektno orijentiranog dizajna u Pythonu. To pravi razliku između projekta koji se vremenom skalira i onog koji se raspada čim malo naraste.

Najbolja IDE okruženja za programiranje u Windowsu 11
Vezani članak:
Najbolja IDE okruženja za programiranje na Windowsu 11