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]

Attributes may be read-only or writable.  In the latter case,
assignment to attributes is possible.  Module attributes are writable:
you can write "modname.the_answer = 42".  Writable attributes may also
be deleted with the "del" statement.  For example, "del
modname.the_answer" will remove the attribute "the_answer" from the
object named by "modname".

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 scope_test():
       def do_local():
           spam = "local spam"

       def do_nonlocal():
           nonlocal spam
           spam = "nonlocal spam"

       def do_global():
           global spam
           spam = "global spam"

       spam = "test spam"
       do_local()
       print("After local assignment:", spam)
       do_nonlocal()
       print("After nonlocal assignment:", spam)
       do_global()
       print("After global assignment:", spam)

   scope_test()
   print("In global scope:", spam)

Rezultatul rulării codului din exemplu este următorul:

   After local assignment: test spam
   After nonlocal assignment: nonlocal spam
   After global assignment: nonlocal spam
   In global scope: global spam

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 ClassName:
       <statement-1>
       .
       .
       .
       <statement-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.

When a class definition is left normally (via the end), a *class
object* is created.  This is basically a wrapper around the contents
of the namespace created by the class definition; we'll learn more
about class objects in the next section.  The original local scope
(the one in effect just before the class definition was entered) is
reinstated, and the class object is bound here to the class name given
in the class definition header ("ClassName" in the example).


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 MyClass:
       """A simple example class"""
       i = 12345

       def f(self):
           return 'hello world'

then "MyClass.i" and "MyClass.f" are valid attribute references,
returning an integer and a function object, respectively. Class
attributes can also be assigned to, so you can change the value of
"MyClass.i" by assignment. "__doc__" is also a valid attribute,
returning the docstring belonging to the class: ""A simple example
class"".

*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 = MyClass()

creează o *instanță* nouă a clasei și îi atribuie obiectul nou creat
variabilei locale "x".

The instantiation operation ("calling" a class object) creates an
empty object. Many classes like to create objects with instances
customized to a specific initial state. Therefore a class may define a
special method named "__init__()", like this:

   def __init__(self):
       self.data = []

When a class defines an "__init__()" method, class instantiation
automatically invokes "__init__()" for the newly created class
instance.  So in this example, a new, initialized instance can be
obtained by:

   x = MyClass()

Of course, the "__init__()" method may have arguments for greater
flexibility.  In that case, arguments given to the class instantiation
operator are passed on to "__init__()".  For example,

   >>> class Complex:
   ...     def __init__(self, realpart, imagpart):
   ...         self.r = realpart
   ...         self.i = imagpart
   ...
   >>> x = Complex(3.0, -4.5)
   >>> x.r, x.i
   (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*).

*Data attributes* correspond to "instance variables" in Smalltalk, and
to "data members" in C++.  Data attributes need not be declared; like
local variables, they spring into existence when they are first
assigned to.  For example, if "x" is the instance of "MyClass" created
above, the following piece of code will print the value "16", without
leaving a trace:

   x.counter = 1
   while x.counter < 10:
       x.counter = x.counter * 2
   print(x.counter)
   del x.counter

The other kind of instance attribute reference is a *method*. A method
is a function that "belongs to" an object.  (In Python, the term
method is not unique to class instances: other object types can have
methods as well.  For example, list objects have methods called
append, insert, remove, sort, and so on. However, in the following
discussion, we'll use the term method exclusively to mean methods of
class instance objects, unless explicitly stated otherwise.)

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

In the "MyClass" example, this will return the string "'hello world'".
However, it is not necessary to call a method right away: "x.f" is a
method object, and can be stored away and called at a later time.  For
example:

   xf = x.f
   while True:
       print(xf())

va afișa "salutare, lume" fără să se mai oprească din a da binețe.

What exactly happens when a method is called?  You may have noticed
that "x.f()" was called without an argument above, even though the
function definition for "f()" specified an argument.  What happened to
the argument? Surely Python raises an exception when a function that
requires an argument is called without any --- even if the argument
isn't actually used...

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.

If you still don't understand how methods work, a look at the
implementation can perhaps clarify matters.  When a non-data attribute
of an instance is referenced, the instance's class is searched.  If
the name denotes a valid class attribute that is a function object, a
method object is created by packing (pointers to) the instance object
and the function object just found together in an abstract object:
this is the method object.  When the method object is called with an
argument list, a new argument list is constructed from the instance
object and the argument list, and the function object is called with
this new argument list.


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 Dog:

       kind = 'canine'         # class variable shared by all instances

       def __init__(self, name):
           self.name = name    # instance variable unique to each instance

   >>> d = Dog('Fido')
   >>> e = Dog('Buddy')
   >>> d.kind                  # shared by all dogs
   'canine'
   >>> e.kind                  # shared by all dogs
   'canine'
   >>> d.name                  # unique to d
   'Fido'
   >>> e.name                  # unique to e
   'Buddy'

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 *mutabile*, 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 Dog:

       tricks = []             # mistaken use of a class variable

       def __init__(self, name):
           self.name = name

       def add_trick(self, trick):
           self.tricks.append(trick)

   >>> d = Dog('Fido')
   >>> e = Dog('Buddy')
   >>> d.add_trick('roll over')
   >>> e.add_trick('play dead')
   >>> d.tricks                # unexpectedly shared by all dogs
   ['roll over', 'play dead']

O proiectare corectă a clasei ar trebui să folosescă, în locul unei
variabile de clasă, o variabilă de instanță:

   class Dog:

       def __init__(self, name):
           self.name = name
           self.tricks = []    # creates a new empty list for each dog

       def add_trick(self, trick):
           self.tricks.append(trick)

   >>> d = Dog('Fido')
   >>> e = Dog('Buddy')
   >>> d.add_trick('roll over')
   >>> e.add_trick('play dead')
   >>> d.tricks
   ['roll over']
   >>> e.tricks
   ['play dead']


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 Warehouse:
   ...    purpose = 'storage'
   ...    region = 'west'
   ...
   >>> w1 = Warehouse()
   >>> print(w1.purpose, w1.region)
   storage west
   >>> w2 = Warehouse()
   >>> w2.region = 'east'
   >>> print(w2.purpose, w2.region)
   storage east

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:

   # Function defined outside the class
   def f1(self, x, y):
       return min(x, x+y)

   class C:
       f = f1

       def g(self):
           return 'hello world'

       h = g

Now "f", "g" and "h" are all attributes of class "C" that refer to
function objects, and consequently they are all methods of instances
of "C" --- "h" being exactly equivalent to "g".  Note that this
practice usually only serves to confuse the reader of a program.

Metodele pot apela alte metode folosind atributele de metodă ale
argumentului "self":

   class Bag:
       def __init__(self):
           self.data = []

       def add(self, x):
           self.data.append(x)

       def addtwice(self, x):
           self.add(x)
           self.add(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 DerivedClassName(BaseClassName):
       <statement-1>
       .
       .
       .
       <statement-N>

The name "BaseClassName" must be defined in a scope containing the
derived class definition.  In place of a base class name, other
arbitrary expressions are also allowed.  This can be useful, for
example, when the base class is defined in another module:

   class DerivedClassName(modname.BaseClassName):

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 întoarce "True" dacă și
  numai dacă "obiectul_în_cauză.__class__" este fie "int" fie altă
  clasă derivată din "int".

* Întrebuințați "issubclass()" pentru a testa moștenirea unei clase:
  "issubclass(bool, int)" va returna "True" pentru că "bool" este o
  *subclasă* (adică, o *moștenitoare*; sau o *urmașă*) a lui "int". În
  schimb, "issubclass(float, int)" va întoarce "False" dat fiind că
  "float" nu îi este subclasă lui "int".


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 DerivedClassName(Base1, Base2, Base3):
       <statement-1>
       .
       .
       .
       <statement-N>

For most purposes, in the simplest cases, you can think of the search
for attributes inherited from a parent class as depth-first, left-to-
right, not searching twice in the same class where there is an overlap
in the hierarchy. Thus, if an attribute is not found in
"DerivedClassName", it is searched for in "Base1", then (recursively)
in the base classes of "Base1", and if it was not found there, it was
searched for in "Base2", and so on.

Î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ă.

Dynamic ordering is necessary because all cases of multiple
inheritance exhibit one or more diamond relationships (where at least
one of the parent classes can be accessed through multiple paths from
the bottommost class).  For example, all classes inherit from
"object", so any case of multiple inheritance provides more than one
path to reach "object".  To keep the base classes from being accessed
more than once, the dynamic algorithm linearizes the search order in a
way that preserves the left-to-right ordering specified in each class,
that calls each parent only once, and that is monotonic (meaning that
a class can be subclassed without affecting the precedence order of
its parents). Taken together, these properties make it possible to
design reliable and extensible classes with multiple inheritance.  For
more detail, see https://www.python.org/download/releases/2.3/mro/.


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ă.

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 Mapping:
       def __init__(self, iterable):
           self.items_list = []
           self.__update(iterable)

       def update(self, iterable):
           for item in iterable:
               self.items_list.append(item)

       __update = update   # private copy of original update() method

   class MappingSubclass(Mapping):

       def update(self, keys, values):
           # provides new signature for update()
           # but does not break __init__()
           for item in zip(keys, values):
               self.items_list.append(item)

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 Employee:
       name: str
       dept: str
       salary: int

   >>> john = Employee('john', 'computer lab', 1000)
   >>> john.dept
   'computer lab'
   >>> john.salary
   1000

A piece of Python code that expects a particular abstract data type
can often be passed a class that emulates the methods of that data
type instead.  For instance, if you have a function that formats some
data from a file object, you can define a class with methods "read()"
and "readline()" that get the data from a string buffer instead, and
pass it as an argument.

Instance method objects have attributes, too: "m.__self__" is the
instance object with the method "m()", and "m.__func__" is the
function object corresponding to the method.


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 element in [1, 2, 3]:
       print(element)
   for element in (1, 2, 3):
       print(element)
   for key in {'one':1, 'two':2}:
       print(key)
   for char in "123":
       print(char)
   for line in open("myfile.txt"):
       print(line, 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:

   >>> s = 'abc'
   >>> it = iter(s)
   >>> it
   <str_iterator object at 0x10c90e650>
   >>> next(it)
   'a'
   >>> next(it)
   'b'
   >>> next(it)
   'c'
   >>> next(it)
   Traceback (most recent call last):
     File "<stdin>", line 1, in <module>
       next(it)
   StopIteration

Having seen the mechanics behind the iterator protocol, it is easy to
add iterator behavior to your classes.  Define an "__iter__()" method
which returns an object with a "__next__()" method.  If the class
defines "__next__()", then "__iter__()" can just return "self":

   class Reverse:
       """Iterator for looping over a sequence backwards."""
       def __init__(self, data):
           self.data = data
           self.index = len(data)

       def __iter__(self):
           return self

       def __next__(self):
           if self.index == 0:
               raise StopIteration
           self.index = self.index - 1
           return self.data[self.index]

   >>> rev = Reverse('spam')
   >>> iter(rev)
   <__main__.Reverse object at 0x00A1DB50>
   >>> for char in rev:
   ...     print(char)
   ...
   m
   a
   p
   s


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 reverse(data):
       for index in range(len(data)-1, -1, -1):
           yield data[index]

   >>> for char in reverse('golf'):
   ...     print(char)
   ...
   f
   l
   o
   g

Anything that can be done with generators can also be done with class-
based iterators as described in the previous section.  What makes
generators so compact is that the "__iter__()" and "__next__()"
methods are created automatically.

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 of squares
   285

   >>> xvec = [10, 20, 30]
   >>> yvec = [7, 5, 3]
   >>> sum(x*y for x,y in zip(xvec, yvec))         # dot product
   260

   >>> unique_words = set(word for line in page  for word in line.split())

   >>> valedictorian = max((student.gpa, student.name) for student in graduates)

   >>> data = 'golf'
   >>> list(data[i] for i in range(len(data)-1, -1, -1))
   ['f', 'l', 'o', 'g']

-[ Note de subsol ]-

[1] Except for one thing.  Module objects have a secret read-only
    attribute called "__dict__" which returns the dictionary used to
    implement the module's namespace; the name "__dict__" is an
    attribute but not a global name. Obviously, using this violates
    the abstraction of namespace implementation, and should be
    restricted to things like post-mortem debuggers.
