0% au considerat acest document util (0 voturi)
3 vizualizări107 pagini

03 Object Oriented Programming in Python

Documentul discută tipurile de programare, împărțindu-le în programare imperativă și declarativă, cu detalii despre programarea procedurală și orientată pe obiecte. Programarea imperativă se bazează pe instrucțiuni clare și pași definiți, în timp ce programarea declarativă se concentrează pe rezultatul final fără a detalia procesul. De asemenea, se abordează conceptele programării orientate pe obiecte, inclusiv încapsularea, abstractizarea, polimorfismul și moștenirea.

Încărcat de

Sparrow Jack
Drepturi de autor
© All Rights Reserved
Respectăm cu strictețe drepturile privind conținutul. Dacă suspectați că acesta este conținutul dumneavoastră, reclamați-l aici.
Formate disponibile
Descărcați ca PDF, TXT sau citiți online pe Scribd
0% au considerat acest document util (0 voturi)
3 vizualizări107 pagini

03 Object Oriented Programming in Python

Documentul discută tipurile de programare, împărțindu-le în programare imperativă și declarativă, cu detalii despre programarea procedurală și orientată pe obiecte. Programarea imperativă se bazează pe instrucțiuni clare și pași definiți, în timp ce programarea declarativă se concentrează pe rezultatul final fără a detalia procesul. De asemenea, se abordează conceptele programării orientate pe obiecte, inclusiv încapsularea, abstractizarea, polimorfismul și moștenirea.

Încărcat de

Sparrow Jack
Drepturi de autor
© All Rights Reserved
Respectăm cu strictețe drepturile privind conținutul. Dacă suspectați că acesta este conținutul dumneavoastră, reclamați-l aici.
Formate disponibile
Descărcați ca PDF, TXT sau citiți online pe Scribd

Contents

1.1. Tipuri de programare ....................................................................................................................... 3


Ce este programarea imperativă? .......................................................................................................... 4
Ce este programarea declarativă? ......................................................................................................... 5
Ce sunt programarea procedurală și cea orientată pe obiecte?....................................................... 6
1.2. Concepte ale programării orientate pe obiecte ......................................................................... 8
Încapsularea și abstractizarea................................................................................................................ 9
Moştenirea ................................................................................................................................................ 11
Polimorfism .............................................................................................................................................. 12
2.1. Clase și obiecte ................................................................................................................................ 14
Clase şi obiecte în Python ...................................................................................................................... 16
2.2. Membrii clasei, câmpuri și metode ............................................................................................. 19
Membrii clasei.......................................................................................................................................... 19
Câmpurile clasei ...................................................................................................................................... 20
Metode ....................................................................................................................................................... 23
2.3. Membrii clasei statice și instanță ................................................................................................ 29
Metodele instanță.................................................................................................................................... 30
Metode statice .......................................................................................................................................... 31
Metodele clasă ......................................................................................................................................... 32
2.4. Decoratori ........................................................................................................................................... 34
Decoratori definiți de utilizator ................................................................................................................ 35
Decorator obiect ........................................................................................................................................ 38
2.5. Încapsulare, moștenire, polimorfism ......................................................................................... 41
Încapsularea ............................................................................................................................................. 41
Moştenirea ................................................................................................................................................ 45
Polimorfism .............................................................................................................................................. 50
2.6. Clase abstracte ................................................................................................................................. 51
Clase abstracte ......................................................................................................................................... 52
Metodele abstracte ................................................................................................................................. 55
3.1. Gestionarea datei și orei ................................................................................................................ 57
Lucrul cu timpul ...................................................................................................................................... 57
Modulul datetime .................................................................................................................................... 61
3.2. Gestionarea erorilor și a excepțiilor ........................................................................................... 64
Captarea excepţiei ................................................................................................................................... 67
4.1. Reflecţie ............................................................................................................................................ 73
4.2. Tipuri generice ................................................................................................................................. 82
4.3. Gestionarea firelor și a proceselor .............................................................................................. 87
Multiprocessing ....................................................................................................................................... 92

Page 1 of 107
4.4. Gestionarea evenimentelor .......................................................................................................... 94
4.5. Șabloane de proiectare ................................................................................................................ 100
Singleton pattern ................................................................................................................................... 101
Factory ..................................................................................................................................................... 101
Adapter pattern ..................................................................................................................................... 102
Builder pattern ...................................................................................................................................... 104

Page 2 of 107
1.1. Tipuri de programare

Rezumat

• Tipurile de programare sunt concepte pe care programatorii le aplică atunci când utilizează un
limbaj de programare și creează programe.
• Tipurile de programare se împart în două categorii de bază: programarea
imperativă și programarea declarativă.
• În programarea imperativă, programarea programului se realizează printr-o serie de pași definiți
cu precizie pe care programul trebuie să îi execute.
• Programarea imperativă include cele mai importante două tipuri de programare, și anume
programarea procedurală și cea orientată pe obiecte.
• Unul dintre cele mai cunoscute limbaje imperative este Python.
• Programarea declarativă nu necesită să ne ocupăm cu implementarea logicii de procesare a
datelor, ci mai degrabă să căutăm rezultatul final al procesării deja efectuate.
• Cele mai cunoscute limbaje declarative sunt: HTML, SQL şi XML.
• Tipul procedural de programare implică utilizarea procedurilor (funcțiilor) în scopul grupării și
procesării codului.
• Programarea orientată pe obiecte este tipul de programare în care principalul factor de cod
este un obiect. Deci, obiectul este folosit ca o componentă a programului, în timp ce la
abordarea procedurală aveam doar procedura.

În această lecţie, dar și în următoarele, vom aborda principiile teoretice ale programării. Vom defini ce
tipuri de programare există, cum și de ce alegem un anumit tip și, poate, cel mai important aspect - care
este diferența dintre programarea procedurală și cea orientată pe obiecte.

Astăzi avem un număr mare de limbaje de programare care devin limbaje de programare generale, iar
unul dintre ele este şi Python. Fiecare dintre limbajele de programare are particularitățile sale și
avantajele și dezavantajele sale. Efectiv, fiecare limbaj de programare poate fi încadrat în mai multe
tipuri despre care vom vorbi în continuare.

Înainte de toate, să ne reamintim ce este un limbaj de programare și care îi este scopul. În general,
folosim limbajele de programare ca să controlăm performanța unui calculator sau a unui dispozitiv,
folosind un limbaj specific, adică un mod de a scrie instrucțiuni pentru acele dispozitive și calculatoare.
Programatorilor de astăzi li se oferă o gamă largă de limbaje de programare, fiecare dintre ele având
propriile aspecte specifice. Principalul aspect este chiar tipul de programare, care determină abordarea
scrierii codului.

Imaginea 1.1. Diferite limbaje de programarei1

Page 3 of 107
Tipurile de programare sunt concepte pe care programatorii le aplică atunci când utilizează un limbaj
de programare și creează programe. Aceste concepte sunt rezultatul deciziei programatorului, a naturii
proiectului sau a tehnologiei în sine (limbaj de programare și platforme de suport). Aşa cum am
menționat, unele limbaje de programare au suport pentru o singură variantă de programare, în timp ce
altele pentru mai multe.

Câteva dintre cele mai impresionante tipuri de programare se împart în două categorii de
bază: programarea imperativă și programarea declarativă.

Ce este programarea imperativă?

Programarea imperativă (de la cuvântul latin imperare = a comanda) este cea mai veche paradigmă de
programare. Un program bazat pe această paradigmă este alcătuit din seturi de instrucțiuni clar definite.
Programarea imperativă implică implementarea concretă a ceea ce vrem să facă programul, iar acest
lucru se realizează printr-o serie de pași definiți cu precizie pe care programul trebuie să îi execute.
Comenzile sunt controlate prin bucle și structuri.

Întrebarea care se pune acum e unde folosim programarea imperativă. Aşa cum am menţionat, se
folosește în cazul în care vrem să dăm ordine clare, în lucrul cu obiectele sau când rezolvăm direct
probleme logice (imaginea 1.2).

Imaginea 1.2. Ilustrarea programării imperative

Pe baza acestei descrieri, putem să spunem că majoritatea limbajelor de programare „reale” sunt
imperative. În programarea imperativă se află și cele mai importante două tipuri de programare, adică
programarea procedurală și cea orientată pe obiecte, despre care vom discuta mai detaliat în continuare.
Cele mai cunoscute limbaje imperative sunt:

• C
• C++
• C#
• PHP
• Java
• Python
• JavaScript

Page 4 of 107
Ce este programarea declarativă?

Strict vorbind, programarea declarativă nu necesită lucrul cu implementarea logicii de procesare a


datelor, ci mai degrabă căutarea rezultatului final al procesării deja efectuate. Această explicație pare
abstractă, dar rezumă perfect modul în care programăm declarativ.

Să luăm exemplul unei situații reale, de exemplu, procesul de construire a unei case. Cu abordarea
imperativă, pentru fiecare parte a casei facem o listă cu toate elementele, materialele necesare și
efectuăm procedura de construcție.

Transpus în pași simpli, aceasta poate arăta aşa:

1. Construiește o fundație.
2. Configurează construcția.
3. Instalează instalaţia.
4. Adaugă piesele de bucătărie.

Desigur, e o prezentare foarte simplificată, dar ne putem da seama că avem faze definite care ar include
şi cantitatea de material și alte proceduri etc.

În timp ce în cazul abordării declarative avem doar un singur pas:

1. Nu mă interesează cum se construieşte casa, dar vreau în ea un șemineu, o priveliște frumoasă și o


bucătărie mare.

Exact acest principiu se reflectă în programarea declarativă - deci nu vrem să știm ce se află în spatele
procesului și cum se desfășoară acesta, noi doar emitem o comandă și obținem rezultatul final. Prin
urmare, programarea declarativă este mai puțină programare și mai multă utilizare a elementelor gata
făcute.

Programarea funcțională, manipularea bazelor de date, precum și „programarea” markup se numără


printre tipurile de programare declarativă. Deci, cea mai apropiată abordare a programării declarative
de până acum a fost limbajul HTML de creare a paginilor web. În cadrul acestui limbaj, folosim taguri ca
să obținem elemente gata făcute. Nu știm cum creează browserul acest div, cât cod este ascuns în
spatele lui și altele. Nouă ne trebuie div-ul și plasând un tag într-un anumit loc, obținem acel element.

Imaginea 1.3. Ilustrarea programării declarative

Page 5 of 107
Cele mai cunoscute limbaje declarative sunt:

• HTML
• XML și alți derivați ai acestor limbaje
• SQL

Ce sunt programarea procedurală și cea orientată pe obiecte?

În această lecție, ne-am concentrat pe descrierea programării imperative și a celei declarative. Însă, în
acest curs, accentul se pune pe programarea imperativă, mai exact pe cele două paradigme din cadrul
acesteia: programarea procedurală și programarea orientată pe obiecte. Cu programarea procedurală
v-ați întâlnit deja, căci învățarea limbajelor de programare începe întotdeauna cu aceasta, deoarece
necesită o gândire mai puțin abstractă și, în același timp, facilitează învățarea sintaxei limbajului.
Să mai definim încă o dată programarea procedurală. Acest tip de programare implică utilizarea
procedurilor cu scopul de a grupa și procesa codul.
Poate vă întrebați - care sunt aceste proceduri? Procedurile sunt subrutine mici, bucăți de cod, pe care
le putem activa și care fac o anumită acțiune. Această descriere poate vi se pare familiară acum,
deoarece am vorbit deja despre funcții. Ilustrativ, procedura are următorul comportament:

Imaginea 1.4. Ilustrarea procedurii

Deci, procesul începe cu comanda de activare a procedurii, subrutina (subroutine), respectiv codul din
cadrul procedurii se execută și întoarce rezultatul la locul apelului. Iar dacă demonstrăm acest lucru prin
Python:

1pi = 3.14
2
3def calculateAreaOfCircle(radius):
4 area = pi * radius * radius
5 print('Area: ' + str(area))
6
7calculateAreaOfCircle(10)

Dacă ne uităm la această procedură simplă, vedem că scopul ei este să calculeze aria unui cerc. Primul
pas este Do something, așa cum vedem în ilustrație (imaginea 1.4). În cod, acest pas este activarea
procedurii, adică, apelarea funcției; realizăm acest lucru cu linia calculateAreaOfCircle (10). După ce am
activat procedura, începe executarea subrutinei, respectiv executarea procedurii. În acest caz, este vorba
de a calcula aria cercului conform formulei și, în final, urmează ultimul pas - afișarea rezultatului. Deci,
aceasta este exact abordarea procedurală pe care am folosit-o până acum. Abordarea procedurală
crește vizibilitatea codului, reduce cantitatea de cod și simplifică utilizarea acestuia.
Limbajele de acest tip folosesc adesea o abordare top-down, în care ne străduim să creăm blocuri legate
prin anumite funcționalități, de obicei sub formă de proceduri. Aceste proceduri se află chiar la începutul
(în susul) codului. Cu aceste proceduri definim funcționalitatea, iar în abordarea top-down, descriem
treptat (mergând spre jos) rezultatul muncii, descompunându-l în proceduri mai mici, până când
ajungem la rezultatul final.

Page 6 of 107
Programarea pe obiecte este un tip de programare în care principalii actori ai programării sunt obiectele.
Deci, obiectul este folosit ca o componentă a programului, în timp ce în abordarea procedurală aveam
doar procedura.
Schimbând datele în obiect, respectiv a atributelor lui și executarea operațiunilor lui (a procedurilor) se
schimbă starea lui și, în același timp, și rezultatele și, astfel şi starea programului complet.
De ce este nevoie de o abordare orientată pe obiecte? Când începem să scriem programe, începem cu
un număr mic de linii; pe măsură ce programul se dezvoltă, folosim funcții (proceduri), desigur, în scopul
stocării blocurilor de cod pe care le vom putea folosi de mai multe ori. Dacă programul devine și mai
complex, apare nevoia de a plasa un set de anumite funcții în ceva mai mare și, în felul acesta, reducem
repetarea apelării funcțiilor. Apare nevoia să transferăm datele către o altă funcție.
Aici ajungem la clasă şi la principiul orientat pe obiecte. Cu clasa cuprindem toate funcțiile care au un
scop comun, care, apoi, devin metode și ne permit să le accesăm printr-un singur obiect, în loc să apelăm
fiecare funcție în parte și să transferăm rezultatul altor funcții spre procesare. Aşa cum puteți concluziona,
nu ne aflăm într-o situație sau-sau, ci în funcție de nevoie, o parte din codul aplicației noastre poate fi
orientat pe obiecte și alte părți pot fi scrise procedural (pentru că e posibil să avem nevoie doar de
câteva funcții pentru o funcționalitate).
Desigur, toate acestea par abstracte acum, dar în lecțiile următoare vom explica ce sunt clasele și
obiectele în Python și cum se leagă cu ceea ce am învățat până acum despre programare.

1. [Link]
4884590/

Page 7 of 107
1.2. Concepte ale programării orientate pe obiecte

Rezumat

• Programarea orientată pe obiecte se poate împărţi în patru tipuri de bază: Class, Prototype,
Concurrent şi Actor.
• În Python vom folosi două tipuri principale: Class și Prototype.
• Pentru ca un limbaj de programare să fie considerat orientat obiecte, trebuie să aibă suport
pentru patru principii principale: încapsulare,abstractizare, polimorfism şi moştenire.
• Noțiunea de încapsulare implică ascunderea logicii complexe a unei funcționalități. Încapsularea
poate fi înțeleasă și ca o modalitate de ascundere a datelor, unde permitem ca anumite părți
ale codului să fie vizibile, în timp ce altele rămân ascunse. Acest lucru oferă beneficii în lucru și
în utilizare, atât pentru utilizator, cât și pentru programator.
• Abstractizarea poate fi privită ca o extensie a încapsulării care include unităţi și ascunde
funcţionarea lor. Abstractizarea este, de exemplu, faptul că știm să folosim o telecomandă ca să
controlăm un televizor, dar cum funcționează televizorul rămâne o necunoscută.
• Moștenirea este în esență o modalitate de a utiliza de mai multe ori un cod care a fost deja
scris.
• Prin moștenirea unei clase, folosim codul într-o altă clasă. Clasa din care este utilizat codul este
părintele, în timp ce clasa care folosește acel cod este o clasă derivată, așa-numita clasă copil.
După moștenire, un copil poate să folosească toate funcțiile care au fost scrise în cadrul clasei
părinte.

În această lecție continuăm să explorăm principiile teoretice ale programării. Ne vom axa pe
programarea orientată pe obiecte (OOP) și conceptele ei de bază, înainte de a începe să scriem un cod
orientat pe obiecte în Python.

În primul rând, există patru tipuri de bază de programare orientată pe obiecte:

1. Class – Abordarea Class folosește o clasă ca șablon pentru a crea un obiect. Acesta este cel mai
frecvent utilizat tip de programare și Python se utilizează în cea mai mare parte cu această
abordare Class. În cazul acestei abordări, noțiunea de clasă este tot atât de importantă pe cât
este funcția în abordarea procedurală.
2. Prototype – Spre deosebire de programarea Class, pentru crearea unui șablon, în locul clasei,
se utilizează un obiect deja existent.
3. Concurrent – Această abordare a programării permite efectuarea mai multor operațiuni cu
resurse de program partajate. Această abordare se bazează pe utilizarea firelor și a proceselor
pentru a permite executarea în paralel a operațiilor pe o singură clasă, adică pe obiectul ei.
4. Actor – Abordarea Actor este specifică, deoarece, în acest caz, resursele nu sunt niciodată
partajate între obiecte, spre deosebire de abordarea Concurrent. Dar, similar cu abordarea
Concurrent, există o modalitate de a efectua executarea în paralel a codului prin abordarea
Actor; condiția este ca obiectul să nu poată schimba datele dintr-un alt obiect, ci doar să trimită
mesajul că trebuie făcut ceva, dar nu putem transmite date pentru calcul, ci aceste date trebuie
definite în cadrul obiectului care face operaţia.

În acest curs, vom folosi cele două tipuri principale de programare, și anume Class și Prototype.

Indiferent de tipul de programare orientată pe obiecte pe care o alegem, există patru principii principale
care sunt folosite pentru a crea un limbaj de programare orientat pe obiecte:

Page 8 of 107
• încapsulare;
• abstractizare;
• polimorfism;
• moştenire.

Imaginea 2.1. Concepte de bază ale programării orientate pe obiecte

Încapsularea și abstractizarea

Noțiunea de încapsulare implică ascunderea logicii complexe a unei funcționalități. Un exemplu perfect
în acest sens este mașina. Când întoarcem cheia în mașină, motorul pornește. Ca șoferi, știm că trebuie
să întoarcem cheia ca să pornească motorul. Însă, procesul de activare a motorului este mult mai
complex decât simpla rotire a cheii. Creatorul mașinii ne-a ascuns logica complexă a activării motorului
și ne-a dat acces la cele mai importante funcții ale ei.

Imaginea 2.2. Ilustrarea încapsulării și abstractizării

Putem să spunem că încapsularea este un termen pe care îl folosim pentru a descrie modul în care
interfața este separată de implementare. Încapsularea poate fi perecepută și ca o modalitate de
ascundere a datelor, unde permitem ca anumite părți ale codului să fie vizibile, în timp ce altele rămân
ascunse. Acest lucru oferă beneficii în lucru și în utilizare, atât pentru utilizator, cât și pentru programator.

Page 9 of 107
Să luăm exemplul prezentat în imaginea 2.2. Este ilustrat procesul de controlare a televizorului cu o
telecomandă. Încapsularea definește unități precum senzori, circuite electronice și afișaje, cât și modul
în care aceste unități comunică între ele. Abstractizarea, în acest caz, reprezintă faptul că e suficient să
știm cum să folosim interfața, în cazul nostru, telecomanda, iar restul ne rămâne ascuns, adică nu trebuie
să știm cum se transmite semnalul, care senzor de pe televizor primește semnalul și ce logică de program
controlează mai departe hardware-ul dispozitivului.

Deci, din punctul de vedere al utilizatorului (care poate fi şi un alt programator):

• utilizatorul trebuie doar să înțeleagă interfața;


• utilizatorul nu trebuie să înțeleagă cum se realizează implementarea și cum este creată.

Din punctul de vedere al programatorului:

• programatorul poate să schimbe implementarea, adică codul ei, iar utilizatorul nu va vedea
modificarea și nu trebuie să fie informat de ea.

Acum poate vă întrebaţi cât de des vom folosi așa ceva în programele noastre. Răspunsul este - foarte
des și, deja ca utilizatori, am accesat o interfață în Python de multe ori fără să știm cum funcționează.
Metodele și proprietățile încorporate claselor Python ilustrează exact exemple de încapsulare și
abstractizare. În programele noastre folosim aceste metode și proprietăți fără să știm cum funcționează.

Să ne uităm la următorul exemplu:

1print("Hello World")

Aşa cum vedeţi, această linie imprimă pur și simplu un text folosind funcția print () și am folosit-o de
multe ori până acum. Dar, de fapt am folosit tot timpul interfața. Nouă ne este ascuns modul în care
este scrisă această funcție print(). Ea face parte dintr-unul din obiectele limbajului și nu va trebui
niciodată să facem modificări în codul funcției respective, doar o vom folosi. Pentru noi, print este doar
o comandă, o funcție în limbajul în sine care conține sute de linii de cod pentru a realiza imprimarea în
document. Aceasta este încapsularea: folosim ceva fără să știm cum funcționează, dar obținem rezultatul
dorit și acest lucru ne este suficient.

Pentru cei care vor să afle mai multe, găsiți funcția Python print() la următorul link:

[Link]

Abstractizarea poate fi percepută, în primul rând, ca o extensie a încapsulării. Folosim încapsularea ca


un mecanism pentru a rotunji o unitate de cod și pentru a limita cine vede acea unitate. În cazul unui
televizor, afișajul este o unitate.

Abstractizarea se bazează pe asta și definește exact ce date sunt ascunse. Aceste date ascunse nu pot
fi accesate de alte entități din program. În același timp, putem folosi abstractizarea pentru a defini ce
este comun și accesibil. În exemplul nostru, abstractizarea este faptul că știm să folosim telecomanda
pentru a controla televizorul, dar nu știm cum funcționează televizorul.

Page 10 of 107
Moştenirea

Moștenirea este, în esență, o modalitate de a utiliza de mai multe ori un cod care a fost deja scris. Cum
clasa este o unitate organizată de cod, moștenind clasa, putem să folosim codul respectiv într-o altă
clasă. Clasa din care este utilizat codul este clasa de bază sau clasa părinte, în timp ce clasa care folosește
codul este clasa derivată sau clasa copil. Când clasa copil moștenește o clasă părinte, aceasta poate să
folosească toate funcțiile scrise deja în cadrul clasei părinte, ceea ce ne permite să folosim codul deja
scris.

Să vedem exemplul din imaginea 2.3:

Imaginea 2.3. Ilustrarea moștenirii

Să spunem că toți oamenii aparțin clasei Person. Totuşi, fiecare dintre noi poate face diferite activităţi,
așa că, pe această bază, vom crea seturi noi de oameni, în funcție de activităţile pe care le fac (Principal,
Student, Teacher).

Studentul poate să urmeze liceul sau facultatea. Dacă am traduce acest lucru în modelul de clasă al
limbajului orientat pe obiecte, am spune că clasa HighSchoolStudent moștenește clasa Student și că
clasa Student moștenește clasa Person.

De ce este acest lucru atât de util? Să spunem că în clasa Person avem câteva date generale care sunt
stocate în funcțiile ei, de exemplu, numele, prenumele, numărul de telefon. Aceste trei date sunt logice
pentru fiecare persoană din sistemul nostru. Prin urmare, în cazul clasei Principal, nu mai este nevoie să
definim din nou numele, prenumele și numărul de telefon al directorului din această clasă, ci vom
moșteni clasa Person care are deja codul scris pentru procesarea și stocarea acestor date și le vom folosi
în clasa noastră Principal - deci vom folosi codul scris mai devreme.

Apoi, dacă aplicația noastră se mărește, vom avea din ce în ce mai multă moștenire (este chiar de dorit
să o avem). Acest lucru este ilustrat și în exemplul din imaginea 2.3, în special în cazul studenților.

Page 11 of 107
Deci, accentul este pe clasa Student, unde, de asemenea, trebuie să moștenim și clasa Person și acum,
de ex. în clasa Student, putem să avem un nume, prenume, număr de telefon și, specific pentru această
clasă, o informație nouă, cum ar fi numărul dosarului. Dacă adăugăm acum şi un student (College
Student), atunci în loc să moștenim Person, apoi Student, acum vom moșteni doar Student, fiindcă ea a
moștenit deja mai înainte Person, ceea ce pentru noi înseamnă că în College Student vom avea la
dispoziție funcții și date din mai multe clase, și nu mai trebuie să le rescriem.

Polimorfism

Polimorfismul (din cuvintele grecești poly, în traducere mai mult sau mai multe, și morph, adică formă)
înseamnă că un lucru poate fi folosit în mai multe moduri.

În programarea orientată pe obiecte, un tip de polimorfism se reflectă în faptul că clase diferite pot să
definească în moduri diferite metodele moștenite de la clasa părinte. Polimorfismul este un principiu
important în programarea orientată pe obiecte. Veți folosi adesea polimorfismul fără să scrieți în mod
implicit un cod pentru scopul său.

Să clarificăm mai întâi ce este polimorfismul. Să folosim din nou analogia cu mașina. Polimorfismul este
similar cu transmisia automată într-o mașină. Să presupunem că transmisia are doar trei funcții: Drive,
Reverse și Parking. Știm că în două cazuri avem câte un comportament: dacă alegem Reverse (mersul
înapoi), mașina merge doar înapoi; dacă selectăm Parking, mașina nu se mai mișcă. Dar dacă selectăm
Drive, mașina se deplasează înainte, dar în funcție de condițiile din timpul mersului (numărul de turaţii
și viteză), transmisia automată schimbă vitezele.

Deci, funcția mașinii de a merge înainte rămâne aceeași, doar parametrii săi se schimbă. Acesta este un
polimorfism, adică o funcție se va schimba în funcție de utilizare.

Polimorfismul în programare funcționează după un principiu similar. Să vedem un exemplu de


polimorfism în Python:

Știm că operatorul + este utilizat mult în programare. Acest operator nu are doar un singur scop. Când
lucrăm cu numere, servește pentru a realiza operația aritmetică a adunării. De exemplu:

1number1=2
2number2=3
3print(number1+number2)

În acest caz, programul imprimă numărul 5.

Dacă lucrăm cu tipul de date string, operatorul + este utilizat în scopul concatenării.

1string1 = "Hello"
2string2 = "World"
3print(string1+" "+string2)

Ca rezultat, programul afişează „Hello World”.

Aici vedem că un operator poate fi folosit pentru a efectua două acțiuni diferite în funcție de tipul de
date. Exact acesta este exemplul de bază al polimorfismului în Python.

Page 12 of 107
Desigur, folosim polimorfismul şi în programarea orientată pe obiecte, unde folosim o clasă principală
pe ai cărei copii îi folosesc funcțiile ca să efectueze diferite sarcini. Vedeți acest principiu ilustrat în
imaginea 2.4.

Imaginea 2.4. Un exemplu de polimorfism

Dacă ne uităm la următorul exemplu, putem să spunem că circle, triangle şi rectangle sunt copiii unei
clase principale și pentru toți cei trei copii, cel mai bine este să folosiți funcția calculateArea() din clasa
principală, astfel încât să nu se repete codul pentru calcularea ariei figurii din toate cele trei clase. Asta
ne permite limbajul orientat pe obiecte prin intermediul polimorfismului. În acest caz, clasa principală,
în funcție de copilul care o folosește, va calcula aria unui cerc, unui triunghi sau unui dreptunghi.

În această lecție am discutat despre câteva dintre principiile importante ale programării orientate pe
obiecte, iar în lecția următoare vom începe să facem codul; mai întâi, vom vedea cum lucrăm cu clase și
obiecte, iar după aceea ne vom ocupa exact de aceste principii teoretice pe exemple reale din Python.

Page 13 of 107
2.1. Clase și obiecte

Rezumat

• În programarea orientată pe obiecte (OOP), elementul de construcţie de bază este obiectul.


Obiectul este instanța unei structuri de date pe care am definit-o în cadrul clasei. Deci, când
instanțiem o clasă, o apelăm în cod.
• Clasele sunt șabloane care definesc în mod structural un anumit tip, respectiv șabloane pentru
crearea unui obiect. De exemplu, toți oamenii din lume sunt, de fapt, instanțele unei clase –
Omul, de aceea, toți au aceleași caracteristici (atribute); cu valori diferite.
• În Python clasa se definește cu ajutorul comenzii class, care urmează numele clasei, și care este
complet arbitrar. La sfârșitul expresiei punem două puncte, cu care definim că tot ceea ce este
sub expresie reprezintă o parte din clasă. De exemplu: class Cookie:
• Când denumim clasele, respectăm aceleași reguli ca și la denumirea variabilelor, unde mai există
încă o regulă nescrisă: scriem numele claselor cu majusculă.
• Instanțierea clasei se face prin menționarea numelui ei în combinație cu paranteze rotunde.

Pe parcursul lecțiilor anterioare, am explorat principiile teoretice ale programării orientate pe obiecte.
În această lecție ne vom axa pe clase și obiecte, și anume ce sunt ele - mai întâi, din punct de vedere
teoretic şi apoi din punct de vedere al limbajului de programare Python.

În programarea orientată pe obiecte (OOP), elementul de construcţie de bază este obiectul. Un obiect
este o instanță a unei structuri de date pe care am definit-o în cadrul clasei. Deci, când instanțiem o
clasă, o apelăm în cod. De fapt, îi creăm instanța, adică obiectul acelei clase. Acest obiect posedă nişte
proprietăţi caracteristice şi se poate folosi în diferite moduri ca material pentru construcția altor obiecte
mai complexe. În felul acesta, obținem o conexiune ierarhică a obiectelor, care se întinde de la
elementele cele mai simple, foarte abstracte, la elementele mai complexe, specializate.

Să spunem că vrem să construim un zgârie-nori și cea mai mică unitate de construcție din acel zgârie-
nori este o cărămidă. Cărămida este un concept foarte abstract în construcție și are o gamă largă de
scopuri. Din cărămizi construim pereții. Pereții sunt mai puțin abstracţi decât cărămizile, dar rămân în
continuare un concept abstract. Combinând pereții, putem să creăm un etaj. Un etaj este, de asemenea,
un bloc de construcție al zgârie-nori, dar mult mai puțin abstract decât o cărămidă obișnuită sau un
perete. În sfârşit, aranjând în mod corespunzător etajele, obținem un zgârie-nori complet, aceasta fiind
în sfârșit o componentă extrem de specializată. Desigur, nici aceasta nu trebuie să se încheie aici, pentru
că din zgârie-nori putem face complexe și din complexe un oraș întreg...

Dacă ar fi să explicăm ce este, de fapt, o cărămidă, ce este un zid, ce este un etaj și ce este un zgârie-
nori, am putea să enumerăm proprietăţile specifice ale obiectelor menționate. Știm că, în mod obișnuit,
o cărămidă este un pătrat mic din lut, deci ca să o descriem am putea să menționăm lungimea, lățimea
și înălțimea ei. Orice altceva este mai mult sau mai puțin irelevant.

Odată ce am stabilit proprietățile de care avem nevoie ca să reprezentăm ceva (de exemplu, cărămida),
le plasăm într-o singură formă unificată, pe care în programare o numim clasă. Astfel, de fapt, toate
cărămizile din zgârie-nori al nostru (care probabil nu sunt complet identice) sunt, de fapt, instanțe, adică
obiecte, care se bazează pe un șablon (clasă) - cărămida.

Clasele sunt, deci, șabloane care definesc structural un anumit tip, adică șabloane pentru crearea unui
obiect. În acest context, toți oamenii din lume sunt de fapt instanțe ale aceleiași clase - Omul, deoarece
toți au aceleași caracteristici (atribute); doar valorile lor sunt diferite.

Page 14 of 107
Să vedem un exemplu în acest sens. Să luăm o mașină, Volkswagen Beetle; în imaginea 3.1. vedem un
exemplu al clasei acestei mașini (stânga) și instanța mașinii respective, adică obiecte din clasa Beetle:

Imaginea 3.1. Clasa şi obiectele ei (instance)

Deci, clasa reprezintă mașina așa cum a fost definită de inginer: forma, dimensiunile, numărul de uși etc.,
cu toate detaliile suplimentare, dar fără ca aceste detalii să fie combinate.

Combinarea detaliilor are loc în producție, atunci când mașina este creată, de fapt, şi fizic - atunci primim
detalii precum versiunea motorului, culorile, accesoriile etc. Fiecare mașină creată este o instanță a clasei
de bază și toate aspectele care se adaugă mașinii fac parte din obiect. Prin urmare, în imaginea de mai
sus vedem patru instanţe ale clasei Beetle, deci patru obiecte complet diferite care provin toate dintr-o
clasă.

Deci, în terminologia orientată pe obiecte o clasă este un șablon pentru definirea unui obiect. Acesta
definește numele și tipurile de variabile pe care le poate folosi un obiect, precum și metodele (funcţiile)
care reprezentă procedurile pentru gestionarea acelor variabile. Clasa poate fi văzută ca un tip, în timp
ce obiectele pot fi considerate variabile ale acelui tip de date.

Prin urmare, din exemplul anterior, avem nevoie de clase pentru:

• mai buna structurare a unui proiect (mai multe linii de cod, care ar putea fi funcții separate, sunt
acum stocate într-o singură clasă);
• utilizarea multiplă a componentelor proiectului (deoarece putem să folosim clase gata făcute în
mai multe locuri);
• distribuirea responsabilităților (în termeni de dezvoltare și sistemic, cum ar fi distribuirea
responsabilităților în timpul distrugerii și creării obiectelor, în special, în sistemele complexe).

De exemplu, avem o clasă de animale. Această clasă este destul de generală. Din ea putem moșteni alte
clase: păsări, nevertebrate, pești, mamifere... Toate aceste subclase sunt și ele animale, dar fiecare dintre
ele are propriul set de caracteristici.

Unde folosim clase? De exemplu:

• pe rețeaua socială Facebook, utilizatorul este reprezentat în sistem ca o clasă;


• tancul în jocul World of Tanks este reprezentat ca o clasă;
• mașina este reprezentată ca o clasă într-o aplicație de vânzare a mașinilor.

Prin urmare, putem să spunem că avem trei clase aici: utilizatorul, tancul și mașina.

Pe lângă atributele (câmpurile) clasei, există și anumite proprietăți funcționale ale clasei. Aceste
proprietăți se numesc metode și reprezintă ceea ce poate face o anumită clasă, respectiv obiectul care

Page 15 of 107
o reprezintă. Dacă am spune că un om tocmai s-a așezat, am putea spune că a fost activată metoda lui
de ședere. De fapt, metodele nu sunt altceva decât funcțiile dintr-o clasă.

Imaginea 3.2. Ilustrarea obiectelor și claselor

Aşa cum am spus şi mai înainte, o caracteristică importantă a claselor este că se pot moșteni una pe alta.
Dacă ne uităm la ilustrația de mai sus, vedem că fiecare dintre obiectele din joc reprezintă și un obiect
în cod. Fiecare dintre obiectele care fac parte dintr-o clasă are propriile sale metode. Metodele principale
sunt show şi hide și ele sunt moștenite pentru obiecte, deoarece dorim să avem control asupra a ceea
ce este afișat în joc și asupra a ceea ce nu este.

Apoi, avem şi metode specifice; de exemplu în cazul unui tanc, există metoda care rulează atunci când
utilizatorul apasă butonul stâng al mouse-ului - atunci este activată metoda shot. Dacă tancul își
pierde starea de sănătate (atribut) prevăzută, este activată metoda explode etc. Desigur, aceasta este
doar o reprezentare și nu reflectă nume reale și aplicarea claselor și metodelor din exemplul jocului,
dar indică modul în care funcționează aplicarea claselor, obiectelor, metodelor și atributelor. Vom
vedea acum cum lucrăm cu clase și obiecte în Python.

Clase şi obiecte în Python

Primul pas în procesul de creare a claselor și obiectelor este crearea unei clase. Aşadar, începem cu
definirea clasei în sine. Ca să creăm o clasă în Python, creăm un fișier nou (denumit ca şi clasa pe care
dorim să o creăm); în el scriem:

1class exampleName:<br><br>
Așadar, în Python, o clasă este definită folosind comanda class urmată de numele clasei; în cazul nostru,
acesta este exampleName - numele este, desigur, complet arbitrar. La finalul expresiei, specificăm :. Cele
două puncte indică faptul că tot ce e sub expresie reprezintă o parte a clasei, adică a corpului ei
(imaginea 3.3.).

Page 16 of 107
Imaginea 3.3. Definirea clasei ExampleName

Atunci când denumim clasele, respectăm aceleași reguli ca și atunci când denumim variabilele, unde
mai există încă o regulă nescrisă: scriem numele claselor cu majusculă, dacă este posibil, iar dacă numele
are mai multe cuvinte, atunci fiecare cuvânt se scrie cu literă mare.

De exemplu: class Person, class MyClass, class SomeOtherClass, class Car etc.

În imaginea 3.3 puteţi să observați și utilizarea comenzii pass. Aceasta nu este o parte obligatorie a
definirii clasei - este utilizată numai atunci când clasa este goală, fără comenzi în corp și când programul
este executat. Conform sintaxei Python, corpul clasei nu poate fi gol și de aceea, folosim comanda pass
ca placeholder. Această comandă nu are o funcție reală - atunci când se execută, nu se întâmplă nimic,
dar pentru noi este perfectă ca să completăm spațiul.

De asemenea, puteți să vedeți că folosim indentarea, adică indentarea textului. Aşa cum știți, conform
regulilor, este necesar să indentăm rândurile și aplicăm această regulă şi atunci când scriem corpul clasei.

Imaginea 3.4. Exemple de denumire a claselor

Când creăm o clasă, o putem folosi pentru a crea obiecte, deci instanțe ale clasei. În formularul de bază,
creăm un obiect specificând doar numele clasei și apoi paranteze rotunde. Să spunem că avem o clasă
Cookie.

Definiţia ei ar fi:

1class Cookie:

în timp ce apelul clasei, adică instanțarea ei ar fi:

1Cookie()

Page 17 of 107
Linia scrisă înseamnă: creați un obiect după șablonul Cookie. Aşa cum vedeți, acest lucru este identic cu
apelarea unei funcții, doar că, în acest caz, creăm un obiect al clasei. Desigur, aceasta nu este o practică,
iar instanțarea clasei se face întotdeauna în cadrul unei variabile. Astfel, folosim variabila ca obiect al
clasei, cu care accesăm anumite date sau funcții în clasă. Prin urmare, linia ar fi:

1oreo = Cookie()

Deci, acum am definit un obiect nou al clasei Cookie și acesta se numește oreo. Acum, dacă este necesar,
vom folosi acest obiect ca să accesăm codul deja scris al clasei.

Probabil că aţi mai auzit că în Python totul este obiect și că pentru unele componente ale
limbajului există proprietăți și metode. Deși nu am știut care sunt astea, le-am folosit. Un exemplu de
obiect este secvența. Am văzut că secvențele au metodele find și replace, iar acum vedem că funcțiile
care se află în cadrul clasei se numesc metode. Deci, dacă apelăm metoda peste o variabilă, atunci acea
variabilă este după tip obiect.

Însă, aici vorbim despre ceea ce este deja încorporat în Python și, atunci când vrem să facem un obiect,
deci să nu folosim unul deja existent, atunci avem nevoie exact de clasă. Clasa se comportă ca un șablon
pentru crearea unui obiect nou.

Dacă rezumăm ce am învățat până acum, vom obține următorul cod:

1class Cookie:
2 pass
3
4oreo = Cookie()

Cu acest cod, definim o clasă nouă, care va reprezenta prăjitura (cake). Apoi am instanțiat clasa (am
apelat-o) într-un obiect nou creat denumit oreo. Vom folosi apoi acest obiect ca să accesăm datele și
funcțiile care se află în clasă. Deocamdată, clasa noastră este goală, adică conține comanda pass care
nu face nimic în timpul execuției. În lecția următoare, vom învăța despre structura clasei și vom vedea
cum stocăm date și definim funcțiile în cadrul clasei.

Page 18 of 107
2.2. Membrii clasei, câmpuri și metode

Rezumat

• Membrii clasei reprezintă datele care o descriu. Membrii clasei pot fi date, metode (funcții în
cadrul unei clase) și alte clase.
• Membrii clasei pot fi instanță și clasă.
• Instanțele membrilor clasei sunt legate de instanța clasei în sine, adică de un obiect al clasei.
• Membrii clasei reprezintă date legate de clasa în sine.
• Câmpul clasei (proprietate) reprezintă datele care se află în cadrul clasei. Se definește la fel ca
o variabilă. Diferența este că câmpul se află în clasă și doar în cadrul ei, sub definiția clasei și
deasupra metodelor pe care le folosim în clasă.
• Metodele sunt subrutine în cadrul cărora se efectuează o anumită sarcină. Acestea constau
dintr-o serie de comenzi și variabile care reprezintă o unitate de program. Metoda nu este nimic
altceva decât o funcție care se află în cadrul unei clase.
• Ca să definim metoda, folosim cuvântul cheie def urmat de un nume, paranteze rotunde și,
desigur, două puncte. Exemplu: def Calculate():
• În Python, parametrul de bază al unei metode se numește self. Acesta este un parametru care
permite metodei să acceseze câmpurile clasei pe baza obiectului pe care l-a apelat.
• Pe lângă metodele clasice, Python are și metode speciale. Acestea se numesc metode magic,
iar pentru noi cea mai importantă este cea cu care se creează un constructor. Un constructor
este întotdeauna creat și apelat atunci când se creează un obiect al unei clase. În cadrul
constructorului, putem să setăm câteva valori inițiale pentru obiectul instanțiat.
• Metoda prin care creăm un constructor se numeşte __init__.

În această lecție vă vom arăta cum creăm o structură de clasă - adică cum stocăm date și cum definim
funcții, adică, metode în cadrul clasei. Această lecție reprezintă baza modului în care formăm clase și
toate lecțiile următoare nu vor face decât să actualizeze cunoștințele dobândite în această lecție. La
sfârșitul lecției, vom procesa primele exemple de cod orientat pe obiecte în Python.

Să începem prin a ne reaminti câteva informații importante. Folosim clasa pentru a defini un tip de
date nou. Un exemplu al acestui tip de date este instanța clasei, pe care o mai numim și obiect al
acelei clase. Funcțiile care fac parte dintr-o clasă se numesc metode.

Membrii clasei

Membrii unei clase sunt, în general vorbind, datele din ea. Aceşti membri pot fi date sau metode.

Membrii clasei pot fi împărțiți în:

• instanţă – membrii instanță ai unei clase sunt legați de obiecte, adică de instanță;
• clasă – membrii clasă ai unei clase, așa cum le sugerează și numele, reprezintă datele referitoare
la clasă.

Prin membrii clasei înțelegem:

• câmpuri;
• metode;
• alte clase.

Page 19 of 107
Toți membrii clasei au în comun faptul că posedă modificatori de acces. Ați întâlnit modificatorii atunci
când aţi lucrat cu funcțiile. Cu modificatorii de acces definim intervalul de disponibilitate a anumitor
valori. Avem un principiu identic de modificare a accesului şi pentru membrii clasei, unde aceștia pot fi:

• privaţi (private) – membrii sunt vizibili doar în cadrul clasei în care se află; acești membri se
numesc membri ai clasei private (private class members);
• protejaţi (protected) – elementele sunt disponibile chiar din clasa în care au fost definite, dar și
din clase derivate, însă nu pentru utilizatorii externi și se numesc membri protejați ai clasei
(protected class members);
• publici (public) – orice obiect are acces peste element, deci sunt accesibile din exterior și îi
numim membri publici ai clasei (public class members).

În lecțiile următoare vom explica modificarea accesului pentru fiecare dintre membrii clasei. Acum ne
vom axa pe membri în sine și vom începe de la domeniul clasei.

Câmpurile clasei

Ce sunt câmpurile? Câmpurile sunt de fapt variabile care se află în interiorul unei clase. Să vedem
următorul exemplu:

1class Car:
2 make = ""
3 model = ""
4 doorsNumber = 0
5 wheels=4

Deci, în acest exemplu, am definit clasa Car și în cadrul acestei clase avem patru
variabile: make, model, doorsNumber şi wheels. Acestea sunt exact cele patru câmpuri ale clasei.

Cu câmpurile lucrăm la fel ca și cu variabilele clasice; diferența stă în felul în care le abordăm.

Aşa cum am spus, clasa este un șablon pentru crearea unui obiect care va utiliza valoare și funcții. Deci,
ca să utilizăm corect valorile, mai întâi trebuie să creăm un obiect din această clasă și apoi să accesăm
valorile dorite. Deci, vom continua folosind codul anterior:

1class Car:
2 make = ""
3 model = ""
4 doorsNumber = 0
5 wheels = 4
6
7car = Car()
8print([Link])

Notă

Pentru o bună practică, înainte de a rula acest exemplu, salvați codul într-un fișier nou cu un nume identic cu
numele clasei definite; prin urmare, în cazul nostru, numele fișierului este [Link].

Page 20 of 107
Deci, în exemplul nostru vedem două linii noi. În ele, am definit mai întâi un obiect nou numit car (nume
arbitrar) care este o instanță a clasei Car. În linia următoare, am folosit comanda print și am specificat
ca valoare un obiect punct variabila ([Link]). Deci am accesat obiectul și pe el am apelat variabila
wheels. În felul acesta, folosim valoarea variabilei wheels definită în cadrul clasei.

Alternativ, limbajul ne permite să folosim o anumită valoare din clasă fără instanțiere, adică fără obiectul
clasei. Realizăm acest lucru în felul următor:

1print([Link])

Deci, în loc de obiect, acum listăm direct clasa și valoarea ei. Această abordare nu este recomandată,
deoarece, așa cum veți vedea în continuare, în clase folosim funcții, deci nu e nevoie să scoatem o
valoare în afara clasei și apoi să o manipulăm.

Cu aceasta am ajuns la împărțirea câmpurilor după tip; câmpul poate fi instanţă și static. Să ne uităm la
următoarea imagine:

Imaginea 4.1. Câmp instanţă şi static

Aşa cum vedeți, în cazul unui câmp instanță, am definit mai întâi un obiect clasă, am accesat
câmpul make și pe obiect am atribuit o valoare. Este important să nu schimbăm valoarea make din clasă;
dacă am încerca să imprimăm o valoare din [Link], am vedea că este în continuare goală. Câmpul
are valoare doar în cadrul instanței, adică obiectul în sine, prin urmare, este un câmp instanță.
În acest exemplu vedeți ce înseamnă și un șablon clasă. Obiectul nostru are acum toate aceste valori și
poate să stocheze valori noi fără să modifice valoarea în sine din clasă. Toate modificările se petrec în
interiorul obiectului, adică în instanță.
În ceea ce privește câmpul static prezentat în imaginea 4.1., prin câmp static ne referim la valoarea
afișată direct din clasă, așa cum ați văzut puțin mai devreme în lecție.
Să vedem acum un exemplu care va rezuma tot ceea ce am învățat până acum. Vom folosi clasa,
structura datelor și obiectul pe care l-am folosit până acum și doar pentru a exemplifica, vom schimba
câmpurile clasei:
1class Car:
2 make = ""
3 model = ""
4 engineType = ""
5 horsePower = 1
6
7car = Car()

Page 21 of 107
Acum vrem să folosim șablonul pe care îl avem ca să definim o mașină în cod - deci, ca să nu fie o
mașină abstractă. Realizăm acest lucru în felul următor:

[Link] = "BMW"
[Link] = "Z4 M40i"
[Link] = "3.0L I6"
[Link] = 382

Aşa cum vedeți, acum am accesat câmpurile instanţiate și le-am setat valorile, care reprezintă acum
BMW Z4. Dacă dorim să imprimăm datele acestei mașini, putem să facem asta, de exemplu, în felul
următor:

1print ("My car is: ")


2print([Link], [Link], [Link], [Link])

În cadrul comenzii print, listăm acum câmpurile instantanţate și după executarea codului, obținem
rezultatul:

My car is:
BMW Z4 M40i 3.0L I6 382

Codul complet al acestei părţi arată aşa:

1class Car:
2 make = ""
3 model = ""
4 engineType = ""
5 horsePower = 1
6
7car = Car()
[Link] = "BMW"
[Link] = "Z4 M40i"
[Link] = "3.0L I6"
[Link] = 382
12print ("My car is: ")
13print([Link], [Link], [Link], [Link])

Am folosit acum abordarea orientată pe obiecte ca să obținem rezultatul dorit, fără să folosim funcțiile
și să repetăm inutil codul. În cazul în care doriți să imprimați două mașini, puteți, pur și simplu, să repetați
codul complet de la instanță în jos. Deci, pentru două mașini, codul ar fi:

1class Car:
2 make = ""
3 model = ""
4 engineType = ""
5 horsePower = 1
6
7car = Car()
[Link] = "BMW"
[Link] = "Z4 M40i"
[Link] = "3.0L I6"

Page 22 of 107
[Link] = 382
12print ("My car is: ")
13print([Link], [Link], [Link], [Link])
14
15car = Car()
[Link] = "Mercedes-Benz"
[Link] = "SLC R172"
[Link] = "M276"
[Link] = 302
20print ("My car is: ")
21print([Link], [Link], [Link], [Link])

Aşa cum vedeți, oferim un cod identic, schimbăm doar valorile. Rezultatul va fi:

My car is:
BMW Z4 M40i 3.0L I6 382
My car is:
Mercedes-Benz SLC R172 M276 302

Programarea orientată pe obiecte ne permite să folosim aceeași instanță pentru reimprimare, cu


rezultate diferite. Însă, o mică notă: atunci când schimbăm datele, datele din instanța anterioară se
pierd. Deci, după schimbarea datelor în obiect, BMW Z4 nu va mai exista.

Metode

Pe lângă câmpuri, printre membrii clasei se află şi metodele. În general, înainte de toate, metodele sunt
subrutine care îndeplinesc o sarcină specifică. Acestea sunt compuse dintr-o serie de comenzi și variabile
care reprezintă o unitate de program. Ca să utilizăm o metodă, folosim termenul de apelare a metodei.
Fiecare apel execută o serie de comenzi conținute în acea metodă. Înainte de executarea comenzii,
anumite valori denumite argumente ale metodelor (parametri) pot fi transmise metodei. De asemenea,
la finalul executării comenzii, metoda poate avea o valoare returnată pe care metoda respectivă o
returnează ca rezultat.

Acest lucru vă poate părea cunoscut. Motivul este că metoda este, de fapt, o funcție, dar în cadrul unei
clase. Metodele sunt definite în cadrul unei clase ca funcții cu propriul bloc de cod.

Metodele determină comportamentul unui obiect. Metoda funcționează cu caracteristicile obiectului și


cu ajutorul ei, schimbăm starea obiectului, fie prin valorile pe care le găsim în obiect, fie prin operații pe
care le putem efectua automat. Până acum, în lecție ați văzut că am folosit obiectul doar ca să afișăm
câmpul clasei, ceea ce, desigur, nu este ideea programării orientată pe obiecte, pentru că am putea lucra
mult mai simplu cu valori.

Să vedem un exemplu al unei metode din cadrul clasei pe care deja o cunoaștem, Car:

1class Car:
2 make = ""
3 model = ""
4 engineType = ""
5 horsePower = 1
6
7def pourFuel():

Page 23 of 107
Aşa cum vedeți, ca să definim metoda, folosim cuvântul cheie def urmat de nume, paranteze rotunde
și, desigur, două puncte. Deci, la fel ca atunci când definim o funcție clasică. Aici trebuie să luăm în
considerare definirea metodei, unde indentarea este crucială atunci când lucrăm în Python.

Imaginea 4.2. Funcţia pourFuel (stânga) în raport cu metoda pourFuel (dreapta)

Aşa cum vedeți, în imaginea 4.2. codul aparent identic conține o diferență importantă. În partea stângă
vedem codul în care este definită funcția pourFuel. Motivul este că indentarea nu este utilizată, adică
setarea funcției în cadrul corpului clasei. Pe de altă parte, în codul din dreapta vedem o funcție corect
indentată, care reprezintă acum metoda pourFuel din clasa Car.

Parametrii metodei

Așa cum funcțiile au parametri, la fel au și metodele. Parametrii sunt esențiali, deoarece fără parametri
nu putem să accesăm câmpurile clasei și nu avem niciun beneficiu real din metodele în sine din clasă.

La fel ca funcțiile, parametrii care intră într-o metodă se numesc parametri de intrare. Parametrii pe care
metoda îi returnează se numesc parametri de ieșire (avem întotdeauna un parametru de ieșire).

Atunci când creăm o metodă, trebuie să indicăm în semnătura ei parametrii pe care îi va returna și pe
care îi va accepta. Ca să returnăm o valoare dintr-o metodă, folosim cuvântul cheie return urmat de una
sau mai multe valori.

Practica este ca parametrul de bază al metodei să se numească self. Acesta este un parametru care
permite metodei să acceseze câmpurile clasei pe baza obiectului pe care l-a apelat. Să vedem cum arată
toate astea într-un exemplu:

1class Person:
2 def sayHello(self):
3 print("Hello!!")
4
5object1 = Person()
[Link]()

Aşa cum vedeți, am definit clasa Person; în interiorul ei avem metoda sayHello care are ca parametru de
intrare, parametrul self, care ne permite să folosim metoda ce are acum o funcție foarte simplă, și anume
să imprime textul Hello!!. După asta, am instanțiat clasa în obiectul object1 și apoi am apelat
metoda sayHello peste obiect, care apoi se va executa și va imprima textul Hello!!

Page 24 of 107
Self, desigur, nu este un cuvânt cheie, dar oricum am numi primul parametru al metodei, îl vom folosi
ca să accesăm câmpurile clasei. Următorul cod demonstrează accesul la câmpurile clasei și utilizarea lor.

1class Sum:
2 a = 2
3 b = 3
4 def calculate(self):
5 print("Result is: ", self.a + self.b)
6
7object2 = Sum()
[Link]()

Am definit clasa Sum care are două câmpuri - a și b, cu valorile lor. După aceea, am definit o metodă
nouă, care se numește calculate și care are, ca și până acum, un singur parametru. În cadrul metodei
avem imprimarea și adunarea. Cea mai importantă parte a acestui cod este tocmai self.a + self.b. Deci,
specificând numele parametrului metodei și adăugând un punct și un nume câmpului, obținem valoarea
câmpului clasei. Aici am folosit aprovizionarea ambelor câmpuri și le-am însumat valorile.

Desigur, nu e obligatoriu ca metoda să utilizeze câmpurile clasei; poate să-și folosească proprii
parametri ca să facă operaţia pe care v-am arătat-o în exemplul anterior, de exemplu:

1class Calc:
2
3 def add(self,a,b):
4 result = a+b
5 return result
6
7calc = Calc()
8res = [Link](2,3)
9print(res)

Acum, în cadrul clasei Calc avem metoda add și această metodă are trei parametri. Primul este self, la
care permitem accesul, după aceea avem parametrii a și b. Acești parametri sunt adunați în variabila
result și returnați în locul de unde a fost apelată metoda, cu comanda return. Deci, până acum, acest
procedeu ne amintește foarte mult de o funcție clasică.

Apoi instanțiem clasa în obiectul calc. În variabila nouă stocăm metoda call peste obiect și direct de aici
transmitem metodei valorile pentru parametrii a și b. Acordaţi atenţie faptului că aici nu este
introdusă valoarea pentru primul parametru self, deoarece self este doar o modalitate de a accesa
proprietățile (câmpurile) și metodele clasei. Folosim primul parametru al metodei ca să legăm metoda
și câmpurile clasei.

După ce transmitem valoarea, metoda se activează, preia valorile, în acest caz adună valoarea și
returnează rezultatul în locul de unde s-a făcut apelul, în cazul nostru în variabila res, după care este
imprimat rezultatul.

Desigur, acum s-ar putea să vă gândiți - de ce n-am putea să transmitem valorile direct clasei, în loc să
trecem prin procedura puțin mai lungă de transmitere a acestora metodei?

Acest lucru este posibil, deoarece Python ne oferă câteva metode speciale pentru lucrul cu clasele.
Numele lor încep cu __ (două linii jos) și se termină în același fel. Au un nume comun - metode magic.
Una dintre aceste metode speciale este așa-numitul constructor. Acesta este întotdeauna creat și apelat

Page 25 of 107
când creăm un obiect al unei clase. În cadrul constructorului, putem să setăm câteva valori inițiale pentru
obiectul instanțiat.

Dar nu este obligatoriu și dacă nu doriți să setați proprietățile obiectului atunci când îl creați, nu trebuie
să-l utilizați. Totuși e util să definim ceva în cadrul constructorului, care să fie întotdeauna executat
imediat ce este instanţiată clasa, adică creează un obiect al clasei.

Într-o clasă este permis doar un constructor. Numele lui trebuie să fie __init__, astfel încât Python să știe
ce metodă să execute atunci când creează obiectul. De exemplu:

1class Car:
2 make = ""
3 model = ""
4 engineType = ""
5 horsePower = 1
6 def __init__(self):
7 print("New car is being created")
8
9new_car = Car()

Vedem că este definită metoda cu un nume special __init__ și în cadrul ei avem doar o imprimare text.
Diferența față de metoda clasică este că, folosind constructorul, vom obține imprimarea textului imediat
ce este instanțiată clasa, în loc să creăm mai întâi o instanță și apoi cu obiectul să accesăm metoda și să
o activăm pentru imprimare.
Această tendință de a reduce numărul de linii de cod și de a automatiza operațiunile este unul dintre
obiectivele programării orientate pe obiecte.

1class Calc:
2
3 def __init__(self, a, b):
4 self.a = a
5 self.b = b
6
7 def add(self):
8 result = self.a + self.b
9 return result
10
11calc = Calc(2,3)
12res = [Link]()
13print(res)

În exemplul de mai sus vedeți că în cadrul metodei noastre speciale __init__ am definit și parametrii a și
b, care au fost preluați în cadrul metodei și atribuiți ca câmpuri ale clasei. Folosind metoda add, efectuăm
din nou operația de adunare a câmpurilor clasei. Principala diferență se reflectă în modul în care
transmitem parametrii. Folosim momentul de instanțiere ca să transmitem valorile parametrilor a și b
prin intermediul clasei. După aceea, este suficient să apelăm metoda add peste obiect.

Să ne uităm acum la exemplul în care am lucrat la începutul acestei lecții:

1class Car:
2 make = ""
3 model = ""

Page 26 of 107
4 engineType = ""
5 horsePower = 1
6
7car = Car()
[Link] = "BMW"
[Link] = "Z4 M40i"
[Link] = "3.0L I6"
[Link] = 382
12print ("My car is: ")
13print([Link], [Link], [Link], [Link])
14
15car = Car()
[Link] = "Mercedes-Benz"
[Link] = "SLC R172"
[Link] = "M276"
[Link] = 302
20print ("My car is: ")
21print([Link], [Link], [Link], [Link])

Sunteți de acord că în acest cod avem prea multe comenzi introduse manual. Definim în mod special
câmpul și tipul lui, apoi repetăm aceeași procedură atunci când setăm valori pentru câmpuri prin obiect
şi apoi, cea mai dificilă parte a lucrului este când repetăm codul pentru o altă maşină. Acum când știm
să folosim metodele, putem să automatizăm acest cod și să reducem numărul de linii necesare pentru
această funcționalitate. De exemplu, așa:

class Car:
1 def __init__(self,make,model,engineType,horsePower):
2 [Link]=make
3 [Link]= model
4 [Link] = engineType
5 [Link] = horsePower
6
7 def show(self):
8 print("My car is:
9",[Link],[Link],[Link],[Link])
10
11car = Car("BMW","Z4 M40i","3.0L I6",382)
[Link]()
13
14car2 = Car("Mercedes-Benz","SLC R172","M276",302)
[Link]()

Aşa cum vedeți, acum am automatizat semnificativ operațiile. Acum avem pentru fiecare mașină două
linii și toate celelalte operaţii se fac în cadrul metodei clasei. Acum folosim metoda constructorului
__init__ ca să permitem transmiterea parametrilor când instanțiem clasa; aceasta stochează valorile pe
care le-a primit.

Folosim o metodă separată (show) ca să afișăm rezultatele. După aceea, tot ce mai trebuie să facem este
să transmitem parametrii pe care îi vor folosi metodele în timpul instanțierii clasei. Cu cât avem mai
multe obiecte, cu atât vedem o putere mai mare în utilizarea clasei ca șablon și metodă pentru
procesarea datelor.

Page 27 of 107
Exerciţiul 1:

Creați o clasă calculator cu două proprietăți (câmpuri): number1 şi number2.

Clasa ar trebui să aibă metodele:

• add, care ca rezultat returnează suma a doi operanzi;


• sub, care ca rezultat returnează diferența a doi operanzi;
• mul, care ca rezultat returnează produsul a doi operanzi;
• div, care ca rezultat returnează coeficientul a doi operanzi.

Exerciţiul 2:

Creați două clase: Atm şi Card. Clasa Card trebuie să conțină două câmpuri: name şi balance. Clasa Atm trebuie
să aibă un câmp card (valoarea implicită a câmpului None) și metodele set_card şi withdraw.

Metoda set_card plasează cardul în bancomat, iar Metoda withdraw retrage o anumită sumă din cont.

Condiție specială: dacă nu există fonduri suficiente sau cardul nu se află în bancomat, metoda
withdraw raportează eroare.

Notă: Există mai multe modalități în care puteți să realizați funcționalitatea necesară. Atâta timp cât soluția dvs.
îndeplinește cerințele menţionate și obțineți rezultatul solicitat, programul este realizat corect.

Page 28 of 107
2.3. Membrii clasei statice și instanță

Rezumat

• Câmpurile clasă sau statice se partajează cu toate obiectele clasei, în timp ce câmpurile instanță
sau non-statice sunt diferite pentru fiecare obiect în parte, deoarece fiecare obiect conține o
copie a variabilelor.
• Metodele instanță sunt metodele pe care le-am folosit până acum. Ele există în contextul
obiectului, ceea ce înseamnă că sunt disponibile tuturor membrilor obiectului clasei în care se
află.
• Pentru ca o metodă să fie instanțiată, trebuie să aibă un parametru căruia i se va transmite
automat, în timpul apelării, o referință la obiectul peste care e apelată metoda (acest parametru
este, de obicei, denumit self).
• Metodele statice sunt definite ca şi alte metode, dar sunt apelate direct prin clasă. Ele pot, dar
nu trebuie să fie marcate cu un decorator special @staticmethod.
• Metodele clasă sunt transmise direct clasei, spre deosebire de metodele instanță, care sunt
transmise folosind obiectul în sine. Pentru ca o metodă să fie bazată pe clasă, trebuie să aibă
un parametru căruia i se va transmite automat o referință la clasa peste care este apelată
metoda atunci când aceasta este apelată (acest parametru este, de obicei, denumit cls).
• Metodele clasă sunt definite ca şi alte metode și sunt marcate cu un decorator special
@classmethod.

În această lecție ne vom concentra din nou pe membrii statici și instanță. Am lucrat deja cu datele și
acum ne mutăm pe metodele din cadrul claselor. Vă vom arăta ce sunt metodele statice și instanță, care
sunt diferențele dintre ele și cum influenţează implementarea codului.
Vom repeta, mai întâi, regulile pentru câmpuri, adică datele din clasă. În Python, principiul este simplu.
Variabilele clasă sau statice sunt partajate cu toate obiectele din clasă, în timp ce variabilele instanță sau
non-statice sunt diferite pentru fiecare obiect în parte, deoarece fiecare obiect conține o copie a
variabilelor.

1class Car:
2
3 make = 'BMW' # Class Variable
4
5 def __init__(self,name,speedLimit):
6 [Link] = name # Instance Variable
7 [Link] = speedLimit # Instance Variable
8
9
10#Objects of Car class
11
12a = Car("Fiat","360")
13b = Car("Mazda","380")
14
15print([Link]) # prints BMW
16print([Link]) # prints BMW
17
18print([Link]) # prints Fiat
19print([Link]) # prints Mazda
20
21print([Link]) #prints 360
22print([Link]) #prints 380

Page 29 of 107
Vedem asta în exemplul de mai sus. Deci, avem o variabilă clasă sau o variabilă statică make. Dacă
instanțiem o clasă prin două obiecte diferite, atunci când imprimăm, indiferent ce obiect apelăm,
obținem același rezultat - BMW, deoarece aşa a fost definit în clasă.

Pe de altă parte, pentru cele două variabile instanță, name și speedLimit, avem un rezultat diferit, pentru
că le-am transmis parametri diferiți atunci când am instanțiat clasele. Valorile lor depind direct de
instanța pe care sunt apelate, așa că obținem o schimbare a valorii de la Fiat la Mazda.

Spre deosebire de alte limbaje de programare, Python facilitează lucrul cu câmpurile de clasă statice. În
alte limbaje, trebuie utilizat un cuvânt cheie static, în timp ce în Python nu este nevoie de declarații
speciale - este suficient să definim o valoare în cadrul clasei și ea devine automat un câmp static.

Pe lângă câmp, ca membri ai clasei putem avea şi metode. După tip, metodele pot fi împărțite în
metode instanță, statice și clasă. Vom începe această lecție cu metodele instanță, deoarece în acest
moment ne sunt cele mai apropiate.

Metodele instanță

Metodele instanță sunt exact metodele pe care le-am folosit în exemplele lecțiilor anterioare. Sunt
caracterizate prin faptul că există în contextul obiectului și asta înseamnă că toți membrii obiectului
clasei în care se află le sunt la dispoziţie.

Pentru ca o metodă să fie instanțiată, trebuie să aibă un parametru căruia i se va transmite automat, în
timpul apelării, o referință la obiectul peste care e apelată metoda (acest parametru este, de obicei,
denumit self).

1class Car:
2 model = ""
3 def set_model(self):
4 [Link] = "R8"
5
6car = Car()
7car.set_model()
8print([Link])

În exemplu, vedeți o metodă instanță care, atunci când este apelată peste un obiect din clasa Car,
setează valoarea variabilei model din cadrul obiectului la R8.

Deci, metoda instanță are un parametru de bază self care indică instanța clasei Car și asta doar atunci
când metoda este apelată. După activarea metodei instanță, aceasta are acces liber la toate câmpurile
și metodele obiectului peste care este apelată. Acest lucru ne oferă un control mare asupra schimbării
stării obiectelor. Datele se schimbă la nivel de instanță, așa cum am văzut în primul exemplu din lecție.

1class Student:
2
3 def __init__(self, a, b):
4 self.a = a
5 self.b = b
6
7 def avg(self):
8 return (self.a + self.b) / 2
9

Page 30 of 107
10s1 = Student(10, 20)
11print( [Link]() )

În acest exemplu vedem încă un caz de utilizare a metodelor instanță. Concret, variabilele a și b sunt
variabile instanță inițializate atunci când este creat un obiect al clasei Student. Când vrem să apelăm
metoda average(), care este o metodă a unei instanțe, trebuie să creăm un obiect pentru clasa care
deține această metodă.

Metode statice

Metodele statice sunt definite ca şi alte metode, dar sunt apelate direct prin clasă, și nu prin intermediul
instanței ei, adică al obiectului. Ele pot, dar nu trebuie, să fie marcate cu un decorator special
@staticmethod.

Exemplu de metodă statică:

1class Car:
2 make=""
3 model=""
4 wheels=4
5
6 @staticmethod
7 def HowManyWheels():
8 print([Link])
9
[Link]()

În exemplu, vedeți că metoda HowManyWheels este declarată statică folosind o funcție/un decorator
special. Decoratorul împiedică instanțierea metodei și aceasta este accesată printr-o clasă pe care o
vedem în ultima linie de cod.

De ce sunt utile metodele statice?

Nevoia de metode statice apare atunci când avem un set de cod pe care îl putem folosi fără să instanțiem
o clasă. De exemplu, putem să avem o metodă clasă Car, care convertește milele în kilometri și veți fi de
acord că uneori în program poate va fi nevoie de o astfel de conversie, dar atunci nu va mai fi nevoie să
instanțiem obiectul complet cu toate câmpurile și metodele, deci să instanțiem o mașină completă doar
pentru a efectua o conversie simplă. În acest caz, cea mai bună alegere este metoda statică pe care o
putem apela din clasă să efectueze calculul, în loc să instanțiem și să transmitem valoarea metodei prin
obiect.

Important pentru noi este faptul că metodele statice nu au acces prin intermediul parametrului self.
Acestea funcționează exact la fel ca funcțiile obișnuite, doar că aparțin unei clase. Se scriu procedural și
ar trebui folosite doar în cazurile absolut necesare în care facem economie scriind o metodă statică și
nu una instanță, pentru că economisim resurse și optimizăm programul, și nu este nevoie să instanțiem
o clasă întreagă dacă avem nevoie doar de o metodă care nu este neapărat o parte a obiectului care se
instanțiază.

Deși reprezintă un concept interesant, aceste metode sunt rareori folosite în practică, tocmai din cauza
distanței față de programarea orientată pe obiecte și adesea sunt o indicație a unui design slab al
programului dacă sunt folosite într-o măsură mai mare.

Page 31 of 107
Metodele clasă

Metodele clasă sunt transmise direct clasei, spre deosebire de metodele instanță care sunt transmise
folosind obiectul în sine. Pentru ca o metodă să fie bazată pe clasă, trebuie să aibă un parametru căruia
i se va transmite automat o referință la clasa peste care este apelată metoda atunci când aceasta este
apelată (acest parametru este de obicei denumit cls).

Metodele clasă sunt definite ca şi alte metode și sunt marcate cu un decorator @classmethod special.

Deoarece metoda clasă are acces doar prin parametrul său cls, nu poate să modifice starea obiectului
în sine. Totuşi, metoda unei clase are posibilitatea să schimbe starea directă a clasei în sine, care se
aplică apoi pe toate instanțele. Deci, această abordare este complet diferită de cele pe care le cunoaștem
în prezent. Să vedem de ce metode clasă ne pot fi de ajutor.

Să spunem că avem o clasă User pe care o vom folosi pentru a lucra cu informații despre utilizatorii
aplicației noastre şi acea clasă, desigur, are câteva câmpuri:

1class User:
2 name = ""
3 userid = ""
4 number = ""

Și acum vrem să atribuim valori câmpurilor clasei. Prima opțiune ar fi să instanțiem clasa și apoi să
accesăm manual fiecare câmp și să-i atribuim o valoare. Desigur, am spus mai devreme că vrem să
evităm duplicarea și să automatizăm cât mai multe procese. Cu acea ocazie, am menţionat că, în acest
caz, este mai bine să folosim metoda constructorului __init__, cu care accesăm și schimbăm valorile la
nivel de instanță, deci obiect. Aceasta ar arăta, de exemplu așa:

1class User:
2
3 def __init__(self, name, userid, number):
4 [Link] = name
5 [Link] = userid
6 [Link] = number
7 print ([Link],[Link],[Link])
8
9new_user = User("John","34734",12356)

Deci, instanțiem clasa pe obiectul new_user și, datorită constructorului, transmitem parametrii direct în
timpul instanțierii.

Acest principiu este absolut corect și orientat pe obiecte, dar avem o problemă atunci când vrem să
facem o intrare, de exemplu, prin intermediul funcției input. Atunci, codul arată aşa:

1class User:
2
3 def __init__(self, name, userid, number):
4 [Link] = name
5 [Link] = userid
6 [Link] = number
7 print ([Link],[Link],[Link])
8

Page 32 of 107
9new_user = User(
10 input('Name: '),
11 int(input('User ID: ')),
12 int(input('Phone number: ')),
13)

Deci, cu ocazia instanțierii, transmitem intrările ca parametri și problema este că trebuie să transmitem
aceste trei linii de cod pentru fiecare adăugare de utilizator în parte. Asta ne îndepărtează de dorința
noastră de a automatiza cât mai mult procesul. Aici intervine metoda clasă. Acum putem să folosim
metoda clasă ca să scriem în câmpurile clasei în loc să o facem prin obiect. Vedeți un exemplu de metodă
a clasei pentru această problemă:

1class User:
2
3 def __init__(self, name, userid, number):
4 [Link] = name
5 [Link] = userid
6 [Link] = number
7
8 @classmethod
9 def input_user(cls):
10 [Link] = input('Name: ')
11 [Link] = int(input('User ID: '))
12 [Link] = int(input('Phone number: '))
13 return ([Link],[Link],[Link])

Am definit acum o nouă metodă clasă input_user. Parametrul cls stochează toate valorile pe care
utilizatorul le introduce și pe care le returnăm din metodă în clasă.
Puterea reală a utilizării metodei clasă se reflectă în modul în care inițiem adăugarea unui utilizator nou.
Când metoda clasă este utilizată în felul acesta, este suficient să apelăm metoda peste clasă:
1User.input_user()
după care funcția input va fi activată automat și va începe introducerea. Deși nu avem nevoie de apel, îl
putem salva în cadrul obiectului și, în felul acesta, îl returnăm în metoda init:

1new_user = User.input_user()
2print(new_user)

Ați văzut că tocmai prin crearea unei metode clasă, putem să accelerăm și să facilităm semnificativ câteva
dintre procesele care au loc în cadrul clasei noastre și, în același timp, putem să reducem lungimea
codului și să optimizăm funcționarea programului nostru.

Exerciţiu

Creați clasa care va fi utilizată pentru instanţierea obiectelor sub forma unei forme geometrice de paralelipiped.
Definiți metodele pentru calcularea volumului și a ariei obiectului. Valorile celor trei laturi ale paralelipipedului
sunt definite la instanțierea clasei.

Page 33 of 107
2.4. Decoratori

Rezumat

• Funcția decorator constă în a prelua o funcție și a-i adăuga un anumit cod sau o anumită
funcționalitate și apoi, în a returna funcția. Acest mod de lucru se numește metaprogramare,
deoarece o parte a programului încearcă să schimbe cealaltă parte în timpul executării
programului.
• Decoratorii nu influenţează direct funcționalitatea, deci nu modifică codul funcțional de bază și
nu modifică structura clasei în sine.
• Decoratorii pot fi împărțiți practic în două tipuri: încorporaţi și definiţi de utilizator.
• Pentru noi, cei mai cunoscuţi decoratori încorporați sunt: @staticmethod şi @classmethod.
• Metoda __call__ permite ca orice obiect, chiar și clasa în sine, să fie apelat ca funcție, folosind
parantezele(). Această metodă este o condiția prealabilă de bază pentru a crea un decorator
obiect.

Vom dedica această lecție noțiunii de decorator în Python. Mai întâi, vom explica la ce servesc
decoratorii și câte tipuri există și vom demonstra cum creăm decoratorii în Python. În lecția anterioară,
am menționat doi decoratori - respectiv @classmethod și @staticmethod. Pe scurt, am spus că sunt, de
fapt, funcții care înglobează alte funcții existente și le adaugă anumite funcționalități.

Simplu spus, funcția unui decorator este să preia o funcție și să-i adauge un cod sau o funcționalitate și
apoi să o returneze. Acest mod de lucru se numește metaprogramare, deoarece o parte a programului
încearcă să schimbe cealaltă parte în timpul executării programului.

Este important să știm că decoratorii nu influenţează direct funcționalitatea, deci nu modifică codul de
bază al funcției și nici nu modifică structura clasei în sine.

Practic, decoratorii pot fi împărțiți în două tipuri: încorporaţi și definiţi de utilizator.

Am avut ocazia să folosim decoratori încorporați în lecțiile anterioare, iar în această lecție ne vom axa
pe decoratorii definiți de utilizator.

Imaginea 6.1. Exemple de decoratori încorporați în Python

Decoratorii încorporați modifică funcția metodei pe care sunt definiți. Prin urmare:

1@staticmethod

Page 34 of 107
decoratorii schimbă metoda, astfel încât să nu folosească parametrul self; după această modificare,
metoda nu mai are acces la clasele instanță, în timp ce decortorul

1@classmethod

schimbă metoda, astfel încât să primească clasa ca prim parametru în locul clasei instanță (deci, cls în
loc de self, dacă ne uităm la sintaxa limbajului); după această modificare, metoda are acces direct la
clasă în locul obiectului ei.
Putem să identificăm decoratorul după semnul @ din fața numelui și după poziția sa, care este, de
obicei, deasupra clasei sau a membrului ei (imaginea 6.1.).

Decoratori definiți de utilizator

Aşa cum am menţionat la început, în Python putem să creăm singuri decoratori. Apoi, îi vom folosi după
nevoie, pentru a schimba o metodă sau o funcție.

Imaginea 6.2. Definiţia şi utilizarea decoratorilor

Să vedem un exemplu de bază de decorator definit de utilizator. Acesta constă în folosirea decoratorului
pentru a apela o funcție și a returna rezultatul. De exemplu:

1def make_pretty(func):

2 def inner():

3 print("I got decorated")

4 func()

5 return inner

7def ordinary():

8 print("I am ordinary")

Page 35 of 107
În exemplul nostru, decoratorul este exact funcția make_pretty. Acest decorator ia ca parametru variabila
func, care reprezintă funcția ce va fi transmisă decoratorului. Această logică se află în funcția internă a
decoratorului, unde avem o imprimare (care va reprezenta schimbarea funcției apelate). După aceea,
avem un apel al funcției și datele sunt returnate decoratorului.
A doua funcție, numită ordinary, este o funcție pe care o vom schimba folosind un decorator; conține
doar o imprimare obişnuită. Modificările le vedem apelând funcțiile și executarea lor:

Explicație:

Deci, apelând doar funcția ordinary(), demonstrăm ce face funcția neschimbată, și anume imprimă textul
„I am ordinary”. Dacă în variabila nouă apelăm decoratorul make_pretty și transmitem funcția
ordinary ca parametru, obținem imprimarea „I got decorated” și imprimarea „I am ordinary”.

Acum decoratorul a schimbat funcția ordinary, astfel încât să efectueze două imprimări, una definită de
decorator și cealaltă care exista deja în funcție. Aici vedem în acelaşi timp și principiul conform căruia
decoratorul nu schimbă funcția care îl înconjoară, ci doar adaugă la codul existent.

Acum puteți face modificarea în cod unde veți crea funcția make_pretty2 care va scrie "I got decorated"
și înainte și după apelarea funcției func.

Poate nu înțelegeți de ce nu am folosit sintaxa @decorator în cod. Această sintaxă a decoratorului este
reprezentată de următoarea linie: pretty = make_pretty(ordinary)

Deci, am putea scrie codul astfel:

1def make_pretty(func):

2 def inner():

3 print("I got decorated")

4 func()

5 return inner

8@make_pretty

9def ordinary():

10 print("I am ordinary")

11

12

13ordinary()

Acum, cu @make_prety apelăm funcția decorator și îi transmitem funcția de mai jos. Logica se execută
și după aceea, apelând funcția ordinary, obținem acum funcția decorată fără un accent anume pe
make_pretty, deoarece acest lucru s-a făcut deja când am setat @make_pretty deasupra funcției
ordinary.

Page 36 of 107
Această abordare de a apela decoratorul este o practică bună și o vom folosi în continuare pe parcursul
lecției.

Acum puteți schimba exemplul astfel încât veți adăuga ca funcția inner să scrie stringul "I got
decorated" și înainte și după apelarea funcției func.

Să vedem un exemplu de utilizare:

1def decorator(operation):

2 def inner(function):

3 if operation == "+":

4 return lambda a,b: a+b

5 elif operation == "-":

6 return lambda a,b: a-b

8 return function

9 return inner

10

11

12@decorator("+")

13def function1(a,b):

14 pass

15

16@decorator("-")

17def function2(a,b):

18 pass

19

20

21print(function1(4,3))

22print(function2(4,3))

În compilatorul de la sfârșitul lecției scrieți taskul precedent unde veți face operații de înmulțire și de
împărțire (aveți grijă ca al doilea număr pentru împărțire să nu fie 0).

Deci, în acest exemplu, vom defini un decorator care va avea ca variabilă de intrare o operație. În
interiorul decoratorului există o funcție care va avea ca parametru variabila function. Ca și în cazul
anterior, folosim această variabilă ca să transmitem numele funcțiilor. În funcție, adică în blocul de cod
care adaugă funcționalitate, verificăm care operație este selectată și pe această bază returnăm rezultatul
corespunzător.

Page 37 of 107
După aceea, definim primul decorator căruia îi transmitem manual operația sub forma unui string. În
decorator plasăm, desigur, exact funcția a cărei funcționalitate vrem să o extindem. În acest exemplu,
am setat comenzile Pass, deoarece toată logica este realizată în interiorul decoratorului și nu avem
nevoie de alte procese în funcțiile function1 şi function 2.

Când se afișează rezultatele, funcțiile și decoratorul sunt activate. Pe baza parametrului dat decoratorului,
se activează o anumită parte a structurii ifelse și se calculează rezultatele.

Desigur, aceasta poate fi o realizare realistă a funcționalității, dar există și modalități mai simple de a
transmite parametrii pentru o anumită funcționalitate. Cel mai important lucru pentru noi este să
observăm în acest exemplu că decoratorul poate primi un parametru pe care îl putem folosi ulterior,
precum și faptul că decoratorii nu interferează cu ceea ce face funcția în care se află.

Până acum, nu am folosit o abordare orientată pe obiecte, deoarece este important să înțelegem, în
primul rând, cum funcționează decoratorii. În continuare, vom analiza și utilizarea lor în abordarea
orientată pe obiecte.

Decorator obiect

Decoratorii sunt folosiți pe scară largă și recomandați în programarea orientată pe obiecte. Un exemplu
des întâlnit de utilizare a lor este crearea unei clase decorator. Această clasă ar avea un singur scop, și
anume să fie decorator. Apoi, când activăm decoratorul, respectiv apelăm metoda decorată, se
instantanţiază un obiect de clasă.

Având în vedere că până acum am folosit decoratorii doar ca funcții, acum vom defini un decorator ca
clasă. Dar, înainte de a face asta, trebuie să mai explicăm încă o metodă magic nouă pentru noi și
aceasta este metoda __call__ a clasei.

Am menționat mai devreme că un decorator este un obiect pe care îl putem apela și care poate prelua
o funcție ca parametru. Problema este că nu putem apela clasa în acest fel, iar acest lucru este necesar
pentru obiectul decorator. Aici intră în joc metoda __call__. Ea permite ca orice obiect, chiar și clasa în
sine, să fie apelată ca o funcție folosind parantezele().

Vom lua exemplul anterior din lecție și vom schimba abordarea, astfel încât să fie orientată pe obiect.
După aceea, vom comenta codul și diferențele importante în aplicare.

Exemplu de decorator ca și clase

Codul

1class decorator:

2 def __init__(self,operation):

3 [Link] = operation

4 def __call__(self,function):

5 if [Link] == "+":

6 return lambda a,b: a+b

7 elif [Link] == "-":

Page 38 of 107
8 return lambda a,b: a-b

9 return function

10

11

12@decorator("+")

13def function1(a,b):

14 pass

15

16@decorator("-")

17def function2(a,b):

18 pass

19

20

21print(function1(4,3))

22print(function2(4,3))

Scrieți codul în mediul de lucru și verificați rezultatul:


După cum vedeți, rezultatul este următorul:

7
1

Ceea ce observăm în exemplu este că partea de jos a codului din exemplul nostru nu s-a schimbat deloc,
ca și cum am lucra în întregime cu funcții în loc de clasă și obiecte. Aici stă puterea metodei __call__ în
combinație cu decoratorul. Am definit clasa decorator. În cadrul ei avem două metode: init și call.
Metoda init ne permite să transmitem un parametru, iar call ne permite să apelăm o clasă ca și cum ar
fi o metodă. Logica din cadrul metodei call este exact aceeași ca în exemplul anterior.

Acum folosiți compilatorul astfel încât să completați codul cu operații de înmulțire și împărțire ca și în
exemplul precedent.

Cea mai mare schimbare în abordarea obiectului este aceea că apelând decoratorul @decorator,
instanțiem clasa decorator, adică îi creăm obiectul. Acest lucru ne permite să avem în aplicații mai mari
o clasă care va servi drept decorator, în loc ca decorator să fie una dintre metodele unei clase mai mari,
ceea ce ne poate crea probleme atunci când o folosim des.

Să rezumăm această lecție - cel mai important aspect al unui decorator este că acesta cuprinde o
funcție/metodă și îi adaugă o anumită funcționalitate fără a face modificări pe ea. Putem să folosim
decoratorii atât în abordări procedurale, cât și în abordări orientate pe obiecte, unde decoratorul poate
fi o clasă cu exact acest scop specific.

Page 39 of 107
Exerciţiul 1:

Folosind exemplul funcției decorator care permite selectarea unei operații matematice, realizați un calculator.
Acest calculator ar trebui să aibă patru operații (adunare, scădere, împărţire și înmulţire). Este necesar să
permiteți utilizatorului să introducă operația (+ - * /) și două numere.

Exerciţiul 2:

Folosind codul din exercițiul anterior, utilizați abordarea orientată pe obiecte (clasa decorator) pentru a realiza
funcționalitatea programului.
Rezolvați singuri taskul:

Page 40 of 107
2.5. Încapsulare, moștenire, polimorfism

Rezumat

• Încapsularea ne permite să grupăm direct metodele și proprietățile în pachete care conțin


diferite niveluri de acces la date.
• Python este un limbaj specific prin faptul că nu are modificatori de acces la metode și clase.
Modificatorii din Python doar îi amintesc programului să acceseze părțile protejate ale clasei,
dar nu există un mecanism care să interzică accesul.
• Membrii protejați reprezintă valori care ar trebui utilizate doar în cadrul clasei și al copiilor ei.
Pentru a face acest lucru, în Python, în fața numelui proprietății sau a câmpului, specificăm
underscore: _. De exemplu: _protectedAtribute= 702991.
• Membrii privați reprezintă valorile care nu trebuie folosite deloc în afara clasei și nici în clasele
care sunt copiii clasei părinte. Pentru a realiza acest lucru, în Python, în fața numelui proprietății
sau a câmpului, specificăm double underscore: __.
• Membrii privați împiedică accesarea câmpurilor clasei dacă folosim metoda regulată de lucru.
• Moștenirea reprezintă un mecanism prin care clasa își partajează membrii (metode și câmpuri)
cu o altă clasă în timpul executării programului.
• Sintaxa pentru moștenirea clasei este: class Dog(Animal). Deci, la definirea clasei părinte, punem
între paranteze numele clasei părinte ca și parametru: class Child(Parent).
• Putem să transferăm dintr-o clasă părinte într-o clasă copil doar membrii publici și protejați,
deoarece dacă încercăm să folosim un membru privat, vom primi eroare, fiindcă membrul privat
al clasei este destinat doar pentru a fi utilizat în cadrul ei şi copiii clasei nu au acces.
• Funcția super() este specifică moștenirii în Python. Cu această funcție, putem să indicăm direct
membrii părintelui în clasa copil.
• Polimorfismul în programarea orientată pe obiecte înseamnă că o metodă dintr-o clasă se va
comporta diferit de o metodă cu același nume din altă clasă, ambele clase având același părinte.
• Suprascrierea metodelor permite crearea mai multor variații ale metodei cu același nume, dar
cu aplicare diferită.

În această lecție ne vom concentra pe încapsulare, moștenire și polimorfism. În lecțiile anterioare ale
cursului, unde am vorbit despre principiile teoretice ale programării, am menționat și aceste
paradigme de programare. În această lecție vom demonstra cum se realizează aceste paradigme și ce
posibilități ne oferă atunci când scriem un cod orientat pe obiecte.

Încapsularea

Încapsularea se referă la modul în care o interfață este separată de implementare. Putem să înțelegem
încapsularea și ca pe o modalitate de a ascunde date, permițând ca anumite părți ale codului să fie
vizibile, în timp ce altele să rămână ascunse. Acest lucru aduce avantaje în funcționare și utilizare, atât
pentru utilizator, cât și pentru programator.

Încapsularea ne permite să grupăm direct metodele și proprietățile în pachete care conțin diferite
niveluri de acces la date. De exemplu, metoda sau proprietatea definită implicit se consideră publică și,
ca atare, se poate utiliza din alte părți ale codului aplicației noastre.

Pe de altă parte, putem să protejăm anumite părţi de cod; atunci metodele și proprietățile pot fi utilizate
exclusiv de către clase, în timp ce copiii părinților și nici o altă clasă nu au acces.

Această abordare este încapsularea. Programatorilor li se permite să creeze o interfață, deci o modalitate
prin care clasele pot ascunde părţi de cod care sunt folosite pentru clasa însăși și nu trebuie schimbate.

Page 41 of 107
Acest principiu ține pasul cu nevoia principală a programării orientate pe obiecte, care este o cât mai
bună organizare a codului.

Python este specific în acest caz, deoarece nu există modificatori de acces peste metode și clase,
respectiv comenzi care să ne permită să schimbăm condițiile de acces la o metodă. Multe limbaje, de
exemplu, C++ și Java, au conceptul de modificatori de acces. De exemplu, C++ are modificatorul de
acces private. Acesta stabileşte ca metodele și proprietățile să poată fi accesate doar de clasa în care au
fost definite. Modificatorul de acces Protected stabileşte ca doar copiii dintr-o clasă să aibă acces, iar
modificatorul public stabileşte că orice clasă poate să acceseze toate elementele.

Acest lucru, din păcate, nu este posibil în Python, dar avem în continuare posibilitatea să încapsulăm
codul, doar că în alt mod.

Python folosește modificarea accesului, dar nu ca să interzică accesul, ci ca să schimbe valorile protejate,
astfel încât să fie mai greu de accesat și subliniază în mod clar programatorului că nu ar trebui să le
folosească.

În Python, putem să protejăm o valoare în două moduri: să o păstrăm protejată și să o păstrăm privată.

Valori protejate (protected)

Membrii protejați sunt valori care ar trebui utilizate doar în cadrul clasei și al copiilor ei. Ca să facem
acest lucru în Python, în fața numelui proprietății sau a câmpului, specificăm underscore: _.

Este important să menţionăm că, așa cum am spus mai devreme, Python nu împiedică accesul, ci îl
anunță pe programator că nu ar trebui să utilizeze o valoare protejată și privată. În cazul unei valori
protejate, underscore din fața numelui este doar o indicație pentru programator că variabila sau metoda
care foloseşte underscore este utilizată doar în scopuri interne. Putem înțelege acest lucru ca pe un
semn că nu ar trebui să folosim acea valoare, în timp ce C++ sau Java au efectiv un mecanism care să
ne împiedice să o folosim. Dar revenind la Python, să vedem un exemplu de utilizare a unei valori
protejate:

1class Example():
2 def __init__(self):
3 self._protectedAtribute= 702991
4
5 def _protectedMethod(self):
6 print("Hello from protected method")
7
8p1 = Example()
9p1._protectedMethod()
10print(p1._protectedAtribute)
Deci, în exemplu, vedem că în cadrul clasei am stabilit două metode. Prima este, desigur, init, pe care
am întâlnit-o mai devreme, dar această metodă are în ea o valoare protejată. Deci, am folosit un
underscore ca să specificăm că valoarea este protejată. După aceea, în cod mai avem încă o metodă
(denumită în mod arbitrar protectedMethod) și, după cum vedeți, şi ea este protejată cu underscore.
Dacă instanțiem clasa, vedem că avem acces atât la metode, cât și la atribute și s-ar putea să vă întrebați
acum - deci, ce rost au toate astea dacă în continuare pot să accesez totul?

Page 42 of 107
Underscore este un indicator clar a ceea ce vreți să accesați, deoarece trebuie să puneți underscore în
fața numelui atunci când apelați metode și valori. Acesta este un indicator clar al faptului că specificaţi
conștient ceva ce fie dvs., fie un alt programator a protejat atunci când a scris clasa.

Dacă înlăturăm underscore din fața numelui, deci încercăm să-l păcălim pe Python, vom primi mesaje
de eroare, deoarece numele nu se vor mai potrivi.

Valori private (private)

Membrii privați sunt asemănători cu cei protejați; diferența constă în faptul că câmpul clasei care a fost
declarat privat nu ar trebui să fie folosit deloc în afara clasei şi nici în clasele care sunt copiii clasei părinte.
Ca să facem acest lucru în Python, în fața numelui proprietății sau a câmpului, specificăm double
underscore: __.

Spre deosebire de modificarea anterioară a abordării, aici abordarea este blocată dacă folosim metoda
obişnuită de lucru. Vom lua exemplul anterior, doar că vom schimba valorile de la protejat la privat, deci
de la un underscore la double underscore:

1class Example():
2 def __init__(self):
3 self.__privateAtribute= 702991
4
5 def __privateMethod(self):
6 print("Hello from private method")
7
8p1 = Example()
9p1.__privateMethod()
10
11print(p1.__privateAtribute)

Deci, avem un câmp privat __privateAtribute și o metodă privată __privateMethod. Dacă încercăm să
rulăm codul, așa cum am făcut şi înainte, obținem următoarea imprimare:

Traceback (most recent call last):


AttributeError: 'Example' object has no attribute '__privateMethod'

Deci, obținem o imprimare de eroare unde se specifică că obiectul Example nu are atributul
privateMethod, deși vedem clar că a fost definit corect. Acest lucru se întâmplă datorită utilizării lui __
în fața numelui metodei. Apoi Python schimbă numele metodei, astfel încât să nu o putem accesa printr-
un apel obișnuit și în felul acesta protejează metoda de utilizarea sau schimbarea accidentală a valorii
ei.

Utilizarea unui underscore atenţionează programatorul că nu ar trebui să folosească proprietățile sau


câmpurile unei clase în afara clasei în sine sau a copiilor ei. Nu ne împiedică să le folosim, dar servește
ca indicație, în timp ce dacă folosim double underscore creăm, de fapt, o valoare privată pe care nu o
putem accesa cu metode obișnuite și Python returnează o eroare dacă încercăm să o folosim. Acestea
sunt două metode prin care putem proteja, adică, încapsula codul nostru.

Odată ce ne-am familiarizat cu modificatorii de acces la câmpuri, putem să implementăm un sistem de


încapsulare popular, și anume utilizarea metodelor getter și setter.

Page 43 of 107
Getters și seters sunt metode care se utilizează pentru accesul controlat la câmpurile de clasă private.
Câmpurile care sunt expuse prin getters și setters se numesc proprietăți, adică, properties. Să vedem
următorul exemplu:

1class Point:
2 def __init__(self):
3 self.a=0
4 self.b=0
5 def move(self):
6 self.a+=1
7 self.b+=1

Putem să spunem că clasa Point are funcția de a stoca poziția unui punct și de a-i schimba coordonatele.
Dar, în acest exemplu, putem să provocăm ca clasa să creeze o problemă de execuție printr-o utilizare
necorespunzătoare, datorită expunerii câmpurilor sale cheie (a, b). De exemplu:

1class Point:
2 def __init__(self):
3 self.a=0
4 self.b=0
5 def move(self):
6 self.a+=1
7 self.b+=1
8
9pt = Point()
10pt.b("Hello")
[Link]()

Clasa s-ar aștepta ca câmpul b să fie integer, dar, din „greșeală”, noi am trecut string. În acest caz, în loc
de un răspuns adecvat clasei, obținem:

TypeError: can only concatenate str (not "int") to str

Această eroare întrerupe apoi funcţionarea programului complet din cauza unui parametru trecut
incorect.

Folosind metodele getter și setter, putem să controlăm cum sunt accesate câmpurile importante și cum
li se poate modifica valoarea. Să vedem același exemplu, dar cu implementarea metodelor getter și
setter pentru unul dintre câmpuri:

1class Point:
2 def __init__(self):
3 self.__a=0
4
5 def get_a(self):
6 return self.__a
7 def set_a(self,a):
8 if not isinstance(a,int):
9 return
10 self.__a = a
11 def move(self):
12 self.__a+=1

Page 44 of 107
13
14pt = Point()
15pt.set_a = "Hello"
[Link]()

Acum avem două metode noi: get_a, care va reprezenta metoda getter și set_a, care va reprezenta
metoda setter. Metoda get_a, așa cum îi sugerează și numele, preia o valoare, în timp ce metoda setter
o setează, deci creează o valoare. Aici avem condițiile ca, dacă tipul de date din inițializare nu este int,
programul să nu facă nimic și, dacă este - să salveze valoarea. Dacă rulăm acum programul cu un
parametru trecut incorect, vom vedea că nu există nicio întrerupere a programului și nicio raportare a
unei erori, deoarece am definit ce se întâmplă și cum se procesează variabila în cazul unei introduceri
incorecte. Desigur, dacă transmitem un număr, vom obține o funcționare normală, adică funcționarea
programată a programului.

În Python avem şi o soluție mai elegantă pentru implementarea getters și setters. Mai exact, folosim
decoratorul @property ca să definim o metodă care se va comporta ca un getter, în timp ce decorăm
setterul folosind sintaxa @[Link]. Să ne uităm la această soluție aplicată în exemplul
anterior:

1class Point:
2 def __init__(self):
3 self.__a=0
4
5 @property
6 def a(self):
7 return self.__a
8
9 @[Link]
10 def a(self,a):
11 if not isinstance(a,int):
12 return
13 self.__a = a
14 def move(self):
15 self.__a+=1
16
17pt = Point()
18pt.set_a = "Hello"
[Link]()

Moştenirea

Aşa cum am menționat mai devreme, moștenirea este un mecanism care ne permite să folosim un cod
deja scris de mai multe ori. În acest caz, moștenind o clasă, putem să folosim codul ei în altă clasă.

Dacă definim acest lucru printr-o abordare orientată pe obiecte, moștenirea este un mecanism al clasei
prin care își partajează membrii (metode și câmpuri) cu o altă clasă în timpul executării programului. Să
vedem acest lucru într-o diagramă:

Page 45 of 107
Imaginea 7.1. Ilustrarea moștenirii

În diagramă vedem mai întâi clasa de bază Animal. Ea va stoca toate datele comune unor animale, așa
că am enumerat aici vârsta, numele și specia căreia îi aparține animalul.

Dacă avem alte trei clase în program care reprezintă câine, șoarece și pisică, procedura logică ar fi să nu
definim din nou metodele din fiecare clasă pentru vârsta câinelui, șoarecelui și pisicii, deci să repetăm
aceeași logică de trei ori. Este logic ca în exemplul clasei Dog să moștenim tot ce este în clasa Animal,
deoarece am scris acea clasă principală părinte, astfel încât să reprezinte caracteristicile animalelor în
general. Prin moștenire, am obține şi aceste trei metode și atunci ne-ar mai rămâne doar să adăugăm în
cadrul clasei Dog logica specifică câinilor. Să ne uităm la realizarea acestui caz în Python, deci cazul
moștenirii clasei Animal de către clasa Dog:

1# Base class
2class Animal:
3
4 # Class attribute
5 species = 'mammal'
6 name = " "
7 age = 0
8
9 # Instance attributes
10 def __init__(self, name, age):
11 [Link] = name
12 [Link] = age
13
14 # instance method
15 def description(self):
16 return "{} is {} years old".format([Link], [Link])
17
18 # instance method
19 def speak(self, sound):
20 return "{} says {}".format([Link], sound)

În prima parte a codului, am definit clasa Animal care reprezintă clasa noastră părinte. În ea am definit
deja date pentru un animal, iar acel animal a primit specia căreia îi aparține, înălțimea și vârsta. Pe lângă
acestea, în clasă mai avem trei metode: init ca să putem să introducem parametrii și două metode

Page 46 of 107
instanță - description şi speak. Ambele imprimă câteva date pe baza a ceea ce introducem. După aceea,
în partea a doua a programului:

1# Derived class (inherits from Animal class)


2class Dog(Animal):
3 def run(self, speed):
4 return "{} runs {}".format([Link], speed)
5
6# Derived class inherits attributes from the parent class
7
8rex = Dog("Rex", 10)
9print([Link]())<br>
10# Derived class has specific attributes and behavior as well
11print([Link]("slowly"))

vedem clasa Dog. Această clasă este copilul clasei Animal, deci ea moștenește tot ce este specificat în
clasa Animal. Aici, în definiția clasei, vedem și sintaxa necesară pentru a moșteni clasa, mai concret, în
linia:

1class Dog(Animal):

Deci, atunci când definim o clasă copil, între paranteze transmitem ca parametru numele clasei părinte.

1class Child(Parent):<br><br>
Clasa copil are o metodă care realizează imprimarea legată doar de clasa copil. Prin prezenta
reprezentăm o logică de program separată.

Apoi instanțiem clasa Dog și o stocăm în obiectul rex. Acești parametri sunt transmiși metodei run care
oferă imprimarea.

Dar partea cheie este:

1print([Link]())

Cu această linie, printr-o instanță a clasei Dog, accesăm metoda description, care este o parte a clasei
Animal - deci, prin moștenire, obținem toate atributele și metodele clasei Animal și le putem folosi liber,
prin intermediul clasei Dog. Desigur, pe lângă asta, ne mai rămâne în continuare toată logica pe care o
avem în cadrul clasei Dog și o putem folosi (print([Link]("slowly"))).

Dacă nu am putea folosi moștenirea, în clasa Dog ar trebui să copiem toate cele trei metode definite în
interiorul clasei Animal.

Este important să menţionăm că putem să transferăm din clasa părinte în clasa copil doar membrii
publici și protejați, deoarece dacă încercăm să folosim un membru privat, vom primi eroare, fiindcă
membrul privat al clasei este destinat să fie utilizat doar în cadul clasei şi copiii acelei clase nu au acces
la el.

Page 47 of 107
Imaginea 7.2. Moștenirea membrilor conform modificatorului de acces

Desigur, membrii protejați trebuie, de asemenea, să fie utilizați cu atenție, recomandarea fiind
întotdeauna aceea de a nu folosi câmpul dacă este specificat ca protejat, asta din cauza problemelor
care pot apărea ulterior în timpul dezvoltării aplicației.

Specific moștenirii în Python este funcția super(). Folosind această funcție, putem să indicăm direct
membrii părinți din interiorul clasei copil. Funcția super() este foarte importantă dacă are loc o
suprascriere a metodei. Suprascrierea este un fenomen în care metoda clasei părinte este suprascrisă
de clasa copil.

Să vedem următorul exemplu de moștenire, unde atât părintele, cât și copilul au o metodă numită
show() care va fi suprascrisă:

1class Person:
2 def __init__(self,firstname,lastname):
3 [Link] = firstname
4 [Link] = lastname
5 def show(self):
6 print("Person: ", [Link], [Link])
7
8class Student(Person):
9 def __init__(self,firstname,lastname,indexnumber):
10 super().__init__(firstname,lastname)
11 [Link] = indexnumber
12
13 def show(self):
14 print("Student: ", [Link], [Link],[Link])
15
16st = Student("John","Smith","120/001")
[Link]()

Deci, clasa părinte Person colectează și imprimă numele și prenumele persoanei, în timp ce clasa Student
moștenește datele și metodele clasei Person și adaugă numărul indexului. Situația specifică este cu
metoda show. Aceasta apare în ambele clase și este suprascrisă, astfel încât să fie activă doar metoda
show din clasa copil, pe care o putem vedea şi pe imprimare, unde vor fi imprimate toate datele, inclusiv
numărul indexului, care apare doar în clasa Student.

Dar ce facem în cazul în care avem nevoie de o metodă de tip părinte, care este acum suprascrisă? Deși
am suprascris metoda părinte, asta nu înseamnă că a fost pierdută iremediabil, adică că a fost într-
adevăr suprascrisă. Dacă avem nevoie de ea, o putem recupera cu funcția super.

Page 48 of 107
Apoi, e suficient să adăugăm următoarea linie de cod în cadrul metodei show, din cadrul clasei Student:

1super().show()

Apoi, cu un singur apel al metodei show(), obținem o imprimare a clasei Student și o imprimare a clasei
Person:

Student: John Smith 120/001


Person: John Smith

La sfârșitul acestei părţi despre moștenire, vom lua un exemplu teoretic al moștenirii, în care veți vedea
alte câteva modalități prin care putem realiza moștenirea:

1class Person:
2 def __init__(self):
3 pass
4
5# Single level inheritance
6class Employee(Person):
7 def __init__(self):
8 pass
9
10# Multi-level inheritance
11class Manager(Employee):
12 def __init__(self):
13 pass
14
15# Multiple inheritance
16class Enterprenaur(Person, Employee):
17 def __init__(self):
18 pass

Acest exemplu rezumă pe scurt nivelurile de moștenire. Primul nivel, marcat cu comentariul Single level
inheritance, reprezintă moștenirea de prim nivel, unde copilul moștenește părintele. Logica exemplului
este că Angajatul este, în acelaşi timp, și o persoană. Următorul nivel este multi-level. Aici avem cazul în
care o clasă nouă moștenește copilul - acum copilul este părintele acestei clase copil nouă. Logica
acestui tip de moștenire este că Managerul este și un angajat al companiei, deci el moștenește toate
trăsăturile. Și, la final, avem mai multe moșteniri - aici, o clasă copil are doi părinți, deci folosește două
clase care sunt apelate în funcție de ordinea în care au fost listate atunci când am definit clasa. Logica
din spatele acestei părți este că un antreprenor poate fi atât o persoană, cât și un angajat.

Să rezumăm, moștenirea permite reutilizarea codului. De exemplu, clasa Mașină poate fi reutilizată și
extinsă de clasa Taxi, iar apoi putem să folosim și clasa Taxi, să o extindem și să o reutilizăm în cadrul
unei clase noi, de exemplu, ElectricTaxi. Am folosit un șablon mașină pentru a-l extinde la un șablon
taxi și apoi am folosit din nou șablonul taxi pentru un șablon taxi alimentat electric. Desigur, la
aplicațiile mari, trebuie să investim mai mult timp și să facem câteva testări, pentru a realiza astfel de
relații, dar astfel de soluții pot să aibă un impact pozitiv asupra costurilor de dezvoltare, deoarece
elimină scrierea inutilă a logicii care face aceeași treabă în mai multe locuri în aplicație.

Page 49 of 107
Polimorfism

Ca şi celelalte paradigme, am prezentat deja polimorfismul în lecțiile anterioare. Acum ne vom focusa
pe polimorfismul în programare. Polimorfismul în programarea orientată pe obiecte înseamnă că o
metodă dintr-o clasă se va comporta diferit de o metodă cu același nume din altă clasă, ambele clase
având același părinte. În exemplele anterioare am avut cazul în care părintele și copilul au aceeași
metodă (în exemplele show), iar acum vom analiza cazul în care există doi copii cu același părinte care
folosesc aceleași metode.
De exemplu, dacă am spune că un câine și o pasăre sunt subclasele unei clase animale și că ambele au
metoda a mișca, aceste două metode, deși se numesc la fel, ar avea o realizare complet diferită,
deoarece pasărea zboară, iar câinele merge.
Aşa cum am menţionat, pentru ca polimorfismul să aibă loc, trebuie să existe cel puțin două clase copii.
Dacă luăm exemplul anterior, person şi student, în acest context, a doua clasă copil poate fi
clasa Professor; deci, vom extinde exemplul anterior la această clasă nouă:
1class Person:
2 def __init__(self,firstname,lastname):
3 [Link] = firstname
4 [Link] = lastname
5 def show(self):
6 print("Person: ", [Link], [Link])
7
8class Student(Person):
9 def __init__(self,firstname,lastname,indexnumber):
10 super().__init__(firstname,lastname)
11 [Link] = indexnumber
12
13 def show(self):
14 print("Student: ", [Link], [Link],[Link])
15
16class Professor(Person):
17 def __init__(self,firstname,lastname,subject):
18 super().__init__(firstname,lastname)
19 [Link] = subject
20 def show(self):
21 print("Professor: ", [Link], [Link],[Link])

Codul are acum o clasă Professor nouă, care moștenește clasa Person și codul ei este identic cu codul
clasei Student, dar profesorul are subject în loc de numărul indexului. Dacă acum instanțiem toate cele
trei clase:
1person=Person("John","Davidson")
2student=Student("Will","Smith","102/39")
3professor=Professor("Edward","Owen","Python Programming")
4
[Link]()
[Link]()
[Link]()

și apoi activăm metodele show, la ieşire obținem trei rezultate diferite:


Person: John Davidson
Student: Will Smith 102/39
Professor: Edward Owen Python Programming

Rezultatul este diferit pentru metodele cu același nume moștenite, deci, putem spune că a avut loc

Page 50 of 107
polimorfizarea. Deci, metodele cu același nume din două clase se vor comporta diferit, unde ambele
clase au același părinte.

Metoda overload (supraîncărcare)

În încheierea acestei lecții, vă prezentăm încă o noţiune din Python care este utilă atunci când lucrăm
cu polimorfismul și moștenirea; este vorba de metoda overload.
În primul rând, metoda overload implică posibilitatea de a crea multiple variații ale unei metode cu
același nume, dar cu aplicații diferite. Cu această abordare, vom avea mai multe rezultate diferite, în
funcție de ceea ce am introdus. O modalitate obișnuită de implementare a acestei abordări este trecerea
unui număr de parametri care corespund operaţiei necesare. Să vedem exemplul următor:
1class Person:
2
3 def Hello(self, name=None):
4 if name is not None:
5 print('Hello ' + name)
6 else:
7 print('Hello unknown')
8
9obj = Person()
10
[Link]()
12
[Link]('John')

Vedem definiția clasei Person care are o singură metodă Hello. Această metodă, pe lângă self, are un
parametru name, care la început este None. În cadrul metodei, facem următoarea verificare: dacă
valoarea variabilei nu este None, asta înseamnă că a fost introdus numele și, prin urmare, se afișează
utilizatorului textul de salut cu acel nume. Else, deci dacă name are în continuare valoarea None, atunci
afișează „Bună ziua, străine”.
Apoi, instanțiem obiectul clasei Person și abordăm metoda în sine în două moduri diferite. În primul caz,
nu transmitem parametrul și obținem imprimarea „Hello unkown”, în timp ce în al doilea caz, transmitem
parametrul name și obținem imprimarea „Hello John”. Acest exemplu simplu arată un principiu foarte
puternic, în care ne este permis să apelăm o metodă cu un număr arbitrar de parametri, în funcție de ce
avem nevoie.

Exerciţiul 1:
Creați clasa Card care are câmpurile număr și stare și metoda pay.
Metoda pay retrage o anumită sumă din soldul cardului.
Creați clasele Visa și Master, care moștenesc clasa Card, dar percep la plată și un impozit, care pentru cardul
Visa este de 5%, iar pentru cardul Master este de 8%.
Exerciţiul 2:
În cadrul acestui exercițiu, realizați un dicționar simplu în care utilizatorul introduce un cuvânt și obține
traducerea lui în două limbi.
Creați clasa Dictionary care are metoda get_word. Această metodă ar trebui să accepte un parametru string care
va reprezenta cuvântul căutat în dicționar. Prin urmare, metoda returnează traducerea cuvântului căutat. Condiția
este crearea subclaselor clasei Dictionary pentru două limbi: engleză și germană.
Notă: Stocați utilizând dicționarul (dictionary) în format: "hello":"Hallo", pentru fiecare dintre limbi.

2.6. Clase abstracte

Page 51 of 107
Rezumat

• Abstractizarea în Python este procesul de a ascunde realizarea aplicației de utilizator și de a


pune accent pe utilizarea aplicației.
• În Python putem să avem clase și metode abstracte.
• Practic, Python nu cunoaște clasele abstracte, dar ele pot fi simulate cu pachetul abc (abstract
base classes). Acest pachet sau modul conține logica de realizare a abstractizării.
• Dacă avem o clasă abstractă în fișier, trebuie să avem pachetul de import în partea de sus a
fișierului, cu comanda: import abc.
• Dacă vrem ca clasa să aibă posibilitatea de abstractizare, trebuie să moștenim clasa ABC din
pachetul abc. class Example([Link]):.
• Metoda abstractă poate fi înțeleasă ca un contract între o clasă abstractă și clasa care o
moștenește. Acesta garantează că clasa moștenitoare va respecta condițiile de participare în
restul sistemului.

Această lecție este dedicată abstractizării codului, adică claselor și metodelor abstracte. Vom demonstra
care le este semnificația și cum le integrăm în Python care, în acest sens, este mai specific în comparaţie
cu alte limbaje.

În Python, abstractizarea este procesul prin care realizarea aplicației este ascunsă utilizatorului, accentul
punându-se doar pe utilizarea aplicației în sine. Este important să menţionăm că utilizatorul poate fi un
alt programator care lucrează în echipa noastră. Abstractizarea împiedică utilizarea codului în mod inutil
sau chiar dăunător, caz în care un alt programator ar putea să folosească elemente ale claselor sau
metodelor pe care nu ar trebui să le folosească.

Aşa cum am mai spus, un exemplu de abstractizare din viață, binecunoscut nouă, este utilizarea unei
telecomenzi. Știm că trebuie să apăsăm un buton ca să schimbăm canalul, dar nu cunoaștem procesul
prin care comanda de pe casetă schimbă canalul pe televizor.

Dar să revenim la Python și la cum realizăm abstractizarea. În Python putem să avem clase și metode
abstracte.

Clase abstracte

Putem să privim, foarte simplu, clasele abstracte ca pe nişte șabloane pentru crearea altor clase.
Proprietatea lor de bază este că nu pot fi instanțiate, ci doar moștenite sau utilizate într-un context static.
Poate vă întrebați acum - când aș avea nevoie de așa ceva?

Să ne uităm la următorul cod:

class Product:
1 def __init__(self,name,price):
2 [Link]=name
3 [Link]=price
4 def tax(self):
5 return [Link] * 1.2
6 def buy(self):
7 print("You bought a product",[Link],"with a
8price",[Link](),"dollars")
9
10p = Product("Shoes",100)
[Link]()

Page 52 of 107
Vedem o clasă care reprezintă un produs într-un magazin online. Are o metodă prin care calculează
impozitul și apoi, în altă metodă îi arată utilizatorului ce a ales și la ce preț. Sunteți de acord că acesta
este un cod orientat pe obiecte complet valid. Totuşi, avem o problemă. Problema nu se află în sintaxă
și nu avem o eroare de logică, problema este că în viața reală nu cumpărăm niciodată ceva care se
numește produs.

Cumpărăm pantofi sau o mașină de o anumită marcă... În cazul nostru, produsul este prea abstract
pentru că nu ne spune exact ce cumpărăm, ci doar că aparține unei categorii și cum a fost calculată
valoarea lui. În programare exact această clasă ar trebui să devină abstractă, ca să nu o folosim pe ea, ci
pe derivata ei, pe cea care o moștenește. Asta ca să putem comanda efectiv un anumit articol.

Ideea unei clase abstracte este să nu instanțiem niciodată direct clasa Product, ci o altă clasă, de exemplu
Shoes, care este folosește șablonul clasei Product. Astfel, Product rămâne un concept străin care pe noi,
ca utilizatori, nu ne interesează, și putem să vedem și să folosim doar ceea ce vrem să cumpărăm, și
anume Shoes (Pantofi), iar în cadrul acelei clase avem o logică care se ocupă de pantofi, bazată pe
clasa Product.

Practic, Python nu cunoaște clasele abstracte, dar acestea pot fi simulate cu pachetul abc (abstract base
classes). Acest pachet sau modul conține logica pentru a realiza abstractizarea. Important este ca atunci
când în fișier avem o clasă abstractă, în partea de sus a fișierului să avem:

1import abc

ceea ce leagă fișierul nostru de pachet. Pentru ca clasa în sine să aibă posibilitatea de abstractizare, este
necesar să moștenească clasa ABC din pachetul abc.

1import abc
2class Product([Link]):

Este important să menţionăm următoarele: în Python, pentru ca o clasă să devină abstractă, trebuie să
conțină cel puțin o metodă abstractă. Ca să definim o metodă abstractă, folosim decoratorul
abstractmethod din același pachet. El va marca metoda de sub decorator ca fiind abstractă. Prin urmare,
putem să facem metoda tax() abstractă:

import abc
1class Product([Link]):
2 def __init__(self,name,price):
3 [Link]=name
4 [Link]=price
5
6@[Link]
7 def tax(self):
8 return [Link] * 1.2
9 def buy(self):
10 print("You bought a product",[Link],"with a
11price",[Link](),"dollars")
12
13p = Product("Shoes",100)
[Link]()

Page 53 of 107
Din acest moment, clasa Product este abstractă cu metoda abstract tax(). Dacă acum încercăm să rulăm
acest cod, obținem o eroare:

Can't instantiate abstract class Product with abstract methods tax

Nu mai putem să instanțiem clasa pentru că este abstractă. Acum au avut loc o serie de schimbări; mai
întâi, nu mai putem să instanțiem clasa, putem să o folosim doar ca părinte. Apoi, metoda tax(), care
este abstractă, nu ar trebui să aibă corp, deoarece nu poate să execute un anumit cod, dar suntem
obligați să folosim metoda tax() atunci când moștenim această clasă undeva.
Deci, creăm un șablon care va construi un produs și să spunem că vrem o clasă Shoes (Pantofi) care va
utiliza șablonul produsului. Dacă încercăm să-l realizăm în felul acesta:

import abc
1class Product([Link]):
2 def __init__(self,name,price):
3 [Link]=name
4 [Link]=price
5
6 @[Link]
7 def tax(self):
8 pass
9 def buy(self):
10 print("You bought a product",[Link],"with a
11price",[Link](),"dollars")
12
13class Shoes(Product):
14 pass
15
16p = Shoes("Nike",100)
[Link]()

prin urmare, dacă definim clasa Shoes nouă care moștenește Products, dar nu facem nimic în ea, adică
nu avem nicio logică în ea și nu folosim metoda tax(), obținem aceeași eroare ca şi înainte, anume că
folosim metoda abstractă. Metoda abstractă ne cere să scriem logica pentru calcularea impozitelor în
cadrul metodei tax(), adică să respectăm şablonul.

Dacă acum definim corpul metodei, identic cu ceea ce era anterior doar în clasa nouă Shoes:

1import abc
2class Product([Link]):
3 def __init__(self,name,price):
4 [Link]=name
5 [Link]=price
6
7 @[Link]
8 def tax(self):
9 pass
10 def buy(self):
11 print("You bought:", [Link],"with a price",[Link](),"dollars")
12
13class Shoes(Product):

Page 54 of 107
14 def tax(self):
15 return [Link] * 1.2
16
17p = Shoes("Nike Airmax",100)
[Link]()

obținem executarea și imprimarea cu succes a codului:

You bought: Nike Airmax with a price 120.0 dollars

Când respectăm şablonul necesar pentru clasa Product, acum abstractă, obținem același comportament
ca şi mai înainte, însă acum abordarea este logică: în locul clasei Product, avem acum Shoes(Product),
unde avem impozitul care se percepe pentru pantofi.

Este important să înțelegem că o clasă, deși abstractă, poate avea toate funcționalitățile unei clase
obișnuite, inclusiv metode și câmpuri. Orice clasă care moștenește o clasă abstractă și implementează
metodele ei abstracte se poate utiliza în mod normal și va avea la dispoziție toate elementele clasei pe
care a moștenit-o.

Exact acest lucru l-am văzut în exemplul anterior, unde am folosit clasa Shoes fără probleme, însă a fost
important să folosim și metoda abstractă din acea clasă. De asemenea, utilizăm în cod metoda buy din
clasa Product și obținem o imprimare, motivul fiind faptul că metoda buy nu este abstractă și atunci o
folosim ca metodă clasică.

Factorul principal este obligativitatea utilizării metodelor abstracte. În următoarea parte a lecției ne
vom axa pe acestea.

Metodele abstracte

Metoda abstractă poate fi înțeleasă ca un tip de contract între o clasă abstractă și clasă care o
moștenește. Acesta garantează că clasa care moșteneşte va respecta condițiile de participare la restul
sistemului.

De exemplu, orice benzinărie folosește același sistem de injecție a combustibilului în maşină. Când
construiește o pompă, proiectantul trebuie să respecte un standard (șablon). Obiectivul final este ca,
odată pompa introdusă în sistem, conducătorii auto să nu se mai gândească dacă furtunul va intra în
rezervor. De exemplu, să spunem că există un șablon pentru crearea staţiilor de benzină și că acesta
necesită metoda fillCar():

1import abc
2class BaseStation([Link]):
3 @[Link]
4 def fillCar(self):
5 pass

Dacă ar fi să moștenim această clasă cu o clasă goală (pentru o benzinărie nouă care respectă șablonul),

1import abc
2class BaseStation([Link]):
3 @[Link]
4 def fillCar(self):
5 pass

Page 55 of 107
6
7class ShellGasStation(BaseStation):
8 pass
9
10new_station = ShellGasStation()

în timpul instanțierii ar apărea problema:

Can't instantiate abstract class ShellGasStation with abstract methods


fillCar

Asta pentru că nu am respectat contractul (acordul) și nu am implementat metoda abstractă fillCar().


După ce implementăm metoda fillCar(), instanțierea nu mai este o problemă:

1import abc
2class BaseStation([Link]):
3 @[Link]
4 def fillCar(self):
5 pass
6
7class ShellGasStation(BaseStation):
8 def fillCar(self):
9 print("Filling car with water")
10
11new_station = ShellGasStation()
12new_station.fillCar()

Observăm că totul este în regulă, deci obținem execuția și imprimarea codului, chiar dacă mașina va fi
umplută cu apă (ceea ce, cu siguranță, nu este bine).

Dar pe această parte a sistemului (fillCar()) nu o „interesează” problema. Singurul lucru care o interesează
este contractul, să fie respectat și metoda, să fie primită. Deci, umplerea rezervorului mașinii și cu ce -
nu contează.

Cu aceasta închidem cercul și ajungem din nou la exemplul cu telecomanda, prin care am explicat
abstractizarea. Pe mine mă interesează ca atunci când apăs butonul telecomenzii, să se schimbe canalul,
modul în care se realizează asta - nu mă interesează.

Page 56 of 107
3.1. Gestionarea datei și orei

Rezumat

• Python, spre deosebire de alte limbaje de programare, nu are un tip de date care să prezinte
ora sau data, dar în cadrul acestui limbaj avem mai multe module încorporate pentru lucrul cu
timpul.
• Cele două module cel mai frecvent utilizate pentru lucrul cu timpul sunt time și datetime.
• În cadrul modulului time, una dintre funcțiile de bază este funcția sleep. Aceasta întrerupe
programul pentru o anumită perioadă de timp exprimată în secunde.
• Timpul sub forma timestamp îl putem obţine cel mai simplu apelând funcția time în cadrul clasei
time ([Link]()).
• Python ne oferă două funcții pe care le putem utiliza pentru a formata afișarea datei și a orei
mult mai ușor și mai rapid. Acestea sunt funcțiile srftime și strptime.
• Funcția strftime formatează data, conform unui șablon definit de utilizator.
• Funcția strptime generează structura time de string, conform unui şablon definit.
• Modulul Datetime, de asemenea, conține diverse funcționalități care se referă la timp. Modulul
datetime este mai mare şi mai puternic decât modulul time și îl folosim atunci când dorim, pur
și simplu, să efectuăm diferite operații pe oră.
• Cu modulul Datetime putem să comparăm timpul, să adăugăm timp sau să scădem din cel
existent. Pe lângă acestea, ni se permite să calculăm intervalul sau perioada, precum și să lucrăm
doar cu date sau numai cu ora.

În cadrul acestei lecții, vom vorbi despre gestionarea datei și orei. Aproape fiecare aplicație pe care o
vom scrie va necesita, într-un fel, folosirea timpului. Deci, de la noi se va aștepta să o colectăm într-un
fel, să o stocăm și să afișăm ora și data. În această lecție, ne vom concentra asupra complexității care se
află în spatele acestei operații aparent banale.

Lucrul cu timpul

Python, spre deosebire de alte limbaje de programare, nu are un tip de date care să prezinte ora sau
data, dar în cadrul acestui limbaj avem mai multe module încorporate pentru lucrul cu timpul. Vom
începe cu utilizarea modulului time, deci, în fișierul în care lucrăm cu timpul, trebuie să avem un link
către modulul corespunzător. În cazul nostru, acesta este:

1import time

În continuarea lecției, vom procesa câteva dintre cele mai utilizate funcții ale acestui modul. Vom începe
povestea cu funcția sleep. Funcția sleep întrerupe programul pentru un anumit timp exprimat în
secunde, dar cu posibilitatea de a introduce valori zecimale (deci, pauza poate fi sub o secundă, să zicem
0.5, 0.2).

Următorul cod imprimă mesajul Hello, iar după două secunde imprimă mesajul world:

1import time
2print("Hello...")
[Link](2)
4print("...world")

Acest mod de pauză este bun pentru întârzierea executării programului, dar când începem cu executarea
paralelă a codului, vom vedea că trebuie să fim atenți cu întârzierea timpului.

Page 57 of 107
Următoarea funcție a modulului este funcția time. Funcția time returnează numărul de secunde de la
punctul de referință. Punctul de referință în Python pentru funcția time este 1 ianuarie 1970. Pe baza
punctului de referință, adică a numărului de secunde care au trecut de la punctul de referință, este
posibil să se calculeze orice dată. În funcție de platformă, pot apărea şi fracțiuni de secundă. De exemplu:

1import time
2print([Link]())

Imprimarea la momentul scrierii lecției este: 1601382673.1238132

Prima parte, numărul 1601382673, reprezintă numărul de secunde de la 1 ianuarie 1970.

A doua parte a imprimării, .1238132, reprezintă numărul de microsecunde de la ultima secundă.

Ceea ce avem, de fapt, nevoie pentru a lucra cu timpul, este numărul actual de milisecunde care au
trecut de la 1 ianuarie 1970.

În Python, avem posibilitatea de a obţine această informație în mai multe moduri. Deoarece funcția time
are o virgulă zecimală, care întotdeauna este precedată de un număr de secunde, putem spune că după
virgulă există fracțiuni de secundă. Înmulțind acest număr cu 1000 (deoarece o secundă are 1000
milisecunde), vom obține un număr relativ de milisecunde în raport cu punctul de referință.

1import time
2miliseconds=round([Link]()*1000)
3print(miliseconds)

Următorul cod oferă un rezultat precum următorul: 1601385280805.

Veți fi de acord că rezultatele anterioare nu ne sunt deosebit de utile, deoarece ar trebui să petrecem
mult mai mult timp procesând aceste rezultate și transformându-le într-un format lizibil, atât pentru
afișare, cât și pentru stocare. Exact aici apar funcțiile care procesează aceste informații, care nouă ne
sunt ilizibile.

Funcţia gmtime/localtime

La fel ca funcția time(), și aceste funcții returnează timpul în raport cu punctul de referință, dar cu
diferența că valoarea este de tip structural (named tuple) și conține componente individuale ale datei și
orei curente.

1import time
2gmt = [Link]()
3print("Current date is: ",gmt.tm_mday,gmt.tm_mon,gmt.tm_year)

În cadrul noii variabile, după ce am salvat apelul funcției gmtime, putem folosi parametrii ei pentru a
afișa anumite valori, așa că am folosit aici imprimarea zilei (tm_mday), a lunii (tm_mon) și a anului
(tm_year). Această funcție conține şi alți parametri, inclusiv parametri de timp foarte frecvent folosiți:
tm_hour, tm_min și tm_sec.

În mod implicit, gmtime generează timp pe baza punctului de referință (apelând implicit funcția time),
dar timpul se poate genera și pe baza unei valori explicite, așa că să ne uităm şi la acest exemplu:

Page 58 of 107
1import time
2gmt = [Link](631152000)
3print("Current date is: ",gmt.tm_mday,gmt.tm_mon,gmt.tm_year)

De exemplu, următorul cod va genera un obiect de timp (gmt) pentru data de 1 ianuarie 1990, care este
anul în care a fost creat Python. Dezavantajul acestei funcții este că generează dată și oră fără utilizarea
fusurilor orare. În practică, cel mai adesea vom folosi ora și data cu un fus orar, iar prima funcție care ne
permite acest lucru este funcția localtime.

Funcţia localtime se comportă la fel ca funcția gmtime, cu excepția faptului că, în loc de universal,
afișează ora pentru zona setată pe calculator:

1import time
2while True:
3 print("Exact time is: ")
4 my_time = [Link]([Link]())
5 print(my_time.tm_hour,"hours",my_time.tm_min,"minutes")
6 [Link](60)

Deci, în cadrul obiectului timp my_time, apelăm funcția time și peste ea apelăm localtime, care obține
valoarea funcției time pentru parametru, sub forma: 1601382673.1238132 care se compilează, apoi
definim cu parametri părțile de care avem nevoie. După aceea, primim afişajul:

Exact time is:


16 hours 21 minutes

Imprimarea se va repeta la fiecare minut (60 secunde), lucru pe care l-am definit cu funcţia sleep.

Funcţiile strftime şi strptime

În exemplele anterioare, am specificat manual ce date dorim, deci separat pentru zile, ani, ore și minute.
Python ne oferă două funcții pe care le putem utiliza pentru a formata afișarea datei și orei mult mai
ușor și mai rapid. Acestea sunt funcțiile srftime și strptime:

• strftime formatează data conform unui șablon definit de utilizator;


• strptime generează structura time de string, conform unui şablon definit.

Să analizăm cazul utilizării funcției strftime:

1import time
2while True:
3 print("Exact date is: ")
4 my_time = [Link]([Link]())
5 my_time_parsed = [Link]("Month: %b Day %d Year: %Y",my_time)
6 print(my_time_parsed)
7 [Link](60)

Puteți vedea că am schimbat logica exemplului anterior. Acum, obiectul my_time stochează date despre
ora locală, în timp ce noul obiect my_time_parsed stochează timpul procesat (parsat) și realizează
această procesare cu funcția strftime care ia doi parametri. Primul parametru este de utilizator, un șablon
definit pentru afișarea lunii, a zilei și a anului (% b,% d,% Y), iar al doilea parametru este un obiect de
timp care păstrează timpul în forma sa originală.

Page 59 of 107
Referitor la parametrii de formatare care sunt marcați cu semnul %, aceştia sunt peste 20 și, desigur, nu
vor fi memoraţi, ci veți utiliza tabele care conțin liste cu acești parametri. Un exemplu de tabel se află şi
în documentația oficială a limbajului Python, la linkul:

[Link]

Acum, să ne uităm la o altă funcție, strptime. Am spus că generează o structură de timp de sting, conform
şablonului definit. Acest lucru îl vom folosi mai des; veți vedea mai târziu în lucru, și mai ales cu bazele
de date, că baza de date ne returnează timpul într-un anumit șablon (timestamp), pe care uneori trebuie
să îl procesăm pentru a fi util pentru programul nostru. Și să spunem că în program vine data despre
timp sub forma unui string definit astfel:

time = "2022-02-07 02:39:11"

Acum avem o problemă: dacă am trata acest lucru ca pe un string sau un șir, deci într-un fel procesăm
datele individuale pentru lucrări ulterioare, am experimenta o complicație cu separarea, stocarea și altele.

Dar, cu o metodă precum strptime, putem crea foarte ușor un obiect de timp pentru lucrări ulterioare.
Realizarea ar putea arăta astfel:

1import time
2
3entry_time = "2023-02-07 02:43:11"
4
5time_obj = [Link](entry_time,"%Y-%m-%d %H:%M:%S")
6print("Entry created in the year",time_obj.tm_year)

Aşa cum puteți vedea, primim o dată și o oră clar împărțite pentru o utilizare ulterioară în aplicația
noastră. Cum am realizat acest lucru? Esenţială este, desigur, funcția strptime. Această funcție ia ca prim
parametru o variabilă care stochează înregistrarea timpului, în timp ce al doilea parametru este șablonul
nostru care determină cum ar trebui preluate aceste date. Dacă pentru claritate plasăm înregistrarea sub
șablon, putem vedea cum se creează el:

2023-02-07 02:43:11
%Y-%m-%d %H:%M:%S

Deci, în locul unde anul este în înregistrare, setăm %Y în șablon. Știm că acest lucru se referă la
parametrul an; după aceea, specificăm caracterul - pentru că apare în înregistrare, și repetăm același
lucru pentru toate celelalte elemente, iar în șablon specificăm parametrul corespunzător. În felul acesta,
funcția „știe” ce parte a înregistrării reprezintă, de fapt, informații și ce părți reprezintă partea înregistrării
care nu este relevantă pentru a fi stocată (cum ar fi liniuțe și spații în înregistrare).

Desigur, scrierea acestui șablon nu este nicicum rentabilă doar pentru câteva introduceri, dar în cazul în
care lucrăm într-adevăr cu înregistrările dintr-o bază de date, atunci putem avea 10.000 sau 100.000 de
introduceri pe care acum le putem defalca, salva și utiliza cu obiectul timp în aplicația noastră.

Page 60 of 107
Modulul datetime

Datetime, similar cu modulul time, este un modul care conține diverse funcționalități care se referă la
timp.

Modulul datetime este mai mare şi mai puternic decât modulul time și îl folosim atunci când dorim, pur
și simplu, să efectuăm diferite operații pe timp, și anume:

• să comparăm timpul;
• să adăugăm timp sau să scădem din cel existent;
• să calculăm un interval sau o perioadă;
• să formatăm timpul din string sau în string;
• să lucrăm doar cu data sau doar cu timpul.

De aceste operații ne vom ocupa în continuarea lecției, folosind exact modulul datetime. Modulul
trebuie importat, de data aceasta cu:

1import datetime

Acest lucru ne permite să lucrăm cu timpul sub formă de obiecte de timp.

Clasa datetime este clasa principală a acestui modul și o putem instanția în mai multe moduri:

1import time
2import datetime
3
4date1 = [Link](2023,11,23,15,38,46)
5date2 = [Link]()
6date3 = [Link]([Link]())
7
8print(date1)
9print(date2)
10print(date3)

Putem instanția o clasă folosind un constructor, atunci când trebuie să trecem toate valorile necesare
pentru funcționarea ei (minim zi, lună și an) sau prin unele dintre metodele clasei. Pentru acest exemplu,
folosim metoda now(), care returnează ora locală curentă a calculatorului nostru în format:

2023-02-29 21:00:46.073178

În cadrul obiectului date3, folosim interacțiunea dintre două pachete pentru lucrul cu timpul, unde
apelăm funcția time(), care trimite un timestamp care se convertește într-un afişaj identic, care este oferit
de funcția now().

Definirea valorilor de către utilizator

Obiectul datetime poate fi obținut și prin formatarea stringului, așa cum a fost cazul cu modulul anterior,
time. La formatare, se folosesc metode care au același nume ca funcţiile modulului time (strptime și
strftime):

Page 61 of 107
1import time
2import datetime
3
4entry_time = "2023-02-07 02:43:11"
5
6time_obj = [Link](entry_time,"%Y-%m-%d %H:%M:%S")
7
8print("Entry created in the year",time_obj.year)

Deci, vedeți că principiul este identic - diferența este doar în clasele pe care le instanțiem și în modul în
care accesăm datele obiectului timp. În loc de tm_year, acum este suficient să se specifice year; același
lucru se aplică și pentru alți parametri.

Gestionarea perioadei (timedelta)

În afară de formatare, cu ocazia gestionării timpului, este deosebit de important să știți cum să
determinați perioada dintre două unități de timp. Acest lucru vine în prim-plan atunci când trebuie să
măsurăm cât timp a trecut între două introduceri, când dorim să arătăm cât timp a fost setat, modificat
etc.
Aceste operații sunt reprezentate în cadrul modulului datetime de o clasă specială, numită timedelta.

Clasa timedelta poate fi obținută în două moduri:

1. implicit, prin efectuarea unei operațiuni peste două date;


2. explicit, prin instanțierea directă a clasei.

Să ne uităm la prima modalitate:

import datetime
1
2first_entry_date = "2023-02-07 02:43:11"
3last_entry_date = "2023-08-07 03:46:01"
4
5first_entry_obj =
[Link](first_entry_date,"%Y-%m-%d %H:%M:%S")
7
8last_entry_obj = [Link](last_entry_date,"%Y-%m-%d %H:%M:%S")
9
10activity = last_entry_obj - first_entry_obj
11print("User was active for: ",[Link],"days")

Am luat exemplele anterioare și am făcut câteva modificări. Acum, acest mic program poate reprezenta
funcționalitatea de monitorizare a timpului total de activitate al unui utilizator. Avem două date, când
utilizatorul a introdus pentru prima dată ceva și când a făcut ultima modificare. Luăm ambele înregistrări,
le procesăm pentru a obține date individuale, aceste obiecte se scad apoi unele din altele și obținem
diferența de timp.

Aici se activează clasa timedelta a modulului timedate, pentru că altfel nu ar fi posibil să scădem pur și
simplu timpul din timp, deoarece, aşa cum știm, sunt necesare conversii ale zilelor în ore, a orelor în
minute etc., pentru a obține un rezultat corect.

Page 62 of 107
Desigur, așa cum am scăzut, adică am făcut diferența de timp, tot aşa putem adăuga, împărți și înmulți
timpul. Clasa timedelta va face de fiecare dată conversia pentru noi.

Timedelta ne permite să comparăm timpul, să adăugăm timp sau să scădem dintr-unul existent, să
calculăm un interval sau o perioadă folosind operatori. Folosind metodele afișate anterior, formatăm
timpul din string sau în string.

Exerciţiul 1:

În cadrul acestui exercițiu, este necesar să implementați o aplicație simplă reminder, deci un memento digital.

Utilizatorul introduce numărul de minute pentru timer/cronometru și mesaj. După introducere,


începe numărătoarea inversă; când numărătoarea inversă este completă, mesajul este afișat de cinci ori.

Notă: Pentru a realiza introducerea timpului, utilizați strptime() pentru a procesa introducerea în sine, în cadrul
clasei datetime.

Puteți vedea funcția programului în următoarea imagine:

Imaginea 9.1. Afișarea introducerii realizate și a rezultatului funcţionării programului

Page 63 of 107
3.2. Gestionarea erorilor și a excepțiilor

Rezumat

• În timpul executării codului de program al oricărui limbaj, este posibil să apară erori.
• Definim o excepție ca reprezentarea unei erori care a apărut în cod.
• Erorile din aplicații se pot împărți în două tipuri: de sintaxe și logice.
• Erorile se pot împărți şi în funcție de momentul evenimentului în: erori de compilare și erori în
execuție.
• Excepția în Python este Error.
• Procesarea excepțiilor implică o gestionare adecvată a obiectului Error și prevenirea
întreruperilor executării codului utilizând comandă specială ..except.
• Pe lângă blocurile try și except, comanda try…except poate conține încă un bloc suplimentar,
finally, care este utilizat pentru a se defini codul care se va executa obligatoriu.
• Autoaruncarea unei excepții se poate realiza prin utilizarea unei comenzi.
• Limbajul de programare Python ne oferă şi posibilitatea de a crea propria excepție. Pentru a
crea o excepție, aceasta trebuie să aibă un nume și să moștenească clasa Exception. Atunci când
creăm această clasă, putem scrie regulile excepției de care avem nevoie.

În activitatea dvs. de până acum, v-ați întâlnit cu diferite moduri de a controla executarea codului. În
această lecție, va fi descrisă o altă abordare pentru realizarea executării condiționate a codului Python.
Este vorba de abordarea care este utilizată pentru gestionarea excepțiilor care apar datorită erorilor în
executarea codului Python. Prin urmare, în această lecție vor fi prezentate câteva concepte noi care sunt
foarte importante pentru programarea în Python. La început, este necesar să vă familiarizați cu
conceptul de erori care pot apărea la executarea codului programului.

Erori de software

În timpul executării codului de program al oricărui limbaj, este posibil să apară erori. Erorile sunt o parte
integrantă a fiecărui program și, în ciuda marelui efort depus în detectarea și eliminarea lor, este foarte
dificil să se facă o aplicație mai serioasă fără un cod care ar putea provoca o eroare la un moment dat.

Erorile în executarea codului pot fi cauzate de numeroşi factori. Unele dintre ele pot fi atribuite unui cod
scris slab, în timp ce altele își au sursa în mediul extern. Factorii externi care pot provoca erori la
executarea codului pot fi lipsa memoriei interne sau externe, pierderea conexiunii la rețea, funcționarea
defectuoasă a unui dispozitiv etc. Desigur, noi ne vom concentra pe bug-urile din programe.

Erorile din aplicație pot fi împărțite în două tipuri:

• de sintaxă (eroare în sintaxă, ușor de corectat);


• logice (eroare în logică, se corectează mai greu).

Erorile pot fi, de asemenea, împărțite şi după momentul evenimentului:

• erori în compilare;
• erori în execuție (erori runtime).

Practic, aceste două diviziuni se suprapun complet, deoarece eroarea de sintaxă provoacă o problemă
în execuție și, de asemenea, o eroare logică poate provoca o eroare în compilare. O eroare în momentul
evenimentului este ceva care va fi în centrul acestei lecții, deoarece putem crea mecanisme fie pentru a

Page 64 of 107
preveni eroarea, fie pentru a-i indica clar utilizatorului (sau programatorului, sau clientului) de ce a
apărut eroarea.

Erori de sintaxă

Presupunem că ați avut deja experiență în timpul lucrului cu erorile de sintaxă. Erorile de sintaxă apar
atunci când nu respectăm regulile de scriere a codului în limbaj, de exemplu, atunci când am avut un
spațiu în plus, o indentare incorectă, declarație, lipsa parametrilor etc. Aceste erori sunt cel mai ușor de
corectat, deoarece în timpul procesării, limbajul ne oferă informații cu privire la locul în care se află exact
sau aproximativ eroarea.

De exemplu, următoarea linie de cod:

1print("I wiil cause an error!"))

provoacă o eroare de sintaxă; la prima vedere, acest lucru nu poate fi evident, dar Python, cu ocazia
executării, ne oferă următoarele informații:

Imaginea 10.1. Afișarea erorii de sintaxă în terminalul Python

Primim feedback că pe linia 1 a fișierului nostru există o eroare. Și avem un marker care sugerează unde
este problema și, după aceea, mesajul că este vorba de o eroare de sintaxă și că una dintre paranteze
nu are perechea. Dacă ne uităm din nou la cod, putem vedea că există o paranteză în plus.

Recunoaștem întotdeauna erorile de sintaxă după numele lor, care este în general SyntaxError.

Erori logice

Erorile logice sunt cel mai dificil de observat. Aceste erori se manifestă atunci când programul rulează
fără probleme raportate, dar returnează un rezultat greşit. Aceste erori apar din greşeala
programatorului, chiar în logica programului. Principala problemă este că nu primim mesaje despre
aceasta, deci acestea nu sunt nici de sintaxă, nici erori care apar în timpul execuției.

Singura modalitate de a elimina eroarea de acest tip este examinarea întregului cod care este asociat
cu rezultatul incorect.

Exemple de erori logice frecvente sunt:

• apelul unei variabile greșite (să zicem, în loc de var1, ne trebuie var2);
• indentare la un nivel greșit (diferența dintre o funcție și o metodă este doar în nivelul de
indentare);
• împărțirea cu integer, în loc de valoarea float (rezultat insuficient de precis);
• eroare în expresiile boolean etc.

Page 65 of 107
Cea mai comună sursă de erori logice este copierea (repetarea) blocurilor mai mari de cod. Atunci, se
poate întâmpla să avem linii duplicate care au diferențe foarte mici. În aceste cazuri, este foarte ușor de
anticipat modificările necesare.

Erori în executare

Erorile în executare apar cel mai adesea datorită lipsei de resurse, a indexării incorecte, conversiei etc.

Noi, ca şi programatori, trebuie să anticipăm și să prevenim aceste erori. Python conține un mecanism
care permite ca aplicația să anunțe într-un mod „liniștit” că ceva nu este în regulă, oferindu-i
programatorului posibilitatea de a reacționa în acel moment. Acest mecanism este un sistem de excepții
(exceptions).

Excepția este situația în care o parte a programului nu poate funcționa conform planului prevăzut, dar
spre deosebire de eroare, îi permite programului să efectueze o alternativă care va rezolva problema și
va preveni întreruperea funcţionării programului.

Cu alte cuvinte, excepția este reprezentarea erorii care s-a produs în cod. Aceasta înseamnă, practic, că
o greșeală este o cauză, iar excepția este consecința. Astfel, mesajul cu descrierea erorii, obținut în
exemplul anterior, a fost, de fapt, excepția emisă de mediul executabil Python.

În limbajul Python, excepțiile sunt obiecte de tipul Exception, în cadrul cărora sunt combinate două
informații foarte importante despre eroarea apărută.

• name
• message

Python cunoaște diferite tipuri specializate de obiecte Exception care oferă informații mai detaliate
despre diferite tipuri de erori. Astfel, în exemplul următor putem vedea o excepție reprezentată de un
obiect de tip ZeroDivisionError.

Dacă am încerca să rulăm următoarea linie de cod:

1x=100/0

ieşirea ar fi:

ZeroDivisionError: division by zero

Deci, ar fi exact în formatul name: message, unde, desigur, name este excepția care a fost activată, în
cazul nostru, ZeroDivisionError, în timp ce mesajul este division by zero. Prin urmare, mesajul în sine
ne spune ce fel de eroare a apărut.
Excepțiile sunt parte integrantă din sistemul de manipulare a erorilor și scopul lor principal este de a
oferi informații despre eroarea care a apărut în cod, pe care tocmai am văzut-o. Totuşi, pe lângă
informații, excepțiile au încă o proprietate foarte importantă, adică scop. Aceasta este să fie
procesate, iar o astfel procedură se numește procesarea excepțiilor.

În exemplul anterior, Python a recunoscut neregularitatea în valorile unei variabile și, pe baza acestei
nereguli, a generat obiectul Exception și apoi i l-a livrat aplicației. Obiectul Exception plutește apoi la
suprafața aplicației, iar dacă nimeni nu se ocupă de el (să îl proceseze), va pluti la suprafață și aplicația
va înceta forțat să funcționeze.

Page 66 of 107
În linkul de mai jos puteți găsi toate tipurile de excepții din Python. Cu unele dintre ele s-ar putea să nu vă
întâlniți niciodată, dar în cazul problemelor în cod pe care nu le puteți rezolva, acesta poate fi punctul de
plecare pentru rezolvarea problemei:

[Link]

Pentru procesarea excepţiei folosim una din cele două tehnologii:

• trimiterea excepției mai departe - în majoritatea cazurilor, aceasta nu este o practică bună, în
care ne dăm seama că există un anumit tip de eroare, dar o acoperim doar pentru ca programul
să continue să funcționeze;
• catch – „captarea” excepției - reprezintă preluarea erorii și scrierea logicii pentru a se corecta
eroarea sau să se furnizeze adecvat o notificare pe aplicație.

Captarea excepţiei

Cea mai frecventă situație (tehnologie) de procesare a excepției este „captarea” ei. Aşa cum am
menționat mai devreme, reprezintă preluarea erorii și scrierea logicii pentru a se corecta eroarea sau
pentru a se furniza adecvat o notificare pe aplicație.

Pentru captarea excepției sunt utilizate blocurile try/except. Aceste blocuri funcționează după un
principiu simplu: creăm o structură cu minimum două blocuri de cod, unul try, unul except:

1try:
2
3# code containing error
4
5except:
6
7# code to fix the error, or just show a custom error message

Partea de cod în care se așteaptă excepția este plasată în blocul try, iar partea de cod care ar trebui
executată în cazul în care eșuează executarea primului bloc, se plasează în blocul except.

Întreaga structură din exemplul anterior, sintactic arată aşa:

1try:
2 x=100/0
3except:
4 print("Hey, you can't divide by zero!")

Dacă rulați acest cod, veți vedea că, în loc de mesajul: ZeroDivisionError: division by zero, acum avem
mesajul nostru personalizat: Hey, you can't divide by zero! De asemenea, acum, executarea codului
complet al programului nu este întreruptă, ci doar o parte a codului pe care l-am plasat sub blocul try.

Dar, aici ajungem la o situație specifică. Deși am împiedicat întreruperea funcţionării programului,
eroarea există în continuare, iar în momentul în care apare o încercare de împărțire cu zero, ramura
completă va fi întreruptă. Să vedem următorul exemplu:

1try:
2 x=100/0
3 y=10

Page 67 of 107
4except:
5 print("Hey, you can't divide by zero!")
6
7print(y)

Am adăugat acum în cod o definiție a variabilei y, care are valoarea 10. Dacă încercăm să o imprimăm
după blocul try/except, obținem următorul rezultat:

NameError: name 'y' is not defined

Această eroare ne spune că variabila y nu există. Motivul este că variabila nu a fost niciodată declarată.
Cum am făcut eroarea în divizarea cu zero, Python a ieșit din blocul try și a continuat executarea
blocului except, deci a omis astfel variabila y. Aceasta înseamnă că nu putem folosi aceste blocuri, de
exemplu, copiem întreaga clasă în ele și aşa urmărim erorile, deoarece asta ar însemna că, din cauza
unei linii greșite, omitem toată clasa.
S-ar putea să vă întrebați acum - când vom folosi try/except? Folosim această tehnologie atunci când
avem modalitatea corectă de a rezolva problemele; când ne așteptăm ca o anumită parte din codul
nostru să creeze o problemă. De asemenea, această tehnologie este foarte utilă atunci când depanăm
aplicația noastră, respectiv căutăm erori prin aplicație. Atunci putem testa (izola) partea de cod pe care
o suspectăm și o putem verifica folosind această tehnologie.

În cadrul blocului except, putem specifica și parametrul; acest parametru este exclusiv obiectul clasei
excepție pe care îl așteptăm. Prin sintaxa:

1except object:
Dacă în blocul except dorim să includem toate erorile care ne pot fi afișate, atunci folosim:

1except Exception:

Ca parametru, specificăm obiectul Exception, care reprezintă orice excepție (toate) care apare în timpul
execuției.

Desigur, dacă vrem să urmărim doar o anumită excepție, atunci trebuie să fim atenți când îi specificăm
tipul, deoarece dacă greșim obiectul, eroarea nu va fi captată. De exemplu, următorul cod nu va detecta
eroarea și ea se va afişa:

1try:
2 x=100/0
3except NameError:
4 print("Hey, you can't divide by zero!")

Vom primi mesajul: ZeroDivisionError: division by zero. Motivul este că am folosit obiectul NameError, al
cărui scop este de a urmări variabilele declarate greşit, în timp ce nouă ne trebuie să urmărim dacă se
împarte la zero. Deci, dacă am folosi logica acestui exemplu, ar trebui să folosim
obiectul ZeroDivisionError, deoarece apare atunci când se împarte la zero; prin urmare, codul corectat
ar fi:

1try:
2 x=100/0
3except ZeroDivisionError:
4 print("Hey, you can't divide by zero!")

Page 68 of 107
Dacă suspectăm mai multe tipuri posibile de erori, ceea ce poate fi adesea cazul, în cadrul Python putem
folosi mai multe blocuri except. Atunci, exemplul nostru ar putea arăta aşa:

1try:
2 x=100/0
3except NameError:
4 print("You won't see me!")
5except ZeroDivisionError:
6 print("Hey, you can't divide by zero!")
7except Exception:
8 print("I'm here just in case you didn't find anything")

Aşa cum puteți vedea, putem verifica dacă există mai multe erori pe care le suspectăm. Cu primul bloc
except verificăm dacă variabila este declarată incorect; aceasta nu este o greșeală care este necesară.
Apoi, avem o verificare a diviziunii și acest bloc se va activa. Dar, în cazul în care nu este o eroare de
împărțire, avem blocul except, care este general, deoarece folosim Exception, cea mai înaltă clasă din
ierarhie, care include toate erorile, cu ideea că încă izolăm problema, chiar dacă încă nu știm care este.

Acesta este un exemplu de practică bună și blocurile multiple except se formează, de obicei, astfel încât
excepțiile sunt captate de la cele speciale, la cele generale. Este o practică bună la sfârșitul tuturor
cazurilor speciale să prindeți, în final, şi Exception, ce va prinde toate excepțiile care ar fi putut trece de
filtrare.

Și pentru sfârșitul poveștii despre blocurile try/except, există încă un bloc care ar trebui menționat, și
anume blocul opțional finally. Blocul finally se execută în orice caz, aproape la fel ca atunci când am
scrie codul în afara blocului complet try except (dar nu identic).

1try:
2 x=100
3 y=0
4 print(x/y)
5except NameError:
6 print("You won't see me!")
7except ZeroDivisionError:
8 print("Hey, you can't divide by zero!")
9except Exception:
10 print("I'm here just in case you didn't find anything")
11finally:
12 print("Value of y is: ",y)
Am făcut câteva modificări la exemplul anterior și acum puteți vedea realizarea blocului finally. Acest
bloc se va executa chiar și după apariția Exception și aici am folosit acest bloc doar ca exemplu, pentru
a arăta valoarea problematică. Spre deosebire de blocul except, blocul finally nu are parametri.

Desigur, când lucrați mai târziu cu conexiuni cu baze de date, cu data streams, fișiere și altele, veți utiliza
finally block pentru a închide canalele de date. Deci, chiar dacă apare eroarea și programul nostru nu
mai funcționează, este foarte posibil ca cineva de cealaltă parte să aștepte datele, chiar dacă programul
nostru s-a oprit. Exact cu acest bloc trimitem mesajul final în care, de exemplu, închidem această
conexiune.

Page 69 of 107
Aruncarea manuală a excepţiilor

Excepţia o putem arunca şi manual, în orice moment din timpul executării programului. Acest lucru este
foarte util atunci când vrem să definim că, în cazul îndeplinirii unor condiții, întrerupem programul și
returnăm eroarea.

Pentru a defini manual excepția și pentru a o afișa, folosim comanda raise. Această comandă trebuie să
fie însoțită de un obiect Exception valid. Deci:

raise Exception("My error!")

În exemplu este creată o funcție pentru împărțirea numerelor până la zece.


1def divide(a,b):
2 if b == 0:
3 return 0
4
5 else:
6 return a/b
7
8print(divide(14,2))

Această funcție nu are nicio problemă cu zero ca divizor - atunci, returnează simplu numărul 0 ca rezultat.
Luăm un exemplu în care dorim ca excepţia să se producă dacă numerele sunt mai mari decât 10.

Python, ca limbaj, nu are nicio problemă cu împărțirea numerelor mai mari decât zece, nici nu va elimina
nicio excepție dacă introducem un număr mai mare decât 10 în funcția noastră. Această situație trebuie
să o procesăm manual. Putem realiza acest lucru în felul următor:

1def divide(a,b):
2 if b == 0:
3 return 0
4
5 if a>10 or b>10:
6 raise ArithmeticError("Number is larger than 10")
7
8 else:
9 return a/b
10
11print(divide(14,2))

Acum, dacă unul dintre parametri are o valoare mai mare decât 10, vom primi o afișare a excepției cu
mesajul nostru personalizat. Aceasta este o abordare foarte puternică, deoarece ne putem concentra pe
probleme care nu fac parte din limbajul în sine, ci reprezintă o logică a programului nostru.

Crearea unei excepții definite de utilizator

Limbajul de programare Python ne oferă şi posibilitatea de a crea propria noastră excepție. Pentru a
crea o excepție, aceasta trebuie să aibă un nume și să moștenească clasa Exception. De exemplu:

1class MyException(Exception):
2 pass

Page 70 of 107
Acum, avem o clasă care a moștenit toată logica excepțiilor. Prin urmare, în cadrul clasei noastre, putem
scrie acum regulile care ne sunt necesare și ulterior, le putem afișa instanțând clasa și apelând metodele
dorite.

Să ne uităm la un exemplu de utilizare reală. Avem următorul program simplu:

1class Calculator:
2 @staticmethod
3 def calculate(a, b, op):
4 if op == "+":
5 return a + b
6 if op == "-":
7 return a - b
8 if op == "/":
9 return a / b
10 if op == "*":
11 return a * b
12 return 0
13
14x = [Link](5,0,"/")
15print(x)

Pentru două numere introduse și simbolul operației, returnăm rezultatul. Dar, chiar și un astfel de
program simplu poate avea multe probleme. Mai întâi, am setat ca să i se treacă metodei zero ca şi
parametru; deoarece este o operație de divizare, obținem exception. Dar mai există și alți factori care
vor împiedica funcționarea programului. În primul rând, vom primi o eroare dacă la introducere apare
un string, deci dacă cineva introduce accidental, pe lângă număr, şi o literă. De asemenea, eroarea apare
şi dacă pentru parametrul de operație, introducem orice caracter care nu este procesat în cazurile
noastre, adică dacă caracterul nu este exact unul dintre cele patru posibile (+ - / *). Să vedem cum putem
gestiona corect aceste erori folosind tocmai excepțiile definite de utilizator:

1class CalculatorNotNumberException(Exception): pass


2class CalculatorZeroDivisorException(Exception): pass
3class CalculatorInvalidOperationException(Exception): pass<br><br>
Mai întâi, vom defini trei clase pentru cele trei cazuri problematice ale noastre. La început, toate clasele
vor fi goale. După aceea, vom configura programul nostru:

1class Calculator:
2 @staticmethod
3 def calculate(a, b, op):
4 if not isinstance(a, int) or not isinstance(b,int):
5 raise CalculatorNotNumberException()
6 if b == 0:
7 raise CalculatorZeroDivisorException()
8 if not op in ["+","-","*","/"]:
9 raise CalculatorInvalidOperationException()
10 if op == "+":
11 return a + b
12 if op == "-":
13 return a - b
14 if op == "/":

Page 71 of 107
15 return a / b
16 if op == "*":
17 return a * b
18 return 0

Acum, am făcut modificarea verificând mai întâi dacă parametrii a și b sunt de tip int; dacă nu sunt,
activăm exception.

Următorul caz este procesarea codului în cazul în care b = 0. Atunci, activăm exception care se va ocupa
de împărțirea la zero.

Şi, în final, verificăm dacă valoarea variabilei op se află în setul pe care l-am definit. Dacă nu se potrivește
cu una dintre valori, activăm exception pentru acest caz. Acum, trebuie doar să verificăm metoda:

1try:
2 x = [Link](5,2,"=")
3 print(x)
4except CalculatorNotNumberException:
5 print("Operand is not a number")
6except CalculatorInvalidOperationException:
7 print("Operator is not valid")
8except CalculatorZeroDivisorException:
9 print("You cannot divide with zero")
10except Exception:
11 print("Who knows what is the problem")

Folosim blocul try/except deja cunoscut. În blocul try, apelăm metoda și îi trecem parametrii. În felul
acesta, îi verificăm conținutul și fluxul complet.

După aceea, verificăm toate condițiile cu mai multe blocuri except și setăm mesajele corespunzătoare.
Aşa cum puteți vedea, și aici am folosit la sfârşit întreaga clasă Exception, pentru a face o excepție în
cazul în care apare o altă eroare pe care nu am anticipat-o.

Exerciţiul 1:

Este necesar să creați un program care să adune numere, cerându-i constant utilizatorului să introducă primul,
apoi al doilea operand. Dacă introducerea utilizatorului este incorectă, programul afișează mesajul că variabilele
sunt incorecte, apoi îi oferă utilizatorului ocazia de a introduce din nou.

După eroare, programul ar trebui să înceapă executarea din nou, adică reintroducerea numerelor.

Page 72 of 107
4.1. Reflecţie

Rezumat

• Reflecția, în general, reprezintă capacitatea unui program de a se analiza pe sine.


• Reflecția ca sistem ne permite să accesăm clase și obiecte și modificarea lor în timpul executării
programului.
• Realizăm reflecția cu funcții încorporate pentru analiza obiectelor în Python.
• Cele mai importante funcții încorporate pentru analiza obiectelor sunt: dir, id, type, isinstance,
issubclass și callable.
• Funcția dir() este una dintre cele mai puternice funcții care sunt încorporate în Python. Această
funcție returnează lista tuturor atributelor și metodelor unui obiect.
• Funcția isinstance() ne permite să verificăm dacă o valoare este de un anumit tip. Primul
parametru este variabila care se verifică, în timp ce al doilea parametru este tipul pe care îl
verificăm.
• Cu funcția issubclass verificăm dacă o clasă este o subclasă a unei clase.
• Aplicarea funcției callable este simplă; cu ea verificăm dacă obiectul poate fi apelat. Acesta ia
un parametru, care este exact obiectul care se verifică.
• Reflecția permite instanțierea clasei și pornirea metodelor ei pe baza stringurilor. Acest lucru
permite să ne fie complet necunoscută clasa, atât timp cât știm parametrii de care avem nevoie
pentru a o instanția și, eventual, pentru a-i apela metodele.

În aceasta lecție, vom vorbi despre prima dintr-o serie de principii avansate din cadrul programării
orientate pe obiecte. Reflecția, în general vorbind, este capacitatea programului de a se autoanaliza.

Reflecția, ca sistem, ne permite să accesăm clase și obiecte și modificările lor în timpul executării
programului. În felul acesta, putem instanția dinamic clasele și putem lansa metodele lor. Până acum
am făcut aceste acțiuni exclusiv static.

În practică, reflecția este considerată un sistem lent în toate mediile în care există. Reflecția nu este
deosebit de sigură, deoarece intervine în structura clasei și majoritatea aplicațiilor noastre nu vor avea
nevoie de reflecție. Reflecția poate fi utilă atunci când aplicația noastră folosește clase despre care știm
puțin. Atunci putem scrie logica ca programul nostru să afle ce câmpuri există în clasă și ce metode de
clasă putem folosi.

Reflecția în programele noastre se realizează mai întâi prin funcții încorporate pentru analiza obiectelor
în Python. Cele mai importante funcții încorporate pentru analiza obiectelor sunt: dir, id, type, isinstance,
issubclass și callable. Pe unele dintre ele le-ați întâlnit deja, chiar dacă în afara principiului orientat pe
obiecte, dar în cadrul acestei lecții vom repeta și vom învăța modalităţi noi în care le putem folosi.

Funcţia id

Prin utilizarea acestei funcții pe obiect, obținem identificatorul lui unic în timpul programului.

1class A:
2 x = 10
3 y = 15
4
5a = A()
6b = A()
7
8print(id(a))

Page 73 of 107
9print(id(b))
10
11print(id(a.x))
12print(id(b.x))

Rezultatul executării acestui cod este următorul:

23355720
23355768
2025302080
2025302080

Primele două cifre se referă la obiecte, în timp ce celelalte două cifre se referă la câmpul x din clasa A.
Aşa cum puteți vedea, funcția id ia un singur parametru și returnează identitatea obiectului printr-un ID
unic. Prin utilizarea funcției id peste obiectele din clasa A, obținem două ID-uri diferite. Motivul este că
în memorie, două obiecte din aceeași clasă sunt diferite; pentru noi, ca utilizatori, nu sunt, dar pentru
calculator sunt. După aceea, obținem două ID-uri la fel; acesta este comportamentul dorit, deoarece
indiferent prin ce obiect accesăm câmpul, acel câmp rămâne în continuare același. Deci, câmpul se află
în aceeași clasă și păstrează aceeași valoare.

Ceea ce este important pentru această funcție este că toate valorile ID se păstrează unice și durează atât
timp cât durează şi viața obiectului.

Acești identificatori se bazează pe o adresă de memorie, deci nu sunt potriviţi pentru utilizarea
persistentă. Motivul pentru aceasta este că un calculator le poate înscrie cu ocazia unui al doilea start
sau un start pe un alt calculator la o adresă complet diferită. Dacă dezvoltăm logica care se bazează pe
acești identificatori, aceasta poate deveni inexactă, deoarece adresele s-au schimbat.

Putem folosi funcția id când vrem să verificăm dacă există o suprapunere în adrese ce a cauzat scrierea
greșită a logicii sau o eroare la îmbinarea mai multor documente. ID-ul bazat pe adresa de memorie
este utilizat de Python pentru utilizarea lui internă, în special cu operatorul is.

Funcţia dir

Funcția dir() este una dintre cele mai puternice funcții care sunt încorporate în Python. Această funcție
returnează o listă cu toate atributele și metodele unui obiect, indiferent dacă acel obiect este o funcție,
un modul întreg, un string, o listă etc.

1class Example:
2 first_field = 13
3
4new_object = Example()
5print(dir(new_object))

Aşa cum puteți vedea, funcției dir îi trecem ca parametru un obiect din clasa Exemple. Ca rezultat,
obținem următoarele:

['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__',


'__format__', '__ge__', '__getattribute__', '__gt__', '__hash__',
'__init__', '__init_subclass__', '__le__', '__lt__', '__module__',
'__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
'__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__',
'first_field']

Page 74 of 107
Ca rezultat, obținem o listă întreagă cu informații aparent aleatorii. Pe listă putem vedea toate metodele
pe care le are un obiect clasă, fie încorporate, fie definite de utilizator. Putem vedea toate câmpurile,
inclusiv câmpurile de la sfârșitul tipăririi, precum și câmpul nostru definit de utilizator, first_field. Desigur,
la fel ca înainte, această funcție ia ca parametru doar datele care sunt după tipul de obiect.

Aceasta este, de asemenea, una dintre funcțiile care ne sunt importante pentru reflecţie, deoarece, cu
ajutorul lor, obținem o perspectivă asupra a ceea ce ne este disponibil prin obiect, chiar și atunci când
nu știm ce clasă este, ci avem doar obiectul ei. Adesea se foloseşte şi în scopul de debugging al
programelor și chiar în echipe mari de programatori. Datorită capacității ei de a enumera toți membrii
unei clase, este utilă şi atunci când lucrăm cu un număr mare de clase, moștenirea pe mai multe niveluri
etc. În felul acesta, putem revizui structura, putem verifica dacă există conflicte în nume, metode și altele.

De asemenea, atunci când folosim un modul nou cu care nu am avut experiență, cu funcția dir putem
verifica exact ce se află în el; în felul acesta, îi putem testa mai ușor opțiunile și, în același timp, putem
învăța mai repede. Un exemplu este listarea pachetelor pentru lucrul cu interfața grafică din Python:

Imaginea 11.1. Listarea metodei pachetului Tkinter

Aşa cum puteți vedea în imaginea de mai sus, este suficient să creăm o clasă care utilizează pachetul și
să creăm un obiect din această clasă, iar cu funcția dir, vom obține imediat o perspectivă asupra tuturor
posibilităților pachetului.

O altă funcție utilizată în scopuri de debugging este funcția type(). Cu această funcție, obținem tipul de
obiect transmis ca parametru.

Să vedem următorul exemplu:

1class Example:
2 pass

Page 75 of 107
3
4new_object = Example()
5
6t1 = type(Example)
7t2 = type(new_object)
8
9print(t1)
10print(t2)

Am definit o clasă goală și am instanţiat-o într-un obiect numit new_object. După aceea, stocăm apelul
funcției type în două variabile, mai întâi, peste clasă și apoi peste obiectul acelei clase. Ca rezultat,
obținem:

<class 'type'>
<class '__main__.Example'>

Cu primul rezultat, obţinem informațiile că am verificat tipul care este clasa, iar cu al doilea obținem
informația că tipul este metoda main a clasei Exemple. Aceste informații ne spun că acesta este un obiect
al clasei Exemple, deoarece lansează logica clasei.

Funcția type poate avea trei parametri; dacă trecem toți cei trei parametri, funcția generează un tip nou.

Să ne uităm la următoarea linie:

1calc = type('X',(),{"a":0,"b":0,"add":lambda self,a,b:a+b})<br><br>


În cadrul acestei variabile avem un cod complet ilizibil, dar în cadrul liniei apare definiția clasei X, care
are câmpul a cu valoarea 0 și câmpul b cu valoarea 0. Urmează definiția metodei add, care ia parametrii
a și b și le returnează suma. Dacă adăugăm următorul cod:

1c = calc()
2print([Link](4,5))
3print(type(c))

puteți vedea că acest lucru a fost realizat cu succes și că obiectul c duce într-adevăr la clasa X. Și toate
acestea le-am realizat tocmai cu funcția type. Acum, să divizăm acest cod și să vedem ce s-a întâmplat.

În primul rând, cei trei parametri ai acestei funcții sunt:

1. nume;
2. lista claselor moștenite de clasa noastră; dacă nu există moștenire, rămân paranteze goale;
3. vocabularul care creează corpul clasei; în acesta enumerăm toate câmpurile și metodele.

Dacă ne uităm din nou la linie:

1type('X',(),{"a":0,"b":0,"add":lambda self,a,b:a+b})

Primul parametru este: X

Al doilea parametru este: ()

Al treilea parametru este: {"a":0,"b":0,"add":lambda self,a,b:a+b})

Page 76 of 107
Cu această linie şi funcţia type am înlocuit următoarele linii:

1class X:
2 a = 0
3 b = 0
4 def add(self,a,b):
5 self.a=a
6 self.b=b
7 return self.a+self.b

Ce este cel mai important, am creat clasa dinamic, în timpul executării codului. Și anume, clasa X nu a
existat până când nu a fost pornită funcţia type. În felul acesta, realizăm deja o programare dinamică.

Desigur, acum ar trebui deja menționate următoarele: nu trebuie exagerat cu programarea dinamică.
Mai întâi, aşa cum aţi putut vedea, aceasta afectează lizibilitatea codului. Dacă folosim introducerea
datelor de utilizator în cadrul părților create dinamic, vom primi probleme suplimentare dacă utilizatorul
introduce informații incorecte; poate fi compromisă şi securitatea aplicației, deoarece acestea sunt exact
punctele ideale pentru o intruziune în securitate.

Funcţia is

Cei mai importanți doi reprezentanți ai funcției is sunt: isinstance şi issubclass.


Funcția isinstance ne permite să verificăm dacă o valoare este de un anumit tip. Primul parametru este
variabila care se verifică, în timp ce al doilea parametru este tipul pe care îl verificăm. Cu această funcţie
v-ați întâlnit deja, dar să vedem încă un exemplu:

1x = "13"
2
3if isinstance(x,int):
4 print(x*x)
5else:
6 print("Not a number")

În cadrul structurii if, dorim să verificăm dacă variabila x este de tip int; dacă este, o înmulțim cu ea însăşi.
Dacă nu este de tip int, imprimăm Not a number. Cum numărul 13 este trecut ca sting, obținem
imprimarea mesajului.

Cu funcţia issubclass, verificăm dacă o clasă este o subclasă a unei clase. De exemplu:

1class Top:
2 pass
3
4class Example(Top):
5 pass
6
7print(issubclass(Top,Example)) #false
8print(issubclass(Example,Top)) #true

Primul parametru al funcției este clasa copil, în timp ce al doilea este părintele lui. Funcția verifică apoi
relațiile dintre clase și returnează rezultatul ca valoare boolean.

Page 77 of 107
În exemplul nostru, clasa Example moștenește clasa Top și doar în cazul
issubclass(Example,Top) obținem rezultatul True.

Funcţia callable

Aplicarea funcției callable este simplă; cu ea verificăm dacă obiectul poate fi apelat. Ia un parametru,
care este exact obiectul care se verifică. Verificarea returnează informații cu privire la faptul că obiectul
este o funcție sau o metodă True și, dacă valoarea nu poate fi apelată, returnează False:

1def x():
2 a = 5
3
4num1 = 5
5num2 = 2
6
7z = num1+num2
8
9print(callable(x))
10print(callable(z))

Dacă apelăm funcția callable peste variabila x, obținem True, deoarece am verificat obiectul x, care
reprezintă funcția, în timp ce la verificarea variabilei z obținem False.

Cu funcția callable, am trecut printr-un set de funcții încorporate care ne oferă informații mai detaliate
despre obiectele în sine din codul nostru. Înainte de a analiza câteva exemple de programare dinamică
și reflecţie a codului folosind funcțiile prezentate, este important să menționăm că în Python avem
capacitatea de a adăuga metode și câmpuri în timpul executării codului.

Adăugarea dinamică a unei metode sau câmpuri clasei se realizează prin funcția încorporată setattr().
Mai întâi, să ne uităm la structura funcției setattr():

setattr(object, name, value)

unde este parametrul object al obiectului pe care dorim să-l setăm, adică obiectul cu care modificăm
sau adăugăm câmpuri și metode.

Cu parametrul name definim numele proprietății (membru al clasei).

Cu parametrul value definim valoarea proprietăţii.

Să analizăm această sintaxă într-un exemplu:

1class Person:
2 name = 'John'
3
4new_person = Person()
5print('Before name changing:', new_person.name)
6
7setattr(new_person, 'name', 'David')
8
9print('After name changing:', new_person.name)

Page 78 of 107
Rezultatul acestui cod este următorul:

Before modification: John


After modification: David

Pe următoarea linie: setattr (new_person, 'name', 'David'), apelăm funcția setattr și îi trecem ca
parametru obiectul new_person, deci obiectul pe care dorim să-l modificăm, apoi specificăm câmpul
clasei name, deoarece acesta este proprietatea pe care dorim să o schimbăm, iar în al treilea parametru
setăm această valoare la David. Aşa cum puteți vedea, acum schimbăm dinamic valoarea unui câmp în
clasă în timp ce executăm codul.

Dacă vrem doar să furnizăm un câmp sau o metodă, atunci folosim funcția getattr. Mai multe despre ea
vor fi în continuarea lecției.

O metodă similară poate fi aplicată şi metodelor:

1class Example:
2 pass
3obj = Example()
4
5def new_method(self): # Have to add self since this will become a method
6 print('Hello world!')
7
8setattr(Example, 'show_text', new_method)
9
10obj.show_text()

În acest exemplu, vedem că clasa a fost creată fără metode, apoi definim funcția new_method și îi dăm
parametrul self, deoarece va deveni o metodă. După aceea, folosim din nou funcția setattr, dar de
această dată spunem că clasa Example primește o proprietate nouă, numită show_text, iar valoarea
acelei proprietăți este new_method, deci toată funcția din codul nostru.

Imediat după aceea, putem accesa metoda show_text prin intermediul obiectului, care acum afișează
textul. Deci, în felul acesta, funcției Example i-am adăugat o metodă nouă. Pentru a ne asigura că clasa
are într-adevăr o metodă nouă, vom folosi funcția dir, cu care ne-am familiarizat anterior:

print(dir(Example))

În rezultatul:

['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__',


'__format__', '__ge__', '__getattribute__', '__gt__', '__hash__',
'__init__', '__init_subclass__', '__le__', '__lt__', '__module__',
'__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
'__setattr__', '__sizeof__', '__str__', '__subclasshook__',
'__weakref__', 'show_text']

putem vedea şi câmpul de clasă nou, metoda show_text.

Page 79 of 107
Instanțierea claselor în reflecție

Reflecția permite instanțierea clasei și pornirea metodelor ei pe baza stringurilor. Acest lucru permite să
ne fie complet necunoscută clasa, atât timp cât știm parametrii de care avem nevoie pentru a o instanția
și, eventual, pentru a-i apela metodele.

Aşa cum am menţionat de mai multe ori, adevăratul avantaj al utilizării reflecției este atunci când nu
știm prea multe despre o clasă sau modulul pe care dorim să îl folosim în codul nostru. Deci, trebuie să
salvați următorul exemplu în două fișiere diferite, dintre care unul îl va importa pe celălalt.

Conţinutul fişierului cu numele [Link] este următorul:

1class GameClass:
2 def __init__(self):
3 print("Game class is instantiated")
4 def start(self):
5 print("Game is started")
6 def hit(self,hit_strength):
7 print(f"Hit performed with strength {hit_strength}")

Aşa cum puteți vedea, este vorba de o clasă simplă care imită logica jocului cu câteva metode. Acum
vom folosi logica din această clasă în al doilea fișier, pentru a afișa rezultatele, dar vom instanția clasa și
vom accesa metodele de reflecție (pe baza analizei obiectelor).

Conţinutul celui de-al doilea fişier, cu numele [Link], este următorul:

1import importlib
2games_mod = importlib.import_module("games")
3
4print(dir(games_mod))
5
6game = "GameClass"
7
8game_class = getattr(games_mod,game)
9game_obj = game_class()
10
11print(dir(game_obj))
12
13start_method = "start"
14hit_method = "hit"
15
16method = getattr(game_obj,start_method)
17method()
18method = getattr(game_obj,hit_method)
19game_obj.hit(10)
20game_obj.hit(20)
21game_obj.hit(30)

Mai întâi, trebuie importat fișierul nostru [Link]. Vom salva imediat totul într-un obiect, deoarece
vom folosi obiectul pentru lucrul ulterior.

Page 80 of 107
Să presupunem că nu știm deloc care este structura acestui fișier/modulul pe care l-am importat. În
acest caz, folosim funcția dir pentru a furniza lista de clase, metode și câmpuri ce pot fi obținute prin
intermediul obiectului.

Este exact ce am făcut cu linia: print (dir (games_mod)).

Apoi, printre altele, vom vedea în listă şi valoarea GameClass. Aceasta este exact ceea ce ne indică
numele clasei, așa că am salvat-o într-o variabilă, pentru o utilizare ulterioară.

În următoarea linie folosim deja numele de clasă, acum cunoscut, pentru a obţine atributele ei:

1game_class = getattr(games_mod,game)
Tot acest obiect, care reprezintă clasa, îl putem salva sub un obiect nou, dar acum îl putem instanția:

game_obj = game_class()

Acum, game_obj reprezintă o instanță a clasei GameClass și putem folosi această instanță pentru a
analiza toate metodele și câmpurile pe care le avem în clasă. Analizând obiectele, reflectăm clasa și,
încet, furnizăm conținutul ei complet.

Următorul pas îl reprezintă metodele și câmpurile, iar acest lucru este acum ușor de realizat cu
print(dir(game_obj)); verificând rezultatele, putem vedea două câmpuri, adică metode, start şi hit. Și
acestea sunt informații valoroase pentru noi, așa că le vom stoca în două variabile: start_method și
hit_method.

Imediat, la fel ca înainte, folosim getattr pentru a obţine și activa ambele metode.

Deci, fără a cunoaște structura clasei, şi chiar nici numele ei, am putut analiza obiectele pentru a obține
toate informațiile. Am scris un cod complet dinamic, care gestionează clasa și câmpurile ei dintr-un alt
fișier; aceasta este exact reflecția.

Cealaltă parte a acestui exemplu este că toate numele metodelor și claselor pot fi utilizate în cod sub
formă de string.

La următorul link puteți găsi folderul complet al proiectului acestui exemplu, cu comentarii suplimentare
în cod.

Page 81 of 107
4.2. Tipuri generice

Rezumat

• Programarea generică este o tehnică care îi permite unei variabile să stocheze diferite tipuri de
date atâta timp cât sunt îndeplinite anumite condiții, cum ar fi subclasele și declarațiile
corespunzătoare.
• Un tip generic este un tip de date care nu este specificat și poate fi oricare - deci, orice tip
pentru un moment dat.
• Modul typing ne oferă verificarea ușoară a tipurilor de obiecte și, în același timp, permite
utilizarea sintaxei pentru crearea tipurilor generice, atât variabile, cât și clase și metode.
• Python este un limbaj dynamically typed. Acest lucru înseamnă că în Python, declarația
variabilelor se face dinamic și Python „nu știe” ce tip de variabilă este până când nu este
executată.
• Declarația de tip generic o efectuăm cu comanda: T = TypeVar('T')
• Declarăm clasa generică prin moștenirea clasei Generic a modulului typing și ca parametru
folosim tipul generic.

În cadrul acestei lecții, continuăm cu principiile avansate ale programării orientate pe obiecte. Unul
dintre conceptele importante sunt tipurile generice. Este important să știm că tipurile generice și
programarea generică sunt ceva ce reprezintă poate cele mai complexe principii de lucru dintr-o
abordare orientată pe obiecte. De aceea, ele nu sunt folosite adesea în mod real și nu sunt decisive
pentru programarea cu succes a aplicațiilor în Python. Desigur, utilizarea corectă a tipurilor generice
poate facilita foarte mult munca și poate accelera procesul de scriere a codului; sunt utilizate după cum
este nevoie și în funcție de datele cu care lucrăm.
Manipularea tipurilor generice reprezintă cel mai înalt grad de abstractizare și dinamică în crearea unei
arhitecturi de programe orientate pe obiecte. Această descriere are în spate un concept simplu. Vom
crea o clasă, dar nu vom evidenţia în ea în mod explicit tipurile, ci doar vom manipula simplu variabilele,
indiferent de tip. În felul acesta, îi permitem utilizatorului clasei să-şi aleagă propriul tip cu care clasa va
manipula. Acest lucru afectează îmbunătăţirea clasei și, astfel, şi aplicațiile complete.

În calcul, programarea generică este tehnica care permite ca o variabilă să stocheze diferite tipuri de
date atât timp cât sunt îndeplinite anumite condiții, cum ar fi o subclasă și o declarație adecvată.

Această abordare ne permite să creăm funcții și clase care sunt, de asemenea, independente de tip.
Termenul de programare generică subînţelege crearea unui cod de program care poate fi aplicat într-o
formă nemodificată pe diferite tipuri de date.

Un tip generic este, deci, un tip de date care nu este specificat și poate fi oricare - adică, orice tip pentru
un moment dat. În felul acesta, tipul generic definește un set de tipuri obținute pentru diferite valori ale
parametrilor.

Python şi tipuri

După cum probabil ați mai auzit, Python este un limbaj dynamically typed. Acest lucru înseamnă că în
Python, declarația variabilelor se face dinamic și Python „nu știe” ce tip de variabilă este până când
aceasta nu se execută. Exemplu:

1def din_greet(name):
2 return print('Hello ' + name)
3din_greet("John")

Page 82 of 107
În această funcție simplă, puteți vedea că nu specificăm de ce tip este parametrul, ci doar că este name.
Python, pe baza definiției valorii (pe care o facem atunci când apelăm o funcție), atribuie un tip string.
Aceasta este o abordare excelentă pentru noi, deoarece nu trebuie să pierdem timp definind tipul
fiecărei variabile sau parametri în codul nostru.

Însă, apar probleme când funcția obține numărul ca parametru de intrare:

1def din_greet(name):
2 return print('Hello ' + name)
3
4din_greet(132)

Apoi, vom obține o excepție despre tipul greșit la intrarea în funcție. De ce se întâmplă acest lucru dacă
nu trebuie să specificăm tipul de date la definire? Python funcționează examinând ce operații sunt
efectuate pe o variabilă și pe această bază se defineşte ce trebuie să obțină ca parametru. În cazul nostru,
în cadrul funcției efectuăm concatenarea stringurilor, iar pentru acea operație este necesar un string.

Bine, acesta este principiul după care lucrăm, dar atunci cel mai bine este să împiedicăm să fie introdus
un alt tip. Și putem face asta în Python:

1def greeting(name: str) -> str:


2 return print('Hello ' + name)
3greeting("John")

Acum, spunem în mod explicit că funcția primește parametrul name care trebuie să fie de tip string și
folosim chiar adnotarea suplimentară conform căreia funcția ar trebui să returneze un string.

Când programul se uită la codul nostru, acum știe că la acea funcție trebuie să ajungă un string. Dar ce
va fi dacă vrem ca funcția să utilizeze orice tip - deci, dacă ceva va ajunge la intrarea în funcție, trebuie
să fie procesat într-un fel, pentru a nu primi erori de tip? Aici ne ajută tipurile generice.

Pentru a apropia cele menţionate, de exemplu, trebuie mai întâi să folosim un modul relativ nou care a
apărut pentru prima dată în versiunea Python 3.5. Este vorba de modul typing, care ne asigură o
verificare ușoară a tipurilor de obiecte și, în același timp, ne permite să folosim sintaxa pentru a crea
tipuri generice, atât variabile, cât și clase și metode.

Desigur, la fel ca orice modul, se leagă de un fișier cu comanda import:

1import typing

Vom începe prin declararea unei variabile generice; modulul typing ne permite să folosim expresia
TypeVar, cu care definim variabila de tip - adică variabila care nu are tip la definire. Trebuie să specificăm
pentru Python că dorim să folosim TypeVar și obținem acest lucru cu:

1from typing import TypeVar

După aceea, definim variabila ca:

1T = TypeVar('T')
Aşa cum puteți vedea, sintaxa nu ne este cunoscută; TypeVar ia ca parametru un string cu caracterul T
și totul este stocat într-o variabilă. Motivul este că există o convenție de denumirea tipurilor în
programarea generică. Caracterul T, ca parametru, reprezintă, de fapt, tipul valorii - deci, nu un string,

Page 83 of 107
nu un int, ci un tip. Prin urmare, variabila T va fi de tip tip. Deci, nu folosim tipuri standard, iar Python îl
va salva chiar și ca tip. În cod, această variabilă de tip poate deveni de orice alt tip, în funcție de modul
în care o folosim.

Când vorbim despre convenția de numire, în continuare puteți vedea toate convențiile pentru
denumirea parametrilor de tip, deoarece vom folosi unele în această lecție:

Nume Utilizarea
E element
K cheie
N număr
T tip
V valoare
S, U, V al doilea, al treilea şi al patrulea tip

Tabelul 12.1. Numele parametrilor tip

Nu există prea mult folos de la variabila de tip dacă nu o utilizăm într-o clasă, metodă sau funcție, ca şi
parametru, deci, să vedem cum funcționează.

Funcții și metode generice

Putem scrie funcții generice simple în care argumentele pot fi de diferite tipuri. Pe baza tipului pe care
parametrul i-l oferă metodei generice, compilatorul efectuează acțiunea pentru fiecare apel, ca și cum
ar fi un tip obișnuit.

Parametrii de tip în funcții sunt folosiți pentru a declara ce tip de date i se returnează utilizatorului și de
a se comporta ca placeholders ai tipurilor care i se transmit metodei generice.

Să ne uităm la un exemplu:

1from typing import TypeVar


2
3T = TypeVar('T') # Declare type variable
4
5def do_nothing(one_arg:T,other_arg:T) -> None:
6 pass
7
8do_nothing(1,2)
9do_nothing("abc",UserID(42))

Deci, am definit din nou variabila de tip și apoi am setat-o ca doi parametri ai funcției - funcția care
returnează None. Deci, folosim funcția doar pentru a prelua informații. Dacă apelăm funcția generică, îi
putem transmite orice parametru. Deci, în primul caz, am trecut două numere, adică două date de tip
int, dar, în al doilea caz, am trecut un string și obiectul unei clase. Funcția scrisă în felul acesta va lua
orice date oferite ca parametru, deoarece am folosit o variabilă generică.

Page 84 of 107
Este important să menţionăm că metodele și tipurile generice nu au o aplicație clasică, să spunem că în
exemplul anterior luăm parametrii introduși și efectuăm unele operații peste ei; funcțiile scrise în felul
acesta sunt utilizate pentru a stoca date și apoi pentru a le redirecționa undeva.

Clase generice

Să ne uităm la unul dintre cele mai frecvente exemple de utilizare a programării generice și, în acelaşi
timp, veți vedea cum creăm o clasă generică.

Să presupunem că lucrăm cu un set de obiecte pe care vrem să le organizăm într-un fel. În acest caz,
sunt utilizate clase care sunt notate ca şi colecții.

Ele definesc obiecte care sunt folosite pentru a organiza alte obiecte de tip corespunzător în colecții
într-un mod adecvat. Este posibil să se adauge la colecție obiecte de tipuri diferite, dar atunci avem
problema determinării tipului de obiect când va fi utilizat. Dacă încercăm să folosim un obiect din
această colecție ca obiect al altei clase, vom întâmpina probleme.

Acesta este exact unul dintre cele mai frecvente cazuri de utilizare a tipurilor generice; să vedem un
exemplu în care folosim o listă pentru stocarea datelor:

1from typing import TypeVar, Generic


2
3T = TypeVar('T')
4
5class Stack(Generic[T]):
6 def __init__(self) -> None:
7 # Create an empty list with items of type T
8 [Link]: List[T] = []
9
10 def push(self, item: T) -> None:
11 [Link](item)
12
13 def pop(self) -> T:
14 return [Link]()
15
16stack = Stack()
[Link]("text")
[Link](123)
[Link]([2])
[Link](UserWarning())
[Link](stack)

Mai întâi, este important de menționat faptul că pentru a crea o clasă generică, folosim o clasă
Generic specială din modulul typing, pe care clasa noastră o moștenește. Astfel, aceasta devine generică
și funcționează cu parametri generici.

În cadrul codului putem vedea că creăm o listă goală, care pentru tipul de date pe care le stochează,
primește T, deci va primi tipul generic. După aceea, am definit metoda push, care este utilizată pentru a
adăuga un element în colecție, și metoda pop, care este utilizată pentru a prelua valoarea elementului.

Page 85 of 107
După aceea, instanțăm clasa stack și, în continuare, folosim obiectul ei pentru a trece tipurile. Tipurile
generice își arată puterea aici: transmitem diferite tipuri de date prin clase clasice până la alte clase și
chiar trecem lista ca element. Acest lucru a fost posibil datorită tipurilor generice.

Să vedem cum folosim clasele și tipurile generice într-un singur caz de utilizare:

1from typing import TypeVar


2
3T = TypeVar('T', bound='Shape')
4
5class Shape:
6 def set_scale(self: T, scale: float) -> T:
7 [Link] = scale
8 return self
9
10class Circle(Shape):
11 def set_radius(self, r: float) -> 'Circle':
12 [Link] = r
13 return self
14
15class Square(Shape):
16 def set_width(self, w: float) -> 'Square':
17 [Link] = w
18 return self
19
20circle = Circle().set_scale(0.5).set_radius(2.7)
21square = Square().set_scale(0.5).set_width(3.2)

Deci, avem două clase: Circle și Square; fiecare are propriile câmpuri corespunzătoare pentru figura
geometrică în cauză și un câmp comun la scale. Dacă am folosi abordarea clasică, fiecare din aceste
clase ar trebui să aibă o metodă prin care setăm valori pentru câmpul scale (setter), iar metodele setter
deja scrise ar trebui să aibă câmpuri pentru parametri speciali, pentru rază și width. Și aceasta este
singura abordare, pentru că așa ne dictează tipurile lor.

Folosind clasa generică Shape, avem posibilitatea de a apela pe un obiect două metode care iau tipuri
de date diferite. Deci, un obiect de tip Circle și un obiect de tip Square.

În versiunea clasică, încercarea unei astfel de instanțiere ar duce la raportul unei erori.

La începutul lecției, am menționat faptul că Python este un limbaj dynamically typed și că tipurile de
variabile sunt atribuite dinamic în timpul execuției. Datorită acestui fapt, în Python a fost redusă nevoia
de a crea manual tipuri generice. Acestea sunt utilizate în situații specifice, pe care le-am menționat în
lecție, deci pentru crearea unor colecții de date, definirea tipurilor de obiecte etc. Deoarece este vorba
de un principiu avansat, doar aplicațiile mai complexe au avantaje mai mari de la el. Ca formă de
programare, este puternic și ne oferă libertatea de a conecta tipuri pe care cu principiile de lucru
obișnuite nu le putem conecta.

Page 86 of 107
4.3. Gestionarea firelor și a proceselor

Rezumat

• Operațiunea de blocare este o operație care consumă mult timp, care nu permite efectuarea
operațiunilor ulterioare până când nu își finalizează logica.
• Pentru sistemele de operare, aplicațiile sunt privite ca procese.
• Procesele sunt modul în care sistemul de operare izolează și separă execuția programului.
• Fiecare aplicație poate fi compusă din unul sau mai multe procese.
• Fiecare proces poate avea unul sau mai multe fire.
• Firul este cea mai mică unitate executabilă care poate exista în cadrul unui proces.
• Codul Python pe care îl scriem se execută într-un fir.
• Utilizarea unui modul special threading ne permite să oferim cu ușurință multithreading.
• Creăm un fir cu expresia: [Link](target=, args=(,)), unde target reprezintă metoda
care trebuie să se execute în thread, în timp ce cu parametrul args definim parametrii acelei
metode.
• Multiprocesarea, așa cum îi sugerează și numele, este principiul creării mai multor procese, prin
care ne este permis să avem operațiuni care se execută în acelaşi timp, în loc să se aștepte una
pe cealaltă.
• Creăm un proces cu expresia: [Link](target=, args=(,)).

Atunci când navigați pe web, probabil aţi fost martorul unei înghețări scurte a mediului de utilizare a
paginii web pe care o vizualizaţi. O astfel de înghețare de scurtă durată este adesea rezultatul unor
operații de blocare care au loc la executarea codului paginii. Deoarece, în mod implicit, limbajele de
programare execută codul, comandă după comandă, se poate întâmpla ca o operație care consumă
timp să blocheze toată aplicația pentru o anumită perioadă de timp. Aşa cum ați putut vedea pe
parcursul acestui curs, putem avea un număr mare de clase care se moștenesc una pe alta și că aceleași
clase pot conține o logică complexă și pot efectua un număr mare de apeluri; toți acești factori afectează
viteza de execuție a unei aplicații.
Pentru a înțelege cum pot fi depășite astfel de situații, în această lecție vom vorbi despre execuția multi-
thread; despre noțiunea de fir/thread, proces și modul în care realizăm acest lucru în Python. Mai întâi,
să analizăm de ce este nevoie de fire și procese.

Pentru a înțelege de ce apare blocajul execuției codului și de ce execuția codului este încetinită, este
necesar să înțelegem modul în care este realizată executarea programelor, respectiv a aplicațiilor, de
către hardware prin sistemul de operare al calculatorului. Pentru sistemul de operare, o aplicație,
respectiv un program, poate fi compusă din mai multe procese și fiecare proces poate avea un număr
arbitrar de fire în interiorul lui (imaginea 13.1.).

Imaginea 13.1. Procese şi fire

Page 87 of 107
În imaginea 13.1. se poate vedea cum arată o aplicație din unghiul sistemului de operare pe care rulează.
Aplicația are două procese. Primul proces are un fir (thread), iar al doilea proces are două fire.

• Procesele sunt modul în care sistemul de operare izolează și separă execuția programului.
Simplu, se poate spune că procesele sunt, de fapt, programe sau părți ale programelor care
sunt executate pe calculator prin sistemul de operare.
• Firul este cea mai mică unitate executabilă care poate exista în cadrul unui proces. Fiecare
proces constă din unul sau mai multe fire. Procesele au cel puțin un fir, care este creat în timpul
procesului și care se numește fir principal. Un nucleu al unui proces de calculator poate executa
la un moment dat doar un fir.

Cele mai apropiate exemple de aplicații cu mai multe fire și procese paralele sunt browserele web
(Chrome, Firefox, Safari...). Aceste programe rulează direct pe sistemele de operare ale dispozitivului
client. Majoritatea browserelor web moderne creează pentru fiecare filă deschisă un proces separat. De
exemplu, pe sistemul de operare Windows. Putem vedea cu ușurință acest lucru (imaginea 13.2.).

Imaginea 13.2. Task Manager al sistemului de operare Windows şi browserul web Chrome cu 13 file
deschise

Page 88 of 107
Aşa cum puteți vedea, Google Chrome are 13 procese separate pentru 13 file deschise - unul pentru
fiecare filă.

Programare multi-thread în Python

În imaginea 13.2. puteți vedea că Google Chrome creează pentru fiecare filă deschisă un proces separat.
În mod implicit, fiecare dintre aceste procese are un fir care este utilizat pentru a executa codul
programului pe care îl scriem independent.

Practic, asta înseamnă că codul nostru se execută implicit într-un singur fir. În cadrul fiecărui fir, codul
se execută implicit, adică comandă cu comandă. Cu alte cuvinte, pentru ca o comandă să înceapă
executarea, cea anterioară trebuie să fie încheiată complet. Prin urmare, în cazul efectuării unei operații
care consumă mult timp, așa cum s-a făcut în exemplul prezentat anterior, toată pagina web devine
înghețată. Soluția la această problemă se poate obține prin utilizarea unui mod special de executare a
codului programului - execuție multi-thread/cu mai multe fire.

Imaginea 13.3. Afișare comparativă a executării codului într-unul sau mai multe fire

Imaginea 13.3. ilustrează afişarea comparativă a execuției codului de program într-unul sau mai multe
fire. Jumătatea de sus a imaginii ilustrează execuția codului într-un fir. Este important de observat că
metodele doSomeWork2() şi doSomeWork3() trebuie să aștepte finalizarea metodei doSomeWork1().

Jumătatea de jos a imaginii 13.3. ilustrează execuția cu mai multe fire. În imagine pot fi văzute două fire.
Operația care consumă timp, doSomeWork1(), a fost mutată într-un fir separat. Prin urmare,
metodele doSomeWork2() şi doSomeWork3() nu trebuie să aștepte, ci se execută în paralel cu
metoda doSomeWork1().

În următoarea parte a lecției, vom arăta cum realizăm exact acest lucru în Python.

În Python, utilizarea modului special threading ne permite să activăm cu ușurință multithreading. Acest
modul are toate funcțiile care ne oferă câteva opțiuni suplimentare pentru lucrul cu fire; pe cele mai
importante dintre ele le vom prezenta prin exemple. Să începem cu modul în care definim un fir.

Page 89 of 107
Pentru a lega în primul rând modulul cu fișierul nostru, vom folosi:

1import threading

Când avem modulul activat, crearea unui thread nou o realizăm cu obiectul clasei Thread, care este o
parte a pachetului threading. Instanțierea clasei necesită două argumente:

• target: în cadrul parametrului target specificăm funcția (metoda care se execută în firul nou);
• args: în cadrul parametrului args enumerăm toate argumentele care îi sunt necesare funcției
sau metodei pentru a funcționa.

1import threading
2
3def calculate_cube(num):
4 print("Cube: {}".format(num * num * num))
5
6def calculate_square(num):
7 print("Square: {}".format(num * num))
8
9if __name__ == "__main__": # if the main program started do this:
10 # creating thread
11 t1 = [Link](target=calculate_square, args=(10,))
12 t2 = [Link](target=calculate_cube, args=(10,))
13
14 # start thread 1
15 [Link]()
16 # start thread 2
17 [Link]()
18
19 # wait until thread 1 is completely executed
20 [Link]()
21 # wait until thread 2 is completely executed
22 [Link]()
23
24 # both threads completely executed
25 print("Done!")

În următorul cod puteți vedea unul din exemplele de bază de execuție multi-thread. Avem două funcții,
al căror scop este să calculăm pătratul și cubul unui număr. Cu acest program, de fapt, am
schimbat ordinea de execuție a funcțiilor. Implicit, codul se execută de sus către partea de jos a fișierului.
Cu acest principiu se pornește mai întâi funcția calculate_cube, rezultatul se calculează și se afișează și
abia apoi intrăm în funcția calculate_square. Prin fire, acum am stabilit că mai întâi este necesar să se
execute o altă funcție în cod. Să vedem cum:

1if __name__ == "__main__": # if the program started do this:


2 # creating thread
3 t1 = [Link](target=calculate_square, args=(10,))
4 t2 = [Link](target=calculate_cube, args=(10,))

Această parte de cod este decisivă pentru toată logica. În primul rând, cu structura if verificăm dacă a
început executarea codului; metoda main spune că programul a fost activat. Dacă acesta este cazul,
creăm fire. În acest caz, creăm două fire, unul pentru fiecare dintre funcțiile noastre. Primul fir este utilizat

Page 90 of 107
pentru funcția calculate_square și îi trecem funcției numărul necesar. După aceea, definim un alt fir
pentru a doua funcție.

Desigur, crearea firelor nu este suficientă, ci trebuie să le şi pornim; în același timp, cu acest pas putem
defini ce fir începe cu execuția:

1# start thread 1
2 [Link]()
3 # start thread 2
4 [Link]()
Când lansăm firele, programul principal continuă să ruleze, deoarece acum aceste două funcții sunt
tratate ca programe separate, chiar și de către programul principal. Și dacă lăsăm codul neschimbat, am
avea trei fire care se execută în paralel: firul principal, adică firul programului, primul fir al funcției, al
doilea fir al celei de-a doua funcţii.
Deoarece dorim ca aceste funcții să se execute prima dată, astfel încât programul să poată continua,
folosim metoda join(). Această metodă oprește programul principal și permite executarea firului pe care
este apelată. Deci, cu următorul cod:

[Link]()
[Link]()

oprim programul principal și executăm primul thread(t1), apoi al doilea thread(t2). Când ambele îşi
termină activitatea, adică atunci când codul este executat în funcții, programul principal continuă să
funcționeze și, prin urmare, vedem imprimarea „Done!”. Puteți vedea fluxul de programe ilustrat în
imaginea 13.4:

Imaginea 13.4. Fluxul programului cu fire

Page 91 of 107
În exemplul anterior, am schimbat ordinea de execuție a funcțiilor și am determinat ca primele să fie
executate în raport cu programul principal.

Multiprocessing

În secțiunea anterioară, am vorbit despre fire și, așa cum am menționat în introducerea teoretică a lecției,
fiecare fir este o parte din proces. În cadrul acestui proces putem avea mai multe fire și, în felul acesta,
putem adăuga fire noi și putem distribui firele, astfel încât să nu apară o așteptare inutilă.

Ce este important - cu firele nu putem accelera execuția programului, ci doar să organizăm cum să se
facă execuția, în ce ordine și să împiedicăm oprirea programului, deoarece a întâlnit o parte prea mare
sau complexă a codului. Acum, întrebarea este - ce să facem ca aplicația noastră să se execute mai
repede?

Aici intervine multiprocesarea. Aşa cum îi sugerează și numele, acest principiu ne oferă posibilitatea de
a crea mai multe procese, care vor avea apoi propriile fire. Acest lucru ne permite să avem operațiuni
care se efectuează în același timp, în loc să se aştepte una pe cealaltă.

Acest principiu stă la baza funcționării sistemului de operare în sine; din acest motiv, putem folosi în
paralel, de exemplu, Google Chrome și Microsoft Word. Un proces are grijă de o aplicație, iar celălalt
are grijă de o altă aplicație. Desigur, acest lucru este foarte simplificat, dar acesta este exact principiul
de bază.

Pe partea hardware putem prezenta multiprocesarea în felul următor. Dacă calculatorul are doar un
nucleu de procesor, acelui nucleu îi atribuim în acelaşi timp mai multe procese (aplicații). Apoi procesorul
efectuează mai multe acțiuni în același timp, dar el, de fapt, face pauze - oprește o sarcină și trece scurt
la alta. Aceste acțiuni încetinesc funcţionarea calculatorului și avem un calculator mai rapid dacă
procesorul are mai multe nuclee, deoarece activitatea se distribuie pe mai multe nuclee.
Putem executa același principiu şi în aplicațiile proprii - deci, să creăm mai multe procese, care vor folosi
mai bine resursele calculatorului, prin urmare, şi executarea aplicației noastre va fi mai rapidă. Mai exact,
scriind un program care acceptă multiprocesarea, îi specificăm sistemului, dar și procesorului
calculatorului că unele părți ale codului nostru pot fi executate în paralel cu alte părți. Când sistemul
informatic „vede” acest lucru, unul dintre procesele noastre se poate rula, de exemplu, pe nucleul 4, în
timp ce unul rulează pe nucleul 1, ceea ce îmbunătățește semnificativ timpul de execuție a aplicației pe
sistemul de operare.
În cadrul lui Python, lucrul cu mai multe procese este foarte asemănător cu lucrul cu fire; diferența este
doar în modulul pe care îl folosim și în clasa acelui modul. Am folosit pentru lucrul cu fire, modulul
threading, în timp ce pentru procese folosim multiprocessing.

La fel ca pentru fire, Python ne oferă cu acest modul toate funcțiile pentru lucrul cu procese. Începem
cu modul în care definim procesul.

Pentru a lega în primul rând modulul de fișierul nostru, folosim:

import multiprocessing

Când avem modulul activat, creăm procesul nou cu obiectul clasei Process, care face parte din
pachetul multiprocessing. La fel ca în cazul firelor, instanțierea unei clase necesită două argumente:

• target: în cadrul parametrului target specificăm funcția (metoda care se execută în procesul
nou);

Page 92 of 107
• args: în cadrul parametrului args enumerăm toate argumentele care îi sunt necesare funcției
sau metodei pentru a funcționa.

După cum puteți vedea, este complet identic. Vom lua exemplul anterior din lecție, doar că, de data
aceasta, vom plasa funcțiile în procese separate, nu în fire noi:

1import multiprocessing
2
3def calculate_cube(num):
4 print("Cube: {}".format(num * num * num))
5
6def calculate_square(num):
7 print("Square: {}".format(num * num))
8
9if __name__ == "__main__":
10 process1 = [Link](target=calculate_square, args=(10,))
11 process2 = [Link](target=calculate_cube, args=(10,))
12
13 [Link]()
14 [Link]()
15
16 [Link]()
17 [Link]()
18
19 print("Done!")

Aşa cum puteți vedea, structura exemplului nostru nu s-a schimbat; diferența este că acum apelăm
funcțiile prin clasa Process. De asemenea, avem metode start pentru a porni procesul și metode join()
pentru a opri executarea codului principal al programului până când sunt finalizate procesele.

Principala diferență este că procesele au fost efectuate în același timp, spre deosebire de thread, unde
a fost executat prima dată thread1 şi după aceea thread2.

Page 93 of 107
4.4. Gestionarea evenimentelor

Rezumat

• Programarea care implică monitorizarea și procesarea evenimentelor în timpul executării


aplicației se numește programare event-based (sau event-driven). În această abordare, separăm
logica care procesează evenimentul de restul codului programului.
• Cel mai cunoscut sistem de gestionare a evenimentelor se bazează pe
modelul Distributer/Subscriber.
• În cadrul modelului Distributer/Subscriber, avem trei elemente de bază: event listener,
distributer, subscriber.
• Ascultătorul este o metodă care așteaptă apariţia evenimentului.
• Distribuitorul de evenimente este clasa în care a avut loc evenimentul.
• Clasa distributer trebuie să conţină patru părți:
o lista de ascultători;
o mecanism de adăugare a ascultătorilor pe listă (add_listener);
o mecanism de eliminare a ascultătorului (remove_listener);
o mecanism pentru notificarea ascultătorului.
• Modelul Distributer/Subscriber este cunoscut ca șablon Observer.

În această lecție, vom vorbi despre conceptul de eveniment, precum și despre modul de programare
care este axat pe evenimente și una dintre cele mai frecvent utilizate abordări pentru lucrul cu
evenimente, modelul Distributer/Subscriber.

Evenimentele sunt unul dintre conceptele principale în programarea aplicațiilor GUI (Graphical User
Interface), adică aplicații cu care utilizatorul interacționează prin intermediul unor controale, prin taste,
ferestre, câmpuri și altele asemenea. Desigur, mai mult despre interfețe și urmărirea interacțiunii
utilizatorului cu aplicația vor fi prezentate mai târziu în timpul învățării Python. În acest curs, accentul
este pus pe apropierea principiilor de lucru cu un eveniment în timpul executării aplicației.

Programarea care implică monitorizarea și procesarea evenimentelor în timpul executării aplicației se


numește programare event-based (sau event-driven). În această abordare, separăm logica care
procesează evenimentul de restul codului programului. De asemenea, atunci când lucrăm cu
evenimente, avem un termen pe care îl veți auzi des: procesarea evenimentelor (event handling). Acest
termen reprezintă cel mai adesea o funcție în codul nostru care se execută atunci când se produce un
anumit eveniment.

Pentru a clarifica acest lucru, să luăm un exemplu din viață. Să presupunem că vrem să conducem o
mașină al cărei rezervor nu este plin. Am avea două modalități de a ne asigura că nu vom rămâne fără
combustibil în timp ce conducem. Una este să verificăm permanent starea rezervorului, iar cealaltă este
să ne bazăm pe indicatorul rezervorului de combustibil.

Veți fi de acord că a doua variantă este mult mai bună decât prima și exact așa funcționează şi
evenimentele obiectelor în programare. Deci, codul nostru va monitoriza câțiva indicatori (evenimente)
și va reacționa pe baza lor.

Să ne uităm la următorul exemplu:

1class Reservoir:
2 def __init__(self):
3 [Link] = 10
4 [Link] = 100

Page 94 of 107
5
6 def reserveIndicator(self):
7 print("Warning, low fuel level!")
8
9 def getFuel(self):
10 [Link] -=1
11
12 if [Link] <=[Link]:
13 [Link]()
14 print([Link])
15
16res = Reservoir()
17
18
19for i in range(100):
20 [Link]()

În acest exemplu, am implementat o clasă care reprezintă rezervorul mașinii și avem toată logica
necesară - adică câmpurile pentru nivelul combustibilului, metoda care arată când nivelul rezervorului
este prea scăzut, metoda getFuel care simulează consumul de combustibil - și am putea spune că acest
lucru este un întreg rotunjit. Dar există o problemă.

Rezervorul în sine este conștient de intrarea în rezervă, dar lumea din jurul lui nu este. În cadrul metodei
main, nu știm că rezervorul este „pe rezervă”, deoarece evenimentul este identificat și procesat doar în
cadrul obiectului clasei. Deci, este la fel ca în cazul în care indicatorul de rezervă a combustibilului se
află pe rezervor, în interiorul șasiului mașinii și șoferul nu-l poate vedea. Indicatorul va funcționa, dar
asta nu contează, deoarece șoferul nu-l poate vedea și reacționează la eveniment.

Cu această analogie ajungem la modelul Distributer/Subscriber. Acesta include următoarele:

Pentru ca evenimentul să fie vizibil pentru obiectele din afara obiectului în care a avut loc (distribuitor),
acestea trebuie să se aboneze la el. Și pentru ca un obiect să se aboneze la evenimentul unui alt obiect,
acesta trebuie să îndeplinească condițiile. Aceste condiții implică existența unei metode adecvate pe
obiectul abonatului.

În momentul abonării, distribuitorul obiectului înregistrează abonatul la eveniment (îl plasează pe lista
abonaților). Când se detectează evenimentul, distribuitorul trece prin lista de abonați și le transmite
tuturor informațiile că a avut loc evenimentul. Acest concept este cunoscut şi sub numele de
șablon observer.

Dacă urmărim analogia cu rezervorul din mașină, asta ar însemna că fiecare instrument care dorește să
monitorizeze nivelul de combustibil trebuie să aibă o conexiune cu rezervorul. Deci, luminiţa de pe
panoul de control trebuie să fie conectată la indicatorul nivelului de combustibil din rezervor. Acest fir
este metoda noastră pentru abonat. Iar distribuitorul, în cazul nostru, contorul nivelului, monitorizează
cine ar trebui să fie informat că nu există suficient combustibil (lumina din bord, motorul mașinii etc.).

Să vedem acum pe exemplul nostru cum implementăm modelul Distributer/Subscriber:

Event listener

Primul pas în procesul de definire a evenimentelor este construirea unui ascultător de evenimente.
Ascultătorul este o metodă care așteaptă apariţia evenimentului. În codul în sine, ascultătorul

Page 95 of 107
evenimentului este cel mai adesea o abstractizare care are una sau mai multe metode despre care vor
şti distribuitorul și abonații. Ascultătorul de evenimente este ceva care trebuie legat logic de clasa care
va genera evenimentul (sau mai multe evenimente).

Pentru exemplul nostru, codul ascultătorului, adică event listener arată aşa:

1import abc
2class EventListener([Link]):
3 @[Link]
4 def reserve_reached(self,sender):
5 pass

Deci, în cadrul acestei clase, care reprezintă ascultătorul, avem doar o metodă abstractă, care
monitorizează dacă backup-ul este activat. Are doar un parametru și aceasta este metoda care îi trimite
informații.

Distribuitor de evenimente

Următorul este distribuitorul de evenimente. Acesta este clasa în care a avut loc evenimentul. Pentru a
fi distribuitor, această clasă trebuie să conţină anumite părți:

• lista de ascultători;
• mecanism de adăugare a ascultătorilor pe listă (add_listener);
• mecanism de eliminare a ascultătorului (remove_listener);
• mecanism pentru notificarea ascultătorului.

În cod arată așa:

1class Reservoir:
2 def __init__(self):
3 self.current_state = 100
4 self.reserve_limit = 50
5 [Link] = []
6
7def add_listener(self, listener):
8 [Link](listener)
9
10 def remove_listener(self, listener):
11 [Link](listener)
12
13 def distribute_event(self):
14 for listener in [Link]:
15 if isinstance(listener, EventListener):
16 listener.reserve_reached(self)

Puteți vedea că, mai întâi, am adăugat la câmpurile clasei un câmp nou listeners; aceasta este lista
noastră de ascultători, care este, desigur, goală la început.

După aceea, avem metoda add_listener, al cărei scop este să adauge mai mulți membri în listă, apoi
metoda remove_listener pentru a elimina un anumit membru din listă și, în final, metoda distribuitorului
în sine, care, dacă apare evenimentul, va executa logica.

Page 96 of 107
Activarea evenimentului

Atunci când există un mecanism complet de gestionare a evenimentelor în cadrul unei clase, activarea
efectivă a evenimentelor ar trebui făcută după cum este necesar. În cazul nostru, activarea evenimentului
se face prin logica consumului de combustibil:

1def consume_fuel(self):
2 print(f"Fuel consumed. {self.current_state} liters remaining")
3 self.current_state -= 1
4 if self.current_state < self.reserve_limit:
5 self.distribute_event()
Deci, cu metoda consume_fuel imprimăm cât combustibil a mai rămas, reducem starea combustibilului
și activăm evenimentul. Vom realiza activarea prin următoarele linii:

1if self.current_state < self.reserve_limit:


2 self.distribute_event()

Deci, dacă nivelul combustibilului este mai mic decât nivelul de rezervă predefinit, pornim distribuitorul
evenimentului, ceea ce provoacă o reacție în lanț, deoarece distribuitorul va activa event listener-ul, care
apoi va reacționa la eveniment.

Abonarea la eveniment

Deci, până acum avem toată logica necesară, un distribuitor care trimite informații și o modalitate de a
activa evenimentul. Această logică ne duce din nou la început. Evenimentul are loc, dar nimeni nu vede
că se întâmplă.

Acest lucru se datorează faptului că nu avem un abonat, adică, dacă vă amintiți analogia noastră, nu
avem luminiţa care să semnaleze nivelul de combustibil, astfel încât șoferul să o poată vedea. Asta ne
duce la abonat. Deci, atunci când obiectul este capabil să detecteze un eveniment și să îl distribuie
abonaților, putem adăuga abonați după nevoie:

1class MyListener(EventListener):
2 def reserve_reached(self,sender):
3 print("Warning, Low fuel level")
4
5res = Reservoir()
6res.add_listener(MyListener())<br><br>
Acum, cu clasa nouă moștenim clasa EventListener, definim metoda obligatorie reserve_reached
(datorită abstractizării) și o folosim pentru a afișa alerta.

Cu aceasta am realizat funcționalitatea completă; acum toate părțile codului nostru pot urmări un
eveniment. Codul complet al exemplului cu imprimarea datelor ar putea fi:

1import abc
2class EventListener([Link]):
3 @[Link]
4 def reserve_reached(self,sender):
5 pass
6
7class Reservoir:
8 def __init__(self):

Page 97 of 107
9 self.current_state = 100
10 self.reserve_limit = 50
11 [Link] = []
12
13 def add_listener(self, listener):
14 [Link](listener)
15
16 def remove_listener(self, listener):
17 [Link](listener)
18
19 def distribute_event(self):
20 for listener in [Link]:
21 if isinstance(listener, EventListener):
22 listener.reserve_reached(self)
23
24 def consume_fuel(self):
25 print(f"Fuel consumed. {self.current_state} liters remaining")
26 self.current_state -= 1
27 if self.current_state < self.reserve_limit:
28 self.distribute_event()
29
30class MyListener(EventListener):
31 def reserve_reached(self,sender):
32 print("Warning, Low fuel level")
33
34res = Reservoir()
35res.add_listener(MyListener())
36
37for i in range(0,100):
38 res.consume_fuel()

Deci, acesta este unul dintre cele mai folosite modalităţi pentru gestionarea unui eveniment. Acum, s-
ar putea să vă întrebați - de ce am folosit pe tot parcursul lecției un exemplu, cum vom ști să ne scriem
evenimentele?

Motivul este că am folosit un șablon, mai exact un șablon Observer; despre șabloane vom mai vorbi, dar
deocamdată este suficient să știţi că un șablon este ceva pe care îl scriem o dată și îl folosim din nou în
cazuri similare. Concret, pentru un alt caz, în altă aplicație și situație, indiferent ce eveniment urmăriţi,
utilizați aceeași logică și aceeași structură de metode și clase.

Dacă luăm codul exemplului și eliminăm tot ceea ce este legat de logica exemplului, rămâne un șablon
pentru crearea unui model pentru lucrul cu evenimente:

1import abc
2class EventListener([Link]):
3 @[Link]
4 #activate event logic
5
6class EventHandler:
7 def __init__(self):
8 #fields
9 [Link] = []

Page 98 of 107
10
11 def add_listener(self, listener):
12 [Link](listener)
13
14 def remove_listener(self, listener):
15 [Link](listener)
16
17 def distribute_event(self):
18 for listener in [Link]:
19 if isinstance(listener, EventListener):
20 #activate listener
21
22 def DoSomething(self):
23 # logic for doing something
24 if: #event occurs activate distributer
25 self.distribute_event()
26
27class MyListener(EventListener):
28 # define event
29
30objectEvent = EventHandler()
31objectEvent.add_listener(MyListener())

Deci, acum, în acest șablon, schimbați părțile în care sunt comentariile cu logică sau informațiile necesare
pentru realizare. Tocmai aceasta este puterea și scopul creării unui șablon.

Page 99 of 107
4.5. Șabloane de proiectare

Rezumat

• Șabloanele sunt soluții standard la problemele de programare standard.


• În programare, avem trei categorii de șabloane: șabloane de creație, șabloane structurale și
șabloane comportamentale.
• Singleton permite crearea unei instanțe a unei anumite clase.
• Folosim patternul Factory atunci când vrem să avem un intermediar în instanțierea clasei.
• Șablonul Adapter aparține șabloanelor structurale; acesta ne permite să folosim două obiecte
care nu ar putea comunica în condiții normale. Cu Adapter, ca în viață, conectăm două interfețe
incompatibile.
• Șablonul Builder este foarte specific în proiectare și îl folosim atunci când avem un obiect prea
complex care ar lua un număr mare de parametri. În șablonul Builder, clasa creează produsul
final conform principiului pas cu pas.

Deși cuvântul design poate fi asociat cu o componentă grafică a unei aplicații, șabloanele de proiectare
sunt, de fapt, foarte departe de aceasta. Şabloanele sunt, de fapt, formulare de program care au legătură
doar cu structura programului aplicației. În această lecție vom explica ce sunt șabloanele de proiectare;
vom prezenta tipurile lor și modul în care realizăm cele mai folosite șabloane în Python. În lecția
anterioară, ați putut vedea unul dintre cele mai utilizate șabloane - șablonul Observer, pe care l-am
folosit pentru a lucra cu evenimentul.
Ce este exact un șablon de proiectare? Nu este nimic altceva decât o soluție standard pentru o problemă
de programare standard. La această soluție s-a ajuns evolutiv și empiric, iar prin utilizarea ei eliminăm
fenomenul „inventării apei calde” în crearea aplicației noastre şi accelerăm semnificativ producția, mai
ales dacă are loc într-un mediu de echipă.
Şabloanele (patterns) nu sunt ceva ce putem folosi cu Copy/Paste. Acestea sunt implementate în
structura obiectului și fac parte integrantă din el, cu accent pe cuvântul „obiect”, deoarece șabloanele
de proiectare sunt strâns legate de conceptul de programare orientată pe obiect.

Tipuri de șabloane de proiectare

În programare avem trei categorii de şabloane:

1. Șabloane de creație - Aceste șabloane se ocupă de crearea obiectelor, într-un mod adaptat
unei anumite aplicații. Sunt deosebit de importante în situațiile în care o abordare obişnuită a
creării obiectelor ar duce la creșterea complexităţii proiectului.

Unele dintre patternurile care se încadrează în acest grup sunt: Singleton, Constructor,
Prototype, Factory, Abstract şi Builder.

2. Şabloanele structurale - se ocupă de compoziția codului și, de obicei, reprezintă modalități de


definire a relațiilor dintre obiecte. Acestea asigură ca, în cazul în care are loc o modificare într-
o parte a sistemului, restul sistemului să nu trebuiască să se schimbe.

Unele dintre patternurile structurale sunt: Decorator, Facade, Flyweight, Adapter şi Proxy.

3. Şabloanele comportamentale - Acest grup de patternuri se ocupă de îmbunătățirea


comunicării între diferite obiecte din sistem.

Exemple binecunoscute sunt: Iterator, Mediator, Observer şi Visitor.

Page 100 of 107


Aşa cum puteți vedea, cantitatea de șabloane disponibile este mare; în această lecție vom procesa doar
câteva dintre cele mai faimoase și cele mai des utilizate în Pyhon. Vom începe cu șablonul Singleton.

Singleton pattern

Patternul Singleton garantează că veți crea o singură instanță a unui anumit obiect din cod. Să urmărim
exemplul:

1class MyClass():
2 pass
3
4a = MyClass()
5b = MyClass()
6
7print(a==b)

Acest program va emite rezultatul false, deoarece variabilele a și b nu reprezintă unul şi același obiect.
Acesta este un comportament complet logic al programului specificat, dar ce va fi dacă în clasa
MyClass există un sistem de gestionare a bazelor de date? Atunci, probabil, am dori să ne asigurăm că
utilizatorul poate crea mai multe instanțe ale obiectului, deoarece, în acest caz, ar avea mai mult decât
o conexiune. În astfel de cazuri, cea mai bună soluție ar fi utilizarea unui pattern Singleton:

1class Singleton:
2 __instance = None
3 @staticmethod
4 def getInstance():
5 if Singleton.__instance == None:
6 Singleton()
7 return Singleton.__instance
8 def __init__(self):
9 if Singleton.__instance != None:
10 raise Exception("This class is a singleton!")
11 else:
12 Singleton.__instance = self
13
14s1 = Singleton()
15s2 = Singleton()

Principala caracteristică a clasei Singleton este că constructorul ei este privat. Deci, în exemplul nostru
avem un câmp instance care este privat. După aceea, este suficient ca în metoda __init__ să
verificăm dacă câmpul este deja utilizat, adică dacă avem o instanță. Dacă avem o instanță, folosim
comanda raise pentru a afișa o excepție la fiecare instanțiere ulterioară a clasei. În felul acesta suntem
siguri că clasa va fi instanțiată doar o dată, pentru că, altfel, primim o excepție.

Factory

Folosim acest șablon atunci când dorim să folosim o clasă intermediară cu ajutorul căreia vom crea o
instanță a unei anumite clase. Să presupunem că avem în sistem mai multe clase de utilizatori, dintre
care dorim să creăm unul, pe baza fluxului de programe:

Page 101 of 107


1class Message(object):
2
3 message = ""
4 def get_message(self):
5 return [Link]
6
7class User(Message):
8 message = "Hello user!"
9
10class Administrator(Message):
11 message = "Hello administrator!"
12
13class SuperAdministrator(Message):
14 message = "Hello superadministrator!"

Deci, avem trei clase pentru nivelurile de acces ale utilizatorilor; în cadrul fiecărei clase se va realiza o
anumită logică, cu scopul de a arăta doar un mesaj de salut. Avem o clasă separată care afișează mesajul
în funcție de utilizator şi trei clase de utilizator moștenesc clasa Message.

Dacă am dori să verificăm ce utilizator se autentifică, ar trebui să instanțiem fiecare clasă individual, chiar
și atunci când nu este nevoie. Pentru a evita o astfel de robustețe, vom introduce o clasă separată, care
într-un loc va efectua selecția și va crea instanța corespunzătoare. Această clasă va fi, de fapt, clasa
Factory:

1class UserFactory():
2 def create_user(self, type):
3 targetclass = [Link]()
4 return globals()[targetclass]()

Această clasă va prelua ce nivel de utilizator este selectat și pe baza aceasta va întoarce apelul clasei
solicitate.

După aceea, creăm obiectul din clasa Factory și ulterior adăugăm tipurile, doar într-un loc, în clasa
Factory:

1user_obj = UserFactory()
2
3user = []
4input_user = input("What is your access level?")
[Link](input_user)
6
7for u in user:
8 print (user_obj.create_user(u).get_message())

Adapter pattern

Șablonul Adapter aparține șabloanelor structurale; acesta ne permite să folosim două obiecte care în
condiții normale nu ar putea comunica.

Cu adaptorul, ca de obicei, conectăm două interfețe incompatibile. Un exemplu din viață ar fi un telefon
mobil. Dorim să încărcăm bateria telefonului, dar avem doar un încărcător Type-C, iar telefonul nostru
acceptă standardul mai vechi, MicroUSB. Desigur, acest lucru nu este posibil atâta timp cât nu avem un

Page 102 of 107


adaptor pentru încărcător. Același lucru este valabil și în programare, unde putem crea o clasă adaptor
care ne va permite să ne conectăm la clasa cu care altfel nu am putea crea o conexiune.

Să vedem asta pe un exemplu. Să presupunem că am cumpărat un fierbător care foloseşte o altă priză.
În cazul în care nu sunteți familiarizați cu această problemă - în țările europene se utilizează prize care
au o tensiune de 220 volți, în timp ce conform standardului american, tensiunea se menține la 110 volți,
ceea ce înseamnă că - dacă avem un dispozitiv care utilizează standardul american, dacă îl pornim, ar
arde imediat datorită tensiunii ridicate. De aceea, avem nevoie de un adaptor care să reducă tensiunea
și să ne permită să folosim dispozitivul. Putem reprezenta acest lucru și în Python:

1class EuropeanSocket:
2 def voltage(self): pass
3
4class Socket(EuropeanSocket):
5 def voltage(self):
6 return 230

Priza europeană standard este reprezentată de clasa EuropeanSocket; folosim o clasă Socket separată
pentru a defini tensiunea.

1class USASocket:
2 def voltage(self): pass

Priza folosită de fierbător este prezentată ca USASocket; ea are, de asemenea, și un parametru de


tensiune.
Iar acum ne mai rămâne să definim adaptorul; acesta va lua ca parametru ce priză folosim, adică unde
conectăm dispozitivul și setăm tensiunea dorită, în cazul nostru, la standardul american.

1class Adapter(USASocket):
2 __socket = None
3
4 def __init__(self, socket):
5 self.__socket = socket
6
7 def voltage(self):
8 return 110

Acum putem prezenta fierbătorul cu o clasă; această clasă va lua tensiunea ca parametru de intrare.
Conform metodei boil (fierbere), vom seta condiția ca, dacă tensiunea este mai mare de 100 volți,
fierbătorul arde, iar pentru celelalte cazuri, fierbătorul funcționează corect.

1class ElectricKettle:
2 __power = None
3
4 def __init__(self, power):
5 self.__power = power
6
7 def boil(self):
8 if self.__power.voltage() > 110:
9 print ("Kettle on fire!")
10 else:
11 print ("Coffee time!")<br><br>

Page 103 of 107


Mai rămâne doar să organizăm corect procesul de activare. Deci, mai întâi, instanțiem priza, aceasta
funcționează acum la 220V; apoi, instanțiem clasa Adapter, care moștenește Socket. Deci, am conectat
adaptorul la o priză europeană și acesta a redus tensiunea la 110V; după aceea, instanțiem fierbătorul,
care se conectează la adaptor, iar dacă rulăm acest exemplu, vedem că fierbătorul funcționează corect.

1socket = Socket()
2adapter = Adapter(socket)
3kettle = ElectricKettle(adapter)
4
[Link]()

Deci, realizăm patternul Adapter în cod cu aceeași logică ca și cum am folosi un alt adaptor în viață. Dacă
în cod modificăm următoarea linie:

kettle = ElectricKettle(adapter)

cu:

kettle = ElectricKettle(socket)

imprimarea rezultatelor ar fi: „Kettle on fire!”, deoarece am conectat dispozitivul la o priză greșită.

Desigur, acest exemplu servește la simplificarea acestui șablon doar în scopul unei înțelegeri mai bune
a problemei.

În programare folosim adesea șablonul Adapter pentru conversiile de date. În cursurile ulterioare, atunci
când veţi lucra cu date, veți utiliza două sisteme separate de stocare a datelor: XML și JSON; nu le vom
procesa acum, dar este important că acestea stochează datele în moduri diferite. Pentru a compila datele
dintr-un sistem în altul, folosim tocmai Python pentru a crea un adapter care va face transferul de date
dintr-un format în altul.

La fel cum am folosit aici Adapter pentru a trece de la o priză europeană la una americană.

Builder pattern

Șablonul Builder este foarte specific în proiectare și îl folosim atunci când avem un obiect prea complex
care ar lua un număr mare de parametri. În acest șablon Builder, clasa creează produsul final după
principiul pas cu pas. Marele avantaj este că obiectul clasei Builder este independent de alte obiecte,
deci putem folosi clasa Builder scrisă de mai multe ori.

Acest șablon este format din patru elemente:

1. Director – construiește, de fapt, un obiect complex prin intermediul builder;


2. Product – reprezintă obiectul complex care trebuie construit; include toate clasele necesare
pentru a se descrie părțile care vor fi utilizate pentru a construi un obiect complex;
3. Builder – definește interfața pe care o folosim pentru a construi produsul;
4. Concrete Builder – interfața builder pe care o apelăm dacă este necesar.

Să luăm ca exemplu ansamblarea vehiculului. Acest lucru ar putea arăta aşa:

Dacă urmărim șablonul, pornim de la clasa Director:

Page 104 of 107


1class Director:
2
3 __builder = None
4
5 def setBuilder(self, builder):
6 self.__builder = builder
7
8 # The algorithm
9 def getCar(self):
10 car = Car()
11
12 # First goes the body
13 body = self.__builder.getBody()
14 [Link](body)
15
16 # Then engine
17 engine = self.__builder.getEngine()
18 [Link](engine)
19
20 # And four wheels
21 i = 0
22 while i < 4:
23 wheel = self.__builder.getWheel()
24 [Link](wheel)
25 i += 1
26
27 return car
Deci, clasei Director trebuie să-i trecem ce clasă Builder va folosi și toate metodele utilizate pentru
asamblarea produsului în sine. În cazul nostru, cum exemplul este o mașină, avem șasiu, motor și roți.
Următorul element este product; în cadrul acestei clase avem toate piesele necesare care sunt asamblate
în alte locuri. Deci, în cazul nostru, folosim clasa pentru a „colecta” piesele realizate în alte locuri și i le
arătăm utilizatorului.

1class Car:
2 def __init__(self):
3 self.__wheels = list()
4 self.__engine = None
5 self.__body = None
6
7 def setBody(self, body):
8 self.__body = body
9
10 def attachWheel(self, wheel):
11 self.__wheels.append(wheel)
12
13 def setEngine(self, engine):
14 self.__engine = engine
15
16 def specification(self):
17 print ("body: %s" % self.__body.shape)
18 print ("engine horsepower: %d" % self.__engine.horsepower)
19 print ("tire size: %d'" % self.__wheels[0].size)

Page 105 of 107


După aceea, conform șablonului, creăm clasa Builder; aceasta este abstracția din sistem și rulează doar
metodele care vor compune mașina, în timp ce, așa cum am menționat mai devreme, clasa Director
asamblează mașina pe baza a ceea ce este specificat în clasa Builder:

1class Builder:
2 def getWheel(self): pass
3 def getEngine(self): pass
4 def getBody(self): pass

În Concrete Builder specificăm datele exacte pentru piese, deci specificăm ce mașină fabricăm,
dimensiunea roților, puterea motorului și forma șasiului:

1class JeepBuilder(Builder):
2
3 def getWheel(self):
4 wheel = Wheel()
5 [Link] = 22
6 return wheel
7
8 def getEngine(self):
9 engine = Engine()
10 [Link] = 400
11 return engine
12
13 def getBody(self):
14 body = Body()
15 [Link] = "SUV"
16 return body

Acum am configurat toate clasele șabloane și moștenirile lor. Mai rămâne doar să setăm valorile inițiale
pentru piese și să efectuăm instanțierea:
1class Wheel:
2 size = None
3
4class Engine:
5 horsepower = None
6
7class Body:
8 shape = None
9
10def main():
11 jeepBuilder = JeepBuilder()
12
13 director = Director()
14
15 print ("Jeep")
16 [Link](jeepBuilder)
17 jeep = [Link]()
18 [Link]()
19
20if __name__ == "__main__":
21 main()

Page 106 of 107


Deci, vom instaţia clasa JeepBuilder care definește ce părți avem și datele lor. După aceea, instanțiem
clasa Director și ei îi atribuim imediat clasa JeepBuilder, adică clasa care construiește o anumită mașină;
aceasta se face conform schemei din clasa Builder. În acest moment, avem toate piesele și specificațiile
lor - mai rămâne doar să se asambleze mașina. Prin urmare, apelăm din nou clasa Director și metoda ei
getCar care, conform algoritmului, asamblează toate părțile. După acest proces, avem o mașină
asamblată cu toate piesele, cu datele lor și dispunerea pieselor.
Acest lucru poate părea o mare complicație pentru dvs., deoarece avem mai multe clase, moșteniri și
altele asemenea. Dar adevărata putere a acestui șablon apare atunci când vrem să asamblăm o altă
mașină. Atunci este suficient să adăugați următorul cod:

1class NissanBuilder(Builder):
2
3 def getWheel(self):
4 wheel = Wheel()
5 [Link] = 16
6 return wheel
7
8 def getEngine(self):
9 engine = Engine()
10 [Link] = 85
11 return engine
12
13 def getBody(self):
14 body = Body()
15 [Link] = "hatchback"
16 return body
17
18nissanBuilder = NissanBuilder()
19 print "Nissan"
20 [Link](nissanBuilder)
21 nissan = [Link]()
22 [Link]()

Deci, folosim aceeași clasă Director, aceeași clasă Builder; avem nevoie doar de Concrete Builder pentru
o anumită mașină. Dacă nu am fi ales abordarea șablonului Builder, ar fi trebuit să duplicăm mai multe
metode și câmpuri pentru acest tip nou de mașină.
Acestea au fost cele mai cunoscute șabloane de proiectare. Cel mai important fapt din toată această
lecție este că nu ar trebui să învăţaţi nimic pe de rost, deoarece aceasta este ideea șablonului. Acestea
sunt sisteme gata făcute care schimbă doar scopul - indiferent dacă funcționează cu un fierbător, o
mașină, utilizatori... Nu contează. Întotdeauna rămâne logica; clasele care sunt necesare, modul în care
sunt transmise valorile, modul în care sunt moștenite clasele - totul rămâne la fel. Aşa cum am spus la
începutul lecției, în felul acesta eliminăm fenomenul „inventării apei calde” în crearea aplicației noastre,
accelerând semnificativ producția, mai ales dacă are loc într-un mediu de echipă.

Cu decenii de dezvoltare a limbajului de programare, comunitățile de programatori au conceput


constant modalități de a crea șabloane și de a accelera operațiunile complexe, care consumă mult prea
mult timp. Șabloane goale, deci fără exemple concrete, pot fi găsite în multe locații; una dintre ele este
şi: [Link]

La acest link puteți găsi zeci de șabloane în Python pe care le puteți pune în aplicația dvs. și pe care le
puteți completa cu logica necesară.

Page 107 of 107

S-ar putea să vă placă și