9. Clase¶
Clasele constituie o modalitate de a aduce laolaltă date și funcționalități. Atunci când creăm o clasă nouă, creăm un nou tip de obiecte, adică stabilim felul în care pot fi construite instanțe ale tipului respectiv. Fiecărei instanțe a unei clase oarecare îi putem atașa atribute în scopul păstrării stării instanței în cauză. Instanțele clasei pot dispune și de metode (definite în cadrul acelei clase), folosite la modificarea stărilor lor.
În comparație cu alte limbaje de programare, mecanismul claselor din Python adaugă clase (la familia de tipuri de date deja existente) cu un minim de sintaxă, respectiv de semantică (în sensul dat în teoria compilatoarelor). El este o mixtură a mecanismelor privitoare la clase pe care le găsim în C++ și în Modula-3. Clasele (din) Python posedă toate trăsăturile tipice pe care le cere Programarea orientată-obiect (adică, POO; se folosesc și constructele Programare orientată pe obiecte, respectiv Programare orientată înspre obiecte): mecanismul de moștenire a claselor îngăduie mai multe clase de bază (numite și superclase ori clase-părinte), orice clasă derivată își poate suprascrie (de la englezescul, în jargon, override) metodele moștenite de la una sau de la mai multe din clasele de bază, iar orice metodă (a unei anumite clase) poate apela metoda omonimă a uneia din clasele de bază (ale clasei respective). Obiectele pot conține cantități și tipuri oarecare de date. Aidoma modulelor, clasele împărtășesc natura dinamică a Python-ului: ele sunt create în timpul execuției (sau, în jargon informatic, la runtime) și pot fi modificate după ce au fost create.
Urmând terminologia C++, membrii obișnuiți ai unei clase (incluzând aici datele-membru; sunt utilizate și constructele datele membre, respectiv variabilele de instanță) sunt publici (cu excepția, vezi mai jos, Variabile private), pe când toate funcțiile-membru (ori funcțiile membre) sunt virtuale. La fel ca în Modula-3, nu există simplificări de notație care să deosebească membrii unui obiect de metodele obiectului: funcția metodă se declară cu primul argument (dat) explicit ca reprezentând obiectul, acesta urmând a fi transmis, în mod implicit, la apelul funcției. Precum în Smalltalk, clasele însele sunt obiecte. O atare particularitate aduce cu sine o semantică a importării și a redenumirii. Spre deosebire de C++ ori de Modula-3, tipurile predefinite (de date) pot fi întrebuințate drept clase de bază la extinderile realizate de către utilizator. De asemeni, ca în C++, majoritatea operatorilor predefiniți care au o sintaxă specială (cum ar fi operatorii aritmetici, operatorul de indexare șamd.) pot fi redefiniți pentru instanțele unor clase oarecare.
(Dată fiind lipsa unei terminologii unanim acceptate în discuțiile relative la clase, vom folosi ocazional formulări specifice C++-ului și Smalltalk-ului. Am fi întrebuințat termeni din Modula-3, căci semantica orientată-obiect a acestuia este mai apropiată de Python decât cea a C++-ului, dacă nu am bănui că prea puțini dintre cititori au auzit de acest limbaj.)
9.1. O vorbă despre nume și obiecte¶
Obiectele au individualitate și este permis ca nume diferite (din domenii de valabilitate distincte; de la englezescul scope; se folosește și constructul domenii de vizibilitate) să fie legate de același obiect. În alte limbaje de programare, o atare permisiune este cunoscută drept întrebuințare de pseudonime (de la englezescul aliasing; sau atribuire de pseudonime). Pseudonimele se folosesc rar de către cei aflați la primul contact cu Python-ul, utilizarea lor putând fi evitată în mod eficient atunci când avem de a face, în programul nostru, cu tipurile de date elementare care sunt imutabile (numere, șiruri de caractere, tupluri). Pe de altă parte, atribuirea de pseudonime poate avea efecte neașteptate asupra semanticii programelor Python care întrebuințează obiecte mutabile precum listele, dicționarele de date șamd. Pseudonimele îi sunt utile unui program Python deoarece se comportă ca niște pointeri în anumite privințe. De exemplu, transmiterea (ca argument al unei funcții oarecare) unui obiect este ieftină căci implementarea se îngrijește să fie transmis doar un pointer; iar dacă funcția va modifica obiectul transmis ei ca argument, atunci apelantul va observa schimbarea intervenită — ceea ce elimină nevoia de a dispune de două mecanisme de transmitere a argumentelor, ca în Pascal.
9.2. Domenii de valabilitate și spații de nume în Python¶
Înainte de a introduce clasele, trebuie să discutăm puțin despre regulile Python-ului în ceea ce privește domeniile de valabilitate. Definițiile claselor le joacă adesea feste celor care utilizează spațiile de nume (de la englezescul namespace), așa că este important să cunoașteți precis cum funcționează domeniile de valabilitate și spațiile de nume pentru a putea urmări îndeaproape cum evoluează lucrurile. Că tot a venit vorba, cunoașterea acestei problematici îi este utilă oricărui programator matur în Python.
Să începem cu câteva definiții.
Un spațiu de nume este o asociere (sau o corespondență; de la englezescul mapping; se folosește, ca jargon informatic, și termenul de mapare) a unor nume cu niște obiecte. Cele mai multe spații de nume sunt implementate în Python, la momentul de față, ca dicționare de date, însă respectiva implementare nu iese în evidență prin nimic (poate cu excepția performanței) și există posibilitatea ca ea să se schimbe în viitor. Exemple de spații de nume sunt următoarele: setul numelor predefinite (conținând funcții precum abs()
, respectiv nume de excepții predefinite); numele globale dintr-un modul; ori numele locale dintr-un apel de funcție. Lucrul important de reținut despre spațiile de nume este că nu există niciun fel de legătură între numele situate în spații de nume diferite; de exemplu, două module distincte pot defini fără pericol de confuzie (câte) o funcție intitulată maximizează
– ca să folosească o astfel de funcție, utilizatorii modulelor vor avea de prefixat numele ei cu numele modulului care o conține.
Apropo, folosim cuvântul atribut pentru a ne referi la orice nume care îi urmează unui (operator) punct — cum ar fi faptul că, în expresia z.real
, real
(partea reală) este un atribut al obiectului z
. Într-o exprimare riguroasă, referirea la numele dintr-un modul este o referire la niște atribute: în expresia nume_de_modul.nume_de_funcție
, nume_de_modul
desemnează numele unui modul pe când nume_de_funcție
pe cel al unui atribut al acestui modul. În cazul de față se întâmplă să existe o mapare ușor de sesizat între atributele modulului și numele globale definite în modul: ele împart același spațiu de nume! [1]
Atributele pot fi sau doar-de-citit sau editabile (de la englezescul writable). În cel de-al doilea caz, atributelor li se pot asigna valori. Atributele unui modul sunt editabile: putem scrie nume_de_modul.răspunsul = 42
. De asemeni, atributele editabile pot fi șterse, cu ajutorul instrucțiunii del
. De exemplu, del nume_de_modul.răspunsul
va elimina atributul răspunsul
din obiectul numit nume_de_modul
.
Spațiile de nume sunt create la momente de timp diferite și au durate de viață variate. Spațiul de nume care conține numele predefinite este creat la pornirea interpretorului de Python, fără a mai fi șters pe întreaga durată a funcționării acestuia. Spațiul de nume global al unui modul este creat odată cu citirea de către interpretor a definiției modulului în cauză; în mod obișnuit, spațiile de nume ale modulelor rămân în viață până la oprirea interpretorului. Instrucțiunile executate de o invocare de la cel mai înalt nivel a interpretorului, fie că au fost citite dintr-un fișier de script fie că au fost introduse în mod interactiv, sunt considerate că făcând parte dintr-un modul numit __main__
, astfel că ele posedă propriul lor spațiu de nume global. (Numele predefinite, la rândul lor, funcționează într-un modul; acesta se numește builtins
.)
Spațiul de nume local al unei funcții este creat la apelul funcției și este șters fie la returnarea din execuția codului funcției fie atunci când, în codul funcției, este ridicată o excepție care nu va fi interceptată în cadrul funcției respective. (De fapt, uitare ar fi termenul potrivit pentru a descrie ceea ce se întâmplă cu adevărat într-o atare situație.) Evident, invocările recursive ale unei funcții au fiecare propriul său spațiu de nume.
Un domeniu de valabilitate este o zonă de text dintr-un program Python în care un anumit spațiu de nume este accesibil în mod direct. Prin „accesibil în mod direct” înțelegem faptul că referințele necalificate la nume oarecare reușesc să identifice despre ce nume este vorba în acel spațiu de nume.
Cu toate că domeniile de valabilitate sunt stabilite static, utilizarea lor se face dinamic. La orice moment de timp de pe parcursul execuției unui program Python, există 3 sau 4 domenii de valabilitate imbricate la ale căror spații de nume avem acces în mod direct:
domeniul de valabilitate poziționat cel mai adânc în interiorul codului, acela din care va începe orice căutare de nume, el conține numele locale
domeniile de valabilitate ale funcțiilor care înglobează codul (de la englezescul enclosing), în care căutările de nume se realizează pornind de la domeniul de includere (al codului) situat cel mai adânc, acestea deținând atât nume ne-locale cât și nume ne-globale
penultimul (dinspre interiorul către exteriorul codului) domeniu de valabilitate conține numele globale ale modulului curent
domeniul de valabilitate cel mai de sus (ultimul în care se va căuta numele respectiv) este spațiul de nume care cuprinde numele predefinite
Atunci când un nume este declarat ca fiind global, toate referirile la el precum și toate atribuirile către el vor fi realizate în domeniul de valabilitate penultim, adică în domeniul care deține toate numele globale ale modulului. Pentru a lega din nou variabilele găsite în exteriorul celui mai adânc situat dintre domeniile de valabilitate poate fi întrebuințată instrucțiunea nonlocal
(adică, o declarație de ne-local); dacă nu au fost declarate drept ne-locale, atunci asemenea variabile sunt doar-de-citit (orice tentativă de a edita o atare variabilă va conduce la crearea unei variabile locale noi, conținută în domeniul de valabilitate poziționat cel mai adânc, în timp ce numele omonim din exterior nu-și va modifica valoarea).
Domeniul de valabilitate local se referă, în mod obișnuit, la numele locale ale (chiar) funcției de față. În exteriorul codului de funcții, domeniul de valabilitate local face referire la același spațiu de nume ca și domeniul de valabilitate global: adică la spațiul de nume al modulului. Definițiile claselor, la rândul lor, plasează alte spații de nume în domeniul de valabilitate local.
Este important să realizăm că domeniile de valabilitate se determină textual: domeniul de valabilitate global al unei funcții definite într-un modul este chiar spațiul de nume al modulului respectiv, indiferent de unde ori sub ce pseudonim s-a realizat apelul funcției în cauză. Pe de altă parte, căutarea efectivă a unui anumit nume se realizează dinamic, pe parcursul execuției programului — cu toate acestea, definiția Python-ului ca limbaj de programare evoluează către o rezolvare statică a numelor, adică una la momentul „compilării”, motiv pentru care vă recomandăm să nu vă bazați pe rezolvări dinamice de nume! (În fapt, variabilele locale sunt deja determinate static.)
O caracteristică aparte a Python-ului – atunci când nu ne găsim sub efectul vreuneia din instrucțiunile global
ori nonlocal
– este aceea că atribuirile către nume se fac întotdeauna în domeniul de valabilitate poziționat cel mai adânc în interiorul codului. Atribuirile nu copiază date — ele doar leagă nume de obiecte. Același lucru este valabil și pentru ștergeri: instrucțiunea del x
elimină legătura lui x
din spațiul de nume la care se referă domeniul de valabilitate local. În fapt, orice operație care introduce nume noi întrebuințează domeniul de valabilitate local: în particular, instrucțiunile import
precum și definițiile de funcții leagă fie numele modulului fie numele funcției de domeniul de valabilitate local.
Instrucțiunea global
poate fi utilizată la a arăta că o anume variabilă trăiește în domeniul de valabilitate global, astfel că trebuie re-legată (tot) în el; instrucțiunea nonlocal
indică faptul că variabila respectivă trăiește într-un domeniu de valabilitate care înglobează codul de față, deci că va trebui re-legată în acela.
9.2.1. Un exemplu cu domenii de valabilitate și spații de nume¶
Acesta este un exemplu care ilustrează cum trebuie referențiate diversele domenii de valabilitate și spațiile de nume, respectiv cum afectează cuvintele-cheie global
și nonlocal
legarea unei variabile oarecare:
def testează_domeniul_de_vizibilitate():
def construiește_ceva_local():
șuncă = "șuncă produsă local"
def construiește_ceva_nelocal():
nonlocal șuncă
șuncă = "șuncă produsă ne-local"
def construiește_ceva_global():
global șuncă
șuncă = "șuncă produsă global"
șuncă = "testez șunca"
construiește_ceva_local()
print("După asignarea locală:", șuncă)
construiește_ceva_nelocal()
print("După asignarea ne-locală:", șuncă)
construiește_ceva_global()
print("După asignarea globală:", șuncă)
testează_domeniul_de_vizibilitate()
print("În domeniul de valabilitate global:", șuncă)
Rezultatul rulării codului din exemplu este următorul:
După asignarea locală: testez șunca
După asignarea ne-locală: șuncă produsă ne-local
După asignarea globală: șuncă produsă ne-local
În domeniul de valabilitate global: șuncă produsă global
Remarcați că atribuirea local (deci, cea standard) nu a modificat legarea lui șuncă dată de testează_domeniul_de_vizibilitate. În schimb, atribuirea nonlocal
a schimbat legarea lui șuncă dată de testează_domeniul_de_vizibilitate, respectiv atribuirea global
a modificat legarea la nivel de modul.
În plus, puteți observa că șuncă nu a fost legat în niciun fel până la momentul asignării global
.
9.3. Prima privire aruncată asupra claselor¶
Clasele aduc un strop de nou în conținutul sintaxei, alte trei tipuri de obiecte, precum și ceva noutăți la semantică.
9.3.1. Sintaxa definiției unei clase¶
Cea mai simplă formă a definiției unei clase arată astfel:
class NumeleClasei:
<instrucțiunea-1>
.
.
.
<instrucțiunea-N>
Definițiile de clase, aidoma definițiilor de funcții (instrucțiuni def
) trebuie executate mai întâi pentru a avea efect. (Dat fiind că nu este exclus să plasați o definiție de clasă într-una din ramificațiile unei instrucțiuni if
, după cum nu este imposibil nici să o inserați în codul vreunei funcții.)
În practică, instrucțiunile din codul definiției unei clase vor fi mai ales definiții de funcții, cu toate că sunt permise și altfel de instrucțiuni, și încă cu mult folos — vom reveni la aceasta mai târziu. Definițiile de funcții din interiorul (codului) unei clase impun (de obicei) o formă neobișnuită a listei de argumente, formă dictată de convențiile de apel ale metodelor — și aceste aspecte vor fi explicate ulterior.
Atunci când interpretorul citește definiția unei clase, un spațiu de nume (nou) va fi creat și întrebuințat ca domeniu de valabilitate local — astfel, toate atribuirile către variabile locale vor fi realizate în cadrul acestui (nou) spațiu de nume. În particular, definițiile de funcții leagă fiecare nume nou de funcție de acest domeniu de valabilitate.
Atunci când interpretorul părăsește în mod normal codul unei definiții de clasă (deci, după ce a ajuns la finalul acestui cod), va fi creat un obiect clasă. Acesta este, în fapt, o împachetare (sau o învelitoare; de la englezescul wrapper) a conținutului acelui spațiu de nume creat de însăși definiția clasei; vom afla mai multe despre obiectele clasă în secțiunea următoare. Domeniul de valabilitate local în care ne aflam (adică, domeniul vizibil înainte ca interpretorul să ajungă la definiția clasei) este reinstaurat iar obiectul clasă este legat de numele clasei dat în antetul definiției acesteia (NumeleClasei
din exemplul nostru).
9.3.2. Obiectele clasă¶
Obiectele clasă permit două feluri de operații: referirea la atribute și instanțierea.
Referirea la atribute (de clasă; sau referențierea atributelor ori referința la atribute) întrebuințează sintaxa tipică din Python a referirii la atribute oarecare: obiect.nume
. Numele valide de atribute alcătuiesc întreg ansamblul de nume din spațiul de nume al clasei la momentul creării obiectului clasă. Astfel, dacă definiția clasei arată ca mai jos:
class ClasaMea:
"""Un exemplu simplist de clasă"""
i = 12345
def f(self):
return 'salutare, lume'
atunci ClasaMea.i
și ClasaMea.f
sunt referiri valide la atribute, care returnează un număr întreg, respectiv un obiect funcție. Atributelor de clasă le putem realiza asignări, așa că valoarea lui ClasaMea.i
poate fi modificată prin atribuire. Și __doc__
este un atribut valid, returnând docstring-ul clasei: "Un exemplu simplist de clasă"
.
Instanțierea unei clase utilizează notația cu operatorul funcție. Ne putem închipui că obiectul clasă este o funcție fără parametri care întoarce o (nouă) instanță a clasei. De exemplu (folosind clasa de mai sus):
x = ClasaMea()
creează o instanță nouă a clasei și îi atribuie obiectul nou creat variabilei locale x
.
Operația de instanțiere (adică de „apelare” a unui obiect clasă; de la englezescul instantiation) creează un obiect gol (vid). Pentru multe clase utilizate în practică se dorește crearea de instanțe particularizate printr-o stare inițială specificată (de către utilizator). Din acest motiv, unei clase îi poate fi definită o metodă specială, numită __init__()
, după cum urmează:
def __init__(self):
self.datele_clasei = []
Atunci când în codul unei clase este definită (și) metoda __init__()
, instanțierea clasei respective va invoca în mod automat această metodă pentru nou creata instanță a clasei. Așadar, în exemplul nostru, o instanță nouă, inițializată, poate fi obținută prin:
x = ClasaMea()
Firește, metoda __init__()
poate primi argumente, această caracteristică sporindu-i flexibilitatea în utilizare. Într-o atare situație, argumentele date operatorului de instanțiere a clasei îi vor fi transmise lui __init__()
. Astfel,
>>> class NumărComplex:
... def __init__(self, partea_reală, partea_imaginară):
... self.real = partea_reală
... self.imaginar = partea_imaginară
...
>>> x = NumărComplex(3.0, -4.5)
>>> x.real, x.imaginar
(3.0, -4.5)
9.3.3. Obiectele instanță¶
Odată ajunși aici, la ce putem întrebuința aceste obiecte instanță? Singurele operații înțelese de obiectele instanță sunt referențierile de atribute. Există două tipuri de nume de atribute valide: atributele datelor (sau atributele de date) și metodele (adică, atributele de funcții).
atributele de date le corespund „variabilelor de instanță” din Smalltalk, respectiv „membrilor date” din C++. Atributele (de) date nu au nevoie să fie declarate; asemenea variabilelor locale, ele prind viață (sunt vivificate) doar atunci când li se face o atribuire pentru prima oară. De exemplu, dacă x
este instanța lui ClasaMea
creată mai sus, atunci următorul fragment de cod va produce afișarea lui 16
, fără să lase urme (în starea viitoarea a lui x
):
x.contor = 1
while x.contor < 10:
x.contor = x.contor * 2
print(x.contor)
del x.contor
Celălalt fel de referențiere de atribute de care dispune instanța este metoda. O metodă este o funcție care „îi aparține” unui obiect.
Numele valide de metode ale unui obiect instanță depind de clasa acestui obiect instanță. Prin definiție, toate atributele unei clase care sunt obiecte funcție definesc metodele corespunzătoare (omonime) ale instanțelor clasei. Așa că, în exemplul nostru, x.f
este o referire validă la o metodă, deoarece ClasaMea.f
este funcție, pe când x.i
nu este validă ca referire la vreo metodă pentru că ClasaMea.i
nu este funcție. Atenție, x.f
nu este același lucru cu ClasaMea.f
— primul este un obiect metodă, nu un obiect funcție.
9.3.4. Obiectele metodă¶
Adesea, o metodă va fi apelată de îndată ce a fost legată (de un anumit obiect):
x.f()
În exemplul cu ClasaMea
, un atare apel va returna șirul de caractere 'salutare, lume'
. Pe de altă parte, nu este nevoie să apelăm metodele imediat: cum x.f
este un obiect metodă, el poate fi stocat undeva și apelat la momentul dorit. De exemplu:
xf = x.f
while True:
print(xf())
va afișa salutare, lume
fără să se mai oprească din a da binețe.
Ce se întâmplă, cu adevărat, atunci când se apelează o metodă? Probabil că ați remarcat faptul că x.f()
a fost apelat, în codul de mai sus, fără să i se dea niciun argument, chiar dacă definiția lui f()
specifica un anume argument. Oare ce s-a întâmplat cu acest argument? Doar știm că Python-ul va lansa o excepție atunci când vreo funcție care cerere argument este apelată fără niciun argument — chiar dacă argumentul nici măcar nu urmează să fie folosit…
Păi, se prea poate să fi ghicit, deja, răspunsul: un lucru special privind metodele este acela că obiectul instanță îi este transmis metodei ca prim argument al său. În exemplul nostru, apelul x.f()
este absolut echivalent cu ClasaMea.f(x)
. Practic, a apela o metodă cu o listă de n argumente este totuna cu a apela funcția corespondentă a acestei metode cu o listă de argumente formată prin inserarea obiectului instanță (de care aparține metoda) înaintea primului din cele n argumente.
În general, metodele funcționează după cum urmează. Atunci când un atribut non-dată este referențiat, va fi căutată instanța clasei (sale). Dacă numele (atributului) denotă un atribut valid al clasei iar acest atribut este un obiect funcție, atunci referirile (referințele) respective, atât la obiectul instanță cât și la obiectul funcție, vor fi împachetate într-un obiect metodă. Dacă obiectul metodă este apelat cu o listă de argumente, atunci o nouă listă de argumente va fi construită din obiectul instanță și din elementele listei de argumente în cauză, obiectul funcție corespunzător fiind apelat cu această (nou formată) listă de argumente.
9.3.5. Variabile de clasă și variabile de instanță¶
În jargon POO, variabilele de instanță se referă la datele care îi sunt unice fiecărei instanțe, respectiv variabilele de clasă se referă la atributele și metodele comune tuturor instanțelor clasei respective:
class Câine:
genul = 'canis' # variabilă de clasă comună tuturor
# instanțelor
def __init__(self, numele):
self.numele = numele # variabilă de instanță, unică fiecărei
# instanțe
>>> d = Câine('Fido')
>>> e = Câine('Amicu\'')
>>> d.genul # comun tuturor câinilor
'canis'
>>> e.genul # comun tuturor câinilor
'canis'
>>> d.numele # doar al lui d
'Fido'
>>> e.numele # doar al lui e
"Amicu'"
Precum spuneam în O vorbă despre nume și obiecte, organizarea datelor puse în comun (comune tuturor instanțelor unei clase; sau partajate de către toate instanțele unei clase; de la englezescul shared) poate produce efecte surprinzătoare dacă folosim la o atare acțiune obiecte mutable, precum listele ori dicționarele de date. De exemplu, lista trucuri din codul de mai jos n-ar fi trebuit întrebuințată pe post de variabilă de clasă și aceasta pentru că va exista o singură listă ce va fi folosită la comun de către toate instanțele Câine:
class Câine:
trucuri = [] # folosită din greșeală ca variabilă de clasă
def __init__(self, numele):
self.numele = numele
def adauga_trucul(self, trucul):
self.trucuri.append(trucul)
>>> d = Câine('Fido')
>>> e = Câine('Amicu\'')
>>> d.adaugă_trucul('rostogol')
>>> e.adaugă_trucul('mort')
>>> d.trucuri # folosință la comun neașteptată
['rostogol', 'mort']
O proiectare corectă a clasei ar trebui să folosescă, în locul unei variabile de clasă, o variabilă de instanță:
class Câine:
def __init__(self, numele):
self.numele = numele
self.trucuri = [] # creează câte o listă goală
# pentru fiecare câine
def adaugă_trucul(self, trucul):
self.trucuri.append(trucul)
>>> d = Câine('Fido')
>>> e = Câine('Amicu\'')
>>> d.adaugă_trucul('rostogol')
>>> e.adaugă_trucul('mort')
>>> d.trucuri
['rostogol']
>>> e.trucuri
['mort']
9.4. Observații diverse¶
Dacă același nume de atribut va apărea și într-o instanță și într-o clasă, la găsirea atributului i se va da prioritate instanței:
>>> class Depozitul:
... scopul = 'înmagazinare'
... regiunea = 'vestică'
...
>>> d1 = Depozitul()
>>> print(d1.scopul, d1.regiunea)
înmagazinare vestică
>>> d2 = Depozitul()
>>> d2.regiunea = 'estică'
>>> print(d2.scopul, d2.regiunea)
înmagazinare estică
Atributele de date pot fi referențiate atât de metode cât și de utilizatorii obișnuiți („clienții”) ai obiectului (având respectivele atribute). Cu alte cuvinte, clasele sunt inutilizabile la implementarea unor tipuri de date abstracte pure. Practic, nimic din Python nu ne poate ajuta să asigurăm ascunderea datelor — totul se bazează pe o simplă convenție. (Pe de altă parte, implementarea de față a Python-ului, scrisă în C, poate să mascheze în totalitate detaliile de implementare, respectiv să controleze accesul la un anumit obiect, atunci când este nevoie de așa ceva; o asemenea capacitate poate fi întrebuințată de diversele extensii ale Python-ului scrise în C.)
Clienții trebuie să folosească atributele de date cu grijă — căci ei pot strica invarianții păstrați de metode dacă își vor pune amprenta pe atributele de date ale acestora. Nu uitați că clienții îi pot adăuga atribute după bunul lor plac unui obiect instanță fără ca prin aceasta să le afecteze validitatea metodelor, atâta timp cât se evită conflictele de nume — și aici, întrebuințarea unei convenții de nume ne poate scăpa de o grămadă de bătăi de cap.
Nu ni se pune la dispoziție nicio scurtătură (și nici alte tehnici!) atunci când referențiem atribute de date din interiorul unei metode. Trebuie spus că o astfel de caracteristică a Python-ului crește lizibilitatea codului oricărei metode: deoarece nu avem nicio șansă să confundăm variabilele locale cu variabilele de instanță atunci când ne aruncăm privirea peste instrucțiunile dintr-o metodă.
Adeseori, primul argument al unei metode este numit self
. Aceasta nu este decât o convenție: numele self
nu posedă nicio semnificație aparte pentru Python. Remarcați, însă, că dacă nu vă veți face obiceiul de a-l folosi, atunci s-ar putea întâmpla ca programul dumneavoastră să le provoace dificultăți la lectură altor programatori în Python, după cum s-ar putea și să aveți greutăți cu vreo aplicație cititor de clase care se bazează tocmai pe această convenție de nume.
Orice obiect funcție care este (și) atribut de clasă definește o metodă pentru instanțele clasei în cauză. Nu este obligatoriu ca definiția funcției respective să fie literalmente parte din definiția clasei: este suficient să îi asignăm unei variabile locale din clasă un obiect funcție. De exemplu:
# Funcție definită în afara clasei
def f1(self, x, y):
return min(x, x+y)
class C:
f = f1
def g(self):
return 'salutare, lume'
h = g
Aici, f
, g
și h
îi sunt cu toatele atribute clasei C
și se referă la obiecte funcție, din care motiv ele le vor fi metode tuturor instanțelor lui C
— h
nefiind nimic altceva decât g
. Atenție la faptul că o astfel de abordare va reuși doar să-i provoace confuzii vreunui cititor al programului dumneavoastră.
Metodele pot apela alte metode folosind atributele de metodă ale argumentului self
:
class Punga:
def __init__(self):
self.datele = []
def adaugă(self, x):
self.datele.append(x)
def adaugă_dublu(self, x):
self.adaugă(x)
self.adaugă(x)
Metodele se pot referi la numele globale în același fel ca funcțiile obișnuite. Domeniul de valabilitate global care i se asociază unei metode este chiar modulul care conține definiția metodei. (O clasă, ca atare, nu se folosește niciodată pe post de domeniu de valabilitate global.) Chiar dacă nu vom avea decât arareori motive serioase să întrebuințăm date globale în vreo metodă, există numeroase utilizări legitime ale domeniului de valabilitate global: cum ar fi, funcțiile și modulele importate în domeniul de valabilitate global pot fi folosite atât de către metode cât și de către funcțiile ori clasele care au fost definite în el. În mod obișnuit, clasa care conține o anumită metodă este ea însăși definită în acest domeniu de valabilitate global, iar noi vom face cunoștință în secțiunea următoare cu mai multe motive întemeiate ca o metodă oarecare să dorească să se refere la propria sa clasă.
Orice valoare este, în sine, un obiect și dispune, ca atare, de o clasă (aceasta mai numindu-se și tipul valorii). Clasa respectivă este stocată drept obiectul_nostru.__class__
.
9.5. Moștenirea¶
Așa cum se înțelege de la sine, o caracteristică a indiferent cărui limbaj de programare n-ar putea fi socotită „de clasă” dacă nu le-ar face față cum se cuvine moștenirilor (dificultăților…). În cazul Python-ului, sintaxa pentru definirea unei clase derivate (dintr-o clasă de bază) arată după cum urmează:
class NumeleClaseiDerivate(NumeleClaseiDeBază):
<instrucțiunea-1>
.
.
.
<instrucțiunea-N>
Numele NumeleClaseiDeBază
trebuie să fie definit într-un spațiu de nume ce poate fi accesat din domeniul de valabilitate în care a fost introdusă definiția clasei derivate despre care discutăm. Sunt permise, în locul unui nume de clasă cu rolul de clasă de bază, (și) expresii oarecare. O astfel de flexibilitate în definiții se va dovedi folositoare atunci când, de exemplu, clasa de bază este situată în alt modul:
class NumeleClaseiDerivate(nume_de_modul.NumeleClaseiDeBază):
Execuția (codului) unei definiții de clasă derivată se realizează în același fel cu execuția (codului) unei clase de bază. Atunci când este construit obiectul clasă (derivată), interpretorul își amintește (și) de clasa de bază. Această proprietate se întrebuințează la rezolvarea referințelor de atribute: dacă vreun atribut căutat nu se găsește în codul clasei (derivate), căutarea sa va trece la codul clasei de bază. O atare regulă va fi aplicată recursiv dacă clasa de bază se dovedește a fi, la rândul său, derivată din altă clasă.
Nimic deosebit nu se va întâmpla la instanțierea claselor derivate: ca și până acum, NumeleClaseiDerivate()
va crea o instanță (nouă) a clasei respective. Referințele la metode sunt rezolvate astfel: se caută atributul de clasă corespunzător, coborând, la nevoie, pe lanțul (dependențelor) care duce până la clasele de bază, apoi, odată găsit numele, referința metodei este considerată validă dacă ea întoarce un obiect funcție.
Clasele derivate au dreptul să-și suprascrie metodele (moștenite) de la clasele de bază. Deoarece metodele nu beneficiază de privilegii anume atunci când apelează metode ale aceluiași obiect, o metodă aparținând unei clase de bază care apelează altă metodă definită în aceeași clasă de bază poate avea surpriza că a apelat, de fapt, metoda unei clase derivate care a suprascris metoda (teoretic) apelată. (Pentru programatorii în C++: toate metodele din Python sunt efectiv virtuale
.)
O metodă suprascrisă într-o clasă derivată dorește, cel mai adesea, să extindă și nu să înlocuiască pur și simplu metoda omonimă din clasa de bază. Dispunem de un procedeu necomplicat pentru a apela în mod direct metoda unei clase de bază: apelați, de-a dreptul, NumeleClaseiDeBază.numele_metodei(self, restul_argumentelor)
. O atare proprietate le folosește și clienților. (Țineți seama de faptul că ea funcționează doar atunci când clasa de bază este accesibilă sub denumirea de NumeleClaseiDeBază
în domeniul de valabilitate global.)
Python-ul are două funcții predefinite care conlucrează cu moștenirea:
Utilizați
isinstance()
pentru a verifica tipul unei instanțe:isinstance(obiectul_în_cauză, int)
va întoarceTrue
dacă și numai dacăobiectul_în_cauză.__class__
este fieint
fie altă clasă derivată dinint
.Întrebuințați
issubclass()
pentru a testa moștenirea unei clase:issubclass(bool, int)
va returnaTrue
pentru căbool
este o subclasă (adică, o moștenitoare; sau o urmașă) a luiint
. În schimb,issubclass(float, int)
va întoarceFalse
dat fiind căfloat
nu îi este subclasă luiint
.
9.5.1. Moștenirea multiplă¶
Python-ul permite și o anumită formă de moștenire multiplă. Definiția unei clase care moștenește mai multe clase de bază arată în felul următor:
class NumeleClaseiDerivate(ClasaDeBază1, ClasaDeBază2, ClasaDeBază3):
<instrucțiunea-1>
.
.
.
<instrucțiunea-N>
În cele mai multe dintre situațiile obișnuite (a se citi simple), ne putem imagina căutarea unui atribut moștenit de la o clasă părinte ca efectuându-se în adâncime (de la englezescul, în jargon informatic, depth-first), de la stânga la dreapta, fără să se caute de două ori în aceeași clasă în cazul unor suprapuneri în ierarhie (adică, în ansamblul claselor legate între ele prin mecanismele moștenirii). Astfel, dacă atributul căutat nu este găsit în NumeleClaseiDerivate
, el va fi căutat (mai întâi) în clasa de bază ClasaDeBază1
, apoi (recursiv) în clasele de bază ale lui ClasaDeBază1
, după care, dacă nu a fost găsit încă, va fi căutat în ClasaDeBază2
șamd.
În realitate, procedurile sunt oarecum mai complexe decât schița anterioară; ordinea (căutărilor) în procedura de rezolvare a unei metode se schimbă în mod dinamic pentru a permite apeluri cooperatiste ale lui super()
. O atare abordare este cunoscută în vocabularul unor limbaje de programare care implementează moștenirea multiplă drept apelul metodei următoare și dovedește mai multă eficacitate decât apelul lui super utilizat în limbajele de programare care întrebuințează doar moștenirea simplă.
Ordonarea dinamică este necesară pentru că în toate cazurile de moștenire multiplă survin relațiile în formă de diamant dintre clase (în care cel puțin una din clasele părinte va putea fi accesată de către măcar o clasă urmaș prin cel puțin două drumuri care să plece din clasa părinte, să parcurgă un subset de muchii ale arborelui moștenirii și să se încheie la clasa urmaș respectivă). De exemplu, toate clasele moștenesc clasa object
, din care motiv, în cazul unei moșteniri multiple, vor exista mai multe căi de acces la object
. Pentru a împiedica accesul la indiferent care clasă de bază pe mai multe căi (deci de mai multe ori într-o singură căutare), algoritmul dinamic de căutare va liniariza ordinea acesteia în așa fel încât să fie păstrată ordonarea de la stânga la dreapta specificată pentru fiecare clasă, ordonare care apelează fiecare părinte o singură dată și care este monotonă (adică, în raport cu care o clasă poate fi făcută subclasă a cuiva fără să fie afectată ordonarea realizată până la momentul respectiv a strămoșilor ei). Luate împreună, aceste particularități ale Python-ului ne permit să proiectăm ierarhii de clase stabile și ușor de extins în care să avem moștenire multiplă. Pentru mai multe detalii, a se vedea The Python 2.3 Method Resolution Order.
9.6. Variabile private¶
În Python nu există variabile de instanță „private”, adică variabile care să nu poată fi accesate decât din interiorul instanței respective. Cu toate acestea, dispunem de o convenție respectată de aproape întreg codul Python: orice nume prefixat de o bară jos (adică, de o liniuță de subliniere; precum în _șuncă
) trebuie socotit drept parte non-publică a API-ului (indiferent dacă este vorba de o funcție, de o metodă ori de o dată membru). Numele respectiv trebuie, așadar, tratat cu grija pe care o avem pentru un detaliu de implementare, despre care știm că s-ar putea schimba oricând, fără să fim anunțați.
Deoarece există, în practica POO, un caz de întrebuințare valid pentru membrii privați ai unei clase (mai precis, procedeul prin care să evităm conflictele de nume cu numele definite de clasele urmaș), avem la îndemână în Python un suport restrâns pentru un atare mecanism, denumit transformare de nume (de la englezescul name mangling). Astfel, orice identificator de forma __șuncă
(cel puțin două bare jos pe post de prefixe și cel mult o bară jos pe post de sufix) va fi înlocuit în textul programului cu _numeleclasei__șuncă
, unde numeleclasei
desemnează numele de clasă curent, cu prefixul ori prefixele eliminat(e). Această transformare de nume se va realiza indiferent de poziția sintactică a identificatorului, atâta timp când aceasta se găsește în interiorul unei definiții de clasă.
Vezi și
Specificațiile transformărilor de nume private pentru detalii și cazuri speciale.
Transformarea de nume este utilă la a le permite subclaselor să suprascrie diverse metode fără să strice apelurile de metode intra-clasă. Ca exemplu:
class Mapare:
def __init__(self, iterabilul):
self.lista_de_itemi = []
self.__actualizează(iterabilul)
def actualizează(self, iterabilul):
for itemul in iterabilul:
self.lista_de_itemi.append(itemul)
__actualizează = actualizează # copie privată a metodei
# actualizează() originale
class SubclasaLuiMapare(Mapare):
def actualizează(self, cheile, valorile):
# îi oferă o nouă signatură lui actualizează()
# fără să-l strice pe __init__()
for itemul in zip(cheile, valorile):
self.lista_de_itemi.append(itemul)
Exemplul de deasupra va funcționa chiar și în cazul când în SubclasaLuiMapare
am introduce un identificator __actualizează
și aceasta pentru că identificatorul în cauză va fi înlocuit cu _Mapare__actualizează
în clasa Mapare
, respectiv cu _SubclasaLuiMapare__actualizează
în clasa SubclasaLuiMapare
.
Nu uitați că regulile de transformare ale numelor au fost proiectate mai ales pentru a ne feri de accidente; este posibil, în pofida lor, să accesați, respectiv să modificați orice variabilă socotită drept privată. O asemenea libertate ne poate fi de folos în circumstanțe deosebite, precum cele ale întrebuințării unui depanator de programe (de la englezescul debugger).
Țineți seama (și) de faptul că, indiferent de cod îi transmiteți fie lui exec()
fie lui eval()
, acesta nu va considera numele de clasă al clasei care a făcut invocarea (uneia din cele două funcții) drept numele clasei curente; o atare situație seamănă cu efectul produs de execuția instrucțiunii global
, efect care este restrâns doar la codul compilat într-un singur fragment de cod-de-octeți. Aceeași restricție le privește și pe getattr()
, setattr()
și delattr()
, precum și pe orice referențiere directă a lui __dict__
.
9.7. Șurubărie¶
Ne servește uneori să avem la îndemână un tip de date asemănător celui numit „record” în Pascal, respectiv lui „struct” din C, tip care să pună laolaltă mai mulți itemi de date cu denumiri (individuale). Procedeul idiomatic pentru așa ceva (în Python) este dat de dataclasses
:
from dataclasses import dataclass
@dataclass
class Angajatul:
numele: str
departamentul: str
salariul: int
>>> popescu = Angajatul('popescu', 'sala de calculatoare', 1000)
>>> popescu.departamentul
'sala de calculatoare'
>>> popescu.salariul
1000
Unui fragment de program Python care se așteaptă să primească un anumit tip de date abstracte îi putem transmite, de cele mai multe ori, pe post de înlocuitor o clasă care emulează metodele tipului de date respectiv. De exemplu, admițând că dispunem de o funcție care formatează diverse date preluate dintr-un obiect fișier, putem defini mai întâi o clasă căreia să-i aparțină metodele read()
și readline()
, metode capabile să extragă datele dintr-o memorie-tampon (de la englezescul buffer) dedicată stocării (temporare a) șirurilor de caractere, după care îi putem transmite funcției de formatare această clasă în calitate de argument.
Obiectele metodă de instanță posedă, și ele, atribute: metoda.__self__
este obiectul instanță căruia îi aparține metoda metoda()
, respectiv metoda.__func__
este obiectul funcție care îi corespunde metodei în cauză (adică, lui metoda()
).
9.8. Iteratori¶
Deja ați remarcat, probabil, că majoritatea obiectelor container pot fi inspectate în mod iterativ (de la englezescul, în jargon informatic, loop over; vom întrebuința și constructele iterate de-a lungul, respectiv iterate în lung) folosind o instrucțiune de ciclare for
:
for elementul in [1, 2, 3]:
print(elementul)
for elementul in (1, 2, 3):
print(elementul)
for cheia in {'unu':1, 'doi':2}:
print(cheia)
for cheia in "123":
print(cheia)
for rândul in open("fișierul_meu_text.txt"):
print(rândul, end='')
Acest stil de acces la itemi este clar, concis și convenabil. Utilizarea iteratorilor constituie o practică sistematică, cu caracter unificator, în Python. În culise, instrucțiunea for
face apel la iter()
având drept argument obiectul container. Funcția va întoarce un obiect iterator, care definește metoda __next__()
, metodă ce accesează în mod secvențial elementele containerului respectiv. Atunci când nu mai întâlnește niciun item (al containerului), __next__()
va lansa o excepție StopIteration
pentru a-i spune buclei for
să se încheie. Puteți apela metoda __next__()
cu ajutorul funcției prestabilite next()
; exemplul următor vă dezvăluie cum trebuie procedat:
>>> șirul = 'abc'
>>> iteratorul = iter(șirul)
>>> iteratorul
<str_iterator object at 0x10c90e650>
>>> next(iteratorul)
'a'
>>> next(iteratorul)
'b'
>>> next(iteratorul)
'c'
>>> next(iteratorul)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
next(iteratorul)
StopIteration
Le putem acum, dat fiind că tocmai am văzut mecanismul prin care se pune în practică protocolul iteratorilor, adăuga un comportament de iterator claselor noastre. Este suficient să definim o metodă __iter__()
, care să întoarcă un obiect ce posedă o metodă __next__()
. În caz că clasa noastră îl definește ea însăși pe __next__()
, metoda __iter__()
nu mai are de făcut decât să-l returneze pe self
(apelantului):
class ÎntorsulPeDos:
"""Un iterator pentru a itera de-a lungul unei secvențe,
în sens invers."""
def __init__(self, datele):
self.datele = datele
self.indexul = len(datele)
def __iter__(self):
return self
def __next__(self):
if self.indexul == 0:
raise StopIteration
self.indexul = self.indexul - 1
return self.datele[self.indexul]
>>> reversul = ÎntorsulPeDos('arca')
>>> iter(reversul)
<__main__.ÎntorsulPeDos object at 0x00A1DB50>
>>> for caracterul in reversul:
... print(caracterul)
...
a
c
r
a
9.9. Generatori¶
Generatorii sunt o unealtă puternică, ușor de folosit la construcția de iteratori. Codul Python al generatorilor este asemeni celui al unei funcții obișnuite cu excepția faptului că întrebuințează instrucțiunea yield
(în loc de return) ori de câte ori trebuie întoarse date (apelantului). De fiecare dată când un generator va fi (re)folosit drept argument al lui next()
, el își va continua activitatea (dat fiind că un generator își amintește toate valorile datelor precum și ultima instrucție executată de la apelul precedent al lui next). Exemplul dat în continuare vă arată că generatorii sunt banal de creat:
def întoarce_pe_dos(datele):
for indexul in range(len(datele)-1, -1, -1):
yield datele[indexul]
>>> for caracterul in intoarce_pe_dos('trop'):
... print(caracterul)
...
p
o
r
t
Orice poate fi realizat folosind iteratorii bazați (explicit) pe clase, vezi cele prezentate în secțiunea anterioară, poate fi obținut și cu generatori. Ceea ce face codul acestora din urmă atât de compact este faptul că metodele __iter__()
și __next__()
sunt create automat.
Altă trăsătură cheie a generatorilor este aceea că atât variabilele locale cât și starea de la momentul execuției sunt salvate automate între apeluri. Din acest motiv, codul funcției (generator) este mai ușor de scris și mult mai clar în exprimarea ideilor decât ceea ce putem realiza cu o abordare (complicată) în care avem de folosit variabile de instanță precum self.indexul
și self.datele
.
În afară de creatul automat de metode și de salvatul stării programului, generatorii au proprietatea că, atunci când își termină iterarea, vor lansa automat o StopIteration
. Luate împreună, aceste caracteristici ne permit să construim iteratori cu același efort ca la scrierea codului unei funcții oarecare.
9.10. Expresii generator¶
Generatorii simpli pot fi introduși succint în codul Python ca expresii (generator) a căror sintaxă seamănă cu cea de la comprehensiunea listelor, excepție făcând faptul că acolo erau folosite paranteze drepte pe când aici vom întrebuința paranteze rotunde. Astfel de expresii sunt proiectate pentru situații când este nevoie de un generator (simplu) care să fie utilizat de îndată (ce a fost citit de interpretor) de către funcția în codul căreia a fost inserat. Expresiile generator sunt (încă și) mai compacte deși mai puțin versatile decât definițiile complete de generatori, respectiv tind să fie mai prietenoase în privința consumului de memorie decât comprehensiunile de liste corespondente.
Exemple:
>>> sum(i*i for i in range(10)) # sumă de pătrate perfecte
285
>>> vectorul_x = [10, 20, 30]
>>> vectorul_y = [7, 5, 3]
>>> sum(x*y for x,y in
... zip(vectorul_x, vectorul_y)) # produs scalar
260
>>> cuvinte_unice = set(cuvântul for rândul in pagina
... for cuvântul in rândul.split())
>>> șef_de_promoție = max((studentul.media_generală, studentul.numele)
... for studentul in absolvenți)
>>> datele = 'trop'
>>> list(datele[i] for i in range(len(datele)-1, -1, -1))
['p', 'o', 'r', 't']
Note de subsol