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