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

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

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.data = []

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

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

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

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

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

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

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

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

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

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