Python OOP
După o abatere de la Python, am revenit la această temă. Sper că lucrurile pe care le-ați învățat până acum au fost interesante și practice. Mai sper că vi s-a făcut dor de Python-el. ❤️
Warmup (Mai ții minte?)
Iubești listele din Python așa cum le iubesc eu? Atunci te rog să-mi răspunzi ce va afișa fiecare linie:
Splendid! Te provoc să mai experimentezi cu aceste structuri de date, în timpul liber, după această prezentare.
Programare orientată pe obiecte (OOP)
Programarea orientată pe obiecte este o paradigmă de programare care folosește concepte abstracte (sub forma de clase și obiecte) pentru a crea modele bazate pe elemente din lumea reală. Obiectele sunt definite cu propriele lor variabile, numite atribute și careva funcțional reprezentat prin metode. Metodele de obicei operează cu datele interioare ale obiectului, care, la dorință pot fi făcute inaccesibile direct utilizatorului. Mai multă teorie găsești pe Wikipedia. Toate aceste noțiuni pot părea la prima vedere foarte complicate pentru un programator care nu a mai lucrat cu conceptele OOP, doar cu programarea procedurală(Pascal, C) . Totuși cu ajutorul exemplelor practice pe care urmează să le prezint sper ca aceste concepte să devină mult mai simplu de înțeles.
NB: Mini-ghid de utilizare
În această lecție voi scrie codul de bază în fișiere, însă voi experimenta în python shell. Un fișier .py
poate fi încărcat în shell, la deschidere în felul următor:
Și ca rezultat, în loc de $
în terminal, vom avea: >>>
. Suntem într-un interpretator Python.
Clase, obiecte, instanțe
O clasă este o colecție de obiecte care au proprietăți, operații și comportamente comune. O clasă este o combinație de caracteristici (atribute) și de comportamente (metode). Se poate spune că o clasă este schița care descrie natura unui lucru. De exemplu, clasa Câine
va conține caracteristicile tuturor câinilor, precum rasă sau culoarea părului (caracteristici), precum și capacitatea de a lătra și de a sta (comportament).Clasele poartă nume generice, pe când obiectele se numesc specific de obicei. Spre exemplu clasa se numește Cat
iar un obiect al acestei clase e denumit eddy
, probabil numele pisicii date.
Hai să scrim împreună prima noastră clasă:
Asta e tot! Avem o clasă "goală" cu numele Animal
. Cuvântul pass este adesea folosit ca substituent care indică unde va merge în cele din urmă codul. Acest lucru vă permite să rulați acest cod fără ca Python să arunce o eroare.
Având definiția clasei, putem instanția primele obiecte:
Felicitări! Ai creat prima ta clasă și ai mai și creat obiecte cu acea clasă. Bravo! Acum hai să îmbunătățim această clasă.
După cum am zis mai sus, clasele au atribute și metode. Metodele sunt funcții definite în corpul unei clase. Sunt folosite pentru a defini comportamentele unui obiect. Hai să adăugăm niște date și niște metode clasei noastre.
NB: Ca să executăm metoda say()
a unui obiect a
, vom scrie a.say()
.
Acum putem crea un obiect nou a acestei clase și putem invoca metoda say()
. Ai observat probabil că funcția say
are un parametru numit self
. Încă nu-ți explic ce înseamnă, însă tu scrie-l, ca totul să funcționeze.
NB: Dacă nu punem parametrul self
în declarația metodei, vom avea așa o eroare la încercarea de a invoca această metodă:
În câteva momente voi explica și semnificația acestei convenții.
Haideți să creăm un obiect de tip Animal
și să executăm metoda say()
. Să aplicăm în mod practic:
Perfect! Acum clasa noastră are o metodă care mai și funcționează. Și totuși, probabil te întrebi de ce a fost nevoie de self
ca parametru al funcției și de ce la executarea metodei nu am specificat niciun parametru. Curiozitatea ta e foarte actuală.
Self. Pentru ce ne trebuie el?
Convenția de a plasa self
ca parametru formal al metodei este una istorică și e strict în conformitate cu Zen-ul limbajului de programare Python, documentat în PEP 20 și anume "Explicit is better than implicit". În plus, chiar și Guido van Rossum a scris o notiță cu argumentele necesare pentru self
: Why explicit self has to stay.
Eu însă nu am să intru în detalii prea mult, dar am să-ți explic în ce constă convenția dată, pentru ce se folosește și cum am putea beneficia de ea.
Aici am creat din nou clasa Animal
și i-am definit atributul name
. Metoda info()
va prezenta obiectul dat utilizând atributul name
Încercăm în practică ceea ce am scris:
Nu lucrează cum ne-am fi așteptat! 😞
Totuși, vezi că suntem apți să accesăm atributul name
al obiectului nostru. Hai să beneficiem de această facilitate și să rescriem metoda noastră:
Hai să vedem ce ne-a ieșit:
Yay! Merge! Însă, e incorect pentru că facem referință la un obiect cu un nume exact în cadrul metodei noastre și anume:
Aparent lucrurile au luat o întorsătură neașteptată.😕 Însă, nu te întrista! Totul e mai simplu decât te așteptai!
Probabil ți-ai dat seama unde vreau să ajung. Exact, înapoi la self
. Acest parametru formal al metodelor ține în interiorul său referința spre obiectul curent. Utilizând-ul vom scăpa de ambiguitatea în care am intrat în exemplele precedente. Hai să vedem ce ne-a ieșit:
Atâta tot, să vedem în practică:
Acum totul lucrează exact așa cum doream. Mă bucur că ai înțeles semnificația convenției de a utiliza self
NB: În repetate rânduri am pomenit că self
este doar o convenție. În alte limbaje de programare, cum ar fi Java sau C++ poți întâlni o situație similară, doar că pentru acele limbaje cuvântul cheie este this
. Comportamental este cât de cât similar, însă, în Python el este declarat explicit. În concluzie, ține minte, self
, nu este un cuvând magic, nici un cuvânt cheie al limbajului Python. Ca să-ți demonstrez asta, am să redenumesc self
în my_custom_loved_self
, să vedem ce iese din asta:
Superb! Ca să concluzionez, self
este doar o convenție, chiar dacă editorul sau IDE-ul tău îl colorează într-un mod specific. Totuși, te rog să folosești mereu anume self
pentru a denota referința către instanța curentă a obiectului.
Constructor. __init__()
__init__()
Dacă deja cunoști careva limbaj de programare orientat pe obiecte, probabil știi ce-i aia un constructor. Spre exemplu limbaje ca Java sau C++ au constructori impliciți(main). În Python, metoda __init__()
își are ca scop același lucru.
Totuși, nu ți-am povestit încă ce-i aia un constructor. Numele însă, vorbește pentru sine. Constructorul unei clase este o funcție internă, specială a acelei clase care este apelată în momentul în care se creeaza un obiect al acelei clase. Chiar dacă în Python, metoda __init__()
nu este numită constructor, ea se comportă exact ca un constructor tradițional, adică se execută la crearea unui obiect nou. Ceea ce face această metodă să nu fie numită constructor, e faptul că la momentul execuției ei, scheletul obiectului e deja construit în interiorul interpretatorului Python. Din perspectiva programatorului, acest fapt nu are niciun efect advers. Hai să scriem primul nostru constructor:
Atât de simplu. Hai să testăm:
Așa cum te-ai așteptat, la crearea unei instanțe noi, metoda __init__()
a fost executată! Perfect, însă probabil te întrebi, dacă alte metode acceptă parametri adiționali, oare și __init__
-ul poate accepta acești parametri? Răspunsul e: Cu siguranță! Hai să specificăm numele animăluțului nostru la crearea unui obiect nou:
Hai să testăm ce ne-a ieșit, în linia de comandă:
Perfect! Acum poți crea "constructori" utilizând __init__
. Și mai știi cum să adaugi parametri acestei metode.
O clasă generică
Haideți să creăm o clasă ceva mai completă, din nou cu numele Animal
și cu atributele name
și age
.
Hai să rulăm, să vedem ce ne-a ieșit:
"Aproape" encapsulare
Fiecare lecție de programare orientată pe obiecte descrie cele mai importante concepte, și anume: Encapsularea, Moștenirea (Inheritance) și Polimorfismul. Am să ating și eu aceste concepte pentru a-ți crea o bază rigidă de înțelegere.
Chiar dacă limbajul de programare Python urmează cele mai bune practici OOP, encapsularea nu este în totalitate suportată. Acest fapt nu trebuie considerat neapărat un dezavantaj, ci din contra, e o simplificare justă. Argumentul principal stă în sloganul "We're all responsible users here".
Encapsularea clasică ține de restricționarea accesului la membrii unui obiect sau clase. Pentru a înțelege mai bine encapsularea propun să luăm în considerare un exemplu real de encapsulare.Într-o companie, există diferite secțiuni, cum ar fi contabilitatea, secțiunea financiară, secțiunea vânzări etc.Secțiunea finanțe gestionează toate tranzacțiile financiare și ține evidența tuturor datelor legate de finanțare. În mod similar, secțiunea de vânzări gestionează toate activitățile legate de vânzări și ține evidența tuturor vânzărilor. Acum poate apărea o situație în care, dintr-un anumit motiv, un funcționar din secțiunea financiară are nevoie de toate datele despre vânzări dintr-o anumită lună. În acest caz, nu i se permite accesul direct la datele secțiunii de vânzări. Mai întâi va trebui să contacteze un alt ofițer din secțiunea de vânzări și apoi să-i ceară să furnizeze datele specifice. Aceasta este encapsularea.În Java sau C++ fiecare atribut sau metodă a clasei sunt definite ca public
, private
și protected.
În Python există posibilitatea de mimat comportamentul private
. Deci, un atribut al clasei care va fi denotat ca privat, nu va fi accesibil din exterior, însă vom putea să-l accesăm din metodele clasei. Ca să facem un atribut privat, trebuie să-i punem prefixul __
. Hai să vedem cum lucrează asta:
Acum, vom putea vedea conținutul la __name
și __age
doar prin metoda info()
, însă nu și direct. Hai să experimentăm:
Așa cum am pomenit mai sus, atributele date au devenit private. Dar să vă zic un secret. Ele de fapt nu sunt private. :stuck_out_tongue_winking_eye: Te întrebi probabil cum așa ceva e posibil. Acuși am să îți explic. Python are o subrutină numită Mangle
(a mutila, din engleză). Ce se întâmplă de fapt, e mutilarea numelui atributelor din perspectiva obiectului. Această facilitate poate fi utilă în crearea modulelor publice prin faptul că va elimina posibilele conflicte la moștenire (inheritance). Despre moștenire vorbim mai târziu. Păi, cum anume se mutilează numele? Hai să experimentăm:
Magnific! Acum știi lucruri din interiorul interpretatorului. Însă ai grijă și utilizează aceste facilități doar atunci când într-adevăr ai nevoie de ele. Pe lângă deviza "We're all responsible users here", mai există una ce-mi place foarte mult: "With great power comes great responsibility". Ar fi bine să le memorezi :stuck_out_tongue_winking_eye:
Moștenire (Inheritance)
Moștenirea este proprietatea unei clase de a moșteni caracteristicile altei clase sau altui obiect. Această facilitate ne ajută să abstractizăm logica structurilor de date într-un mod optim ce ne va ușura dezvoltarea ulterioară a proiectului.
În imaginea ce urmează este prezentat un model de moștenire. Acest model va fi utilizat în exemplele ulterioare.
Hai acum să încercăm primul nostru model de moștenire. Iarăși, vom avea clasa Animal
și clasa Cat
care va moșteni de la Animal
.
Sintactic e foarte simplu. Vedem că în __init__()
repetăm careva acțiuni. Ar trebui să îmbunătățim apoi constructoarele noastre ca să respectăm principiul DRY - Don't repeat yourself. Însă acu, să vedem ce ne-a ieșit:
Totul merge așa cum ne-am așteptat. Avem acces atât la metodele din clasa Cat
cât și la cele din clasa-părinte Animal
. Hai acum să modificăm metodele __init__()
utilizând funcția super()
:
super()
ne permite să căpătăm access la metoda __init__()
din superclasă (clasa de la care moștenim). Acum name
și age
sunt atributele clasei Animal
și tot acolo se gestionează și datele de intrare.
Acum totul lucrează, și mai e și făcut corect. Hai să încercăm încă un nivel de moștenire. Știți de existența pisicilor fără blană? :smile_cat: Hai să moștenim de la Cat și că creăm SphynxCat:
Iarăși, destul de simplu și intuitiv. Plus la toate, am și rescris metoda say()
. Let's see it in action:
Perfect, lucrează! Acum că am ajuns să rescriem metode, o să trecem la următorul subiect strâns legat cu acest principiu. :relieved:
Polimorfism
Conform DEX, în lumea chimiei, polimorfismul e proprietatea unor substanțe de a se putea prezenta în două sau mai multe forme cristaline distincte. Situația este similară și în domeniul programării orientate pe obiecte. Pentru a ilustra acest principiu vom oferi comportament diferit metodei say()
pentru diferite animăluțe. Hai să creăm clasele Cat
și Dog
, ambele subclase de la Animal
. Hai să încercăm lucrul practic:
Am implementat aceiași metodă cu efecte diferite. Să vedem rezultatele:
Lucrează! Isn't that amazing? :heart_eyes: Hai să încercăm să îmbunătățim exemplul nostru un pic. Ce zici să forțăm cumva utilizatorul să implementeze metoda say()
după moștenire? Hai să încercăm un exemplu simplu. Hai să adăugăm metoda asta în clasa Animal
:
Și hai să implementăm clasa Pig
, fără metoda say()
:
Încercăm să executăm say()
:
Good, dar hai să nu ne oprim aici. Unele limbaje de programare au conceptul de "abstract method", metodă ce trebuie neapărat implementată în clasa derivată. Hai să forțăm o eroare în caz că metoda nu e implementată. În practică poate fi foarte util la depistarea posibilelor erori de programare. O să facem raise la excepția NotImplementedError. Să vedem ce ne-a ieșit:
Acum când încercăm să executăm metoda say()
, interpretatorul Python ne aruncă o eroare cu mesaj sugestiv:
Nu te speria de excepții, chiar dacă nu vei avea nevoie de așa funcțional în viitorul apropiat, am vrut să știi că există posibilitatea să generezi și tu erori când e cazul.
Cam atât din cursul introductiv despre programarea orientată pe obiecte în python. Sper că ți-a fost interesant! Acum e timpul să experimentezi independent și, dacă ai întrebări - întreabă. Curiozitatea, motivația și entuziasmul vor contribui la crearea cunoștințelor noi. Iar eu sper că ți-am trezit curiozitatea, te-am motivat și entuziasmat măcar un pic.
Succese vă doresc și pe final vă propun un exercițiu care totalizează cunoștințele pe care le-ai acumulat până aici 🌹
Mai departe 🚀
Pentru a exersa cele învățate, îți recomand să aplici în practică rezolvând exercițiile de mai jos.
Python OOPLast updated