post

Python ro:Programare orientată pe obiecte

Contents

Introducere

În toate programele folosite până acum ne-am construit soluţiile în jurul funcţiilor adică blocuri de declaraţii care manipulează date. Acest mod de programare se numeşte orientat pe proceduri. Există şi un alt mod de organizare a programelor, în care funcţionalitatea şi datele sunt împachetate împreună în unităţi numite obiecte. Acest mod de structurare defineşte paradigma “orientat pe obiecte. Aproape tot timpul puteţi folosi abordări procedurale în programare, dar când scrieţi programe mari sau aveţi de rezolvat probleme care sunt mai aproape de acest mod de structurare, puteţi folosi tehnicile de programare orientată pe obiecte..

Clasele şi obiectele sunt două aspecte ale programării orientate pe obiecte. O clasă creeaza un nou tip în care obiectele sunt instanţe ale clasei. O analogie este că puteţi avea variabile de tip int care se traduce prin aceea că variabilele care stochează întregi sunt instanţe (obiecte) ale clasei int.

Notă pentru programatorii în limbaje cu tipuri statice
Observaţi că până şi întregii sunt trataţi ca obiecte (ale clasei int). Asta e diferit de C++ şi Java (în versiunile dinainte de 1.5) în care întregii sunt tipuri primitive native. A se vedea help(int) pentru detalii despre clasă.
Programatorii în C# şi Java 1.5 vor găsi o similaritate cu conceptele de încapsulare şi decapsulare.

Obiectele pot stoca date folosind variabile obişnuite care aparţin obiectului. Variabilele care aparţin unui obiect sunt numite câmpuri. Obiectele pot avea şi funcţionalitate folosind funcţii care aparţin clasei. Aceste funcţii se munesc metode ale clasei. Această terminologie este importantă deoarece ne ajuta să diferenţiem între funcţii şi variabile independente şi cele aparţinând unui obiect sau unei clase. Împreună, variabilele şi funcţiile care aparţin unei clase se numesc atribute ale clasei.

Câmpurile sunt de două tipuri – ele pot aparţine fiecărei instanţe/obiect al clasei sau pot aparţine însăşi clasei. Acestea sunt numite variabile de instanţă respectiv variabile ale clasei.

O clasă este creată folosind cuvântul cheie class. Câmpurile şi metodele clasei sunt listate întrun bloc indentat.

self

Clasele şi metodele au o diferenţă specifică faţă de funcţiile obişnuite – ele trebuie să aibă un prenume suplimentar care trebuie adăugat la începutul listei de parametri, dar nu trebuie să-i daţi o valoare când apelaţi metoda, Python o va furniza. Această variabilă specială se referă la obiectul însuşi (engl. self) şi prin convenţie este numită self.

Cu toate acestea, deşi puteţi să-i daţi orice nume acestui parametru, este puternic recomandat să folosiţi numele self – orice alt nume este dezaprobat. Există multe avantaje în folosire numelui standard – orice cititor al programului va înţelege imediat despre ce este vorba şi chiar mediile IDE (Integrated Development Environments) specializate te pot ajuta dacă foloseşti self.

Notă pentru programatorii în C++/Java/C#
self din Python este echivalent cu pointerul this din C++ şi cu referinţa this din Java şi C#.

Probabil vă întrebaţi cum dă Python valoarea corectă lui self şi de ce nu trebuie să-i dăm o valoare. Un exemplu va clarifica această problemă. Să zicem că aveţi o clasă numită MyClass şi o instanţă a acestei clase, numită myobject. Când apelaţi o metodă a acestui obiect myobject.method(arg1, arg2), apelul este automat convertit de Python în MyClass.method(myobject, arg1, arg2) – asta e toată marea specialitate a lui self.

Asta înseamnă şi că dacă aveţi o metodă care nu primeşte argumente, tot va trebui să aveţi un argument – self.

Clase

Cea mai simplă clasă posibilă este arătată în exemplul următor.

#!/usr/bin/python
# Fişier: simplestclass.py

class Persoana:
    pass # Un bloc gol

p = Persoana()
print(p)

Rezultat:

   $ python simplestclass.py
   <__main__.Persoana object at 0x019F85F0>

Cum funcţionează:

Creăm o clasă nouă folosind declaraţia class şi numele clasei. Aceasta este urmată de un bloc indentat de declaraţii care formează corpul clasei. În acest caz avem un bloc gol, arătat de declaraţia pass.

În continuare creăm un obiect/instanţă a acestei clase folosind numele clasei urmat de o pereche de paranteze. (Vom învăţa mai multe despre instanţiere în paragraful următor). Pentru propria verificare, confirmăm tipul variabilei prin simpla ei tipărire. Aflăm că avem o instanţă a variabilei din clasa Persoana din modulul __main__.

Observaţi că a fost tipărită şi adresa unde este stocat obiectul în memoria calculatorului. Adresa aceasta va avea o valoare diferită în alt calculator deoarece Python îl stochează unde are loc.

Metodele obiectelor

Am discutat deja că obiectele/clasele pot avea metode, exact ca funcţiile, doar că au un argument self în plus. Iată un exemplu.

#!/usr/bin/python
# Fişier: metoda.py

class Persoana:
    def ziSalut(self):
        print('Salut, ce mai faci?')

p = Persoana()
p.ziSalut()

# Acest exemplu poate fi scris şi ca Persoana().ziSalut()

Rezultat:

   $ python metoda.py
   Salut, ce mai faci?

Cum funcţionează:

Aici vedem particula self în acţiune. Observaţi că metoda ziSalut nu primeşte parametri, dar tot are argumentul self în definiţia funcţiei.

Metoda __init__

Există multe nume de metode care au un înteles special în clasele Python. Acum vom afla semnificaţia metodei __init__.

Metoda __init__ este executată imediat ce este instanţiat un obiect al clasei. Metoda este utilă pentru a face iniţializarea dorită pentru obiectul respectiv. Observaţi că numele este încadrat cu dublu underscore.

Exemplu:

#!/usr/bin/python
# Fişier: class_init.py

class Persoana:
    def __init__(self, nume):
        self.nume = nume
    def ziSalut(self):
        print('Salut, numele meu este ', self.nume)

p = Persoana('Swaroop')
p.ziSalut()

# Acest exemplu putea fi scris Persoana('Swaroop').ziSalut()

Rezultat:

   $ python class_init.py
   Salut, numele meu este Swaroop

Cum funcţionează:

Definim metoda __init__ să ia un parametru nume (pe lângă obişnuitul self). În acest caz creăm doar un câmp numit nume. Observaţi că deşi ambele sunt numite ‘nume’, cele două sunt obiecte diferite. Notaţia cu punct ne permite să le deosebim.

Cel mai important, observaţi că nu apelăm explicit metoda __init__ ci îi transmitem argumentele în paranteză după numele clasei în momentul creării obiectului/instanţa a clasei. Aceasta este semnificaţia acestei metode.

Acum putem să folosim câmpul self.name în metodele noastre, ceea ce este arătat în metoda ziSalut.

Variabile de clasă, variabile de instanţă

Am discutat deja partea de funcţionalitate a claselor şi obiectelor (adică metodele), acum să învăţăm ceva despre partea de date. Partea de date, aşa-numitele câmpuri, nu sunt altceva decât variabile obişnuite care sunt legate de spaţiile de nume ale claselor şi obiectelor. Asta înseamnă că aceste nume sunt valabile numai în contextul claselor şi obiectelor respective. Din acest motiv acestea sunt numite spaţii de nume (engl. name spaces).

Există două feluri de câmpuri – variabile de clasa şi variabile de obiect/instanţă, care sunt clasificate în funcţie de proprietarul variabilei.

Variabilele de clasă sunt partajate – ele pot fi accesate de toate instanţele acelei clase. Există doar un exemplar al variabilei de clasă şi când o instanţă îi modifică valoarea, această modificare este văzută imediat de celelalte instanţe.

Variabilele de instanţă sunt proprietatea fiecărei instanţe a clasei. În acest caz, fiecare obiect are propriul exemplar al acelui câmp adică ele nu sunt relaţionate în nici un fel cu câmpurile având acelaşi nume în alte insţante. Un exemplu va ajuta la înţelegerea situaţiei:

#!/usr/bin/python
# Fişier: objvar.py

clasa Robot:
    '''Reprezintă un robot cu nume.'''

    # O variabilă de clasă, numărătorul populaţiei de roboţi
    populaţie = 0

    def __init__(self, nume):
        '''Iniţializează datele.'''
        self.nume = nume
        print('(Iniţializez robotul {0})'.format(self.nume))

        # Când această instanţă este creată, robotul se
        # adaugă la populaţie
        Robot.populaţie += 1

    def __del__(self):
        '''Dispar...'''
        print('{0} este dezmembrat!'.format(self.nume))

        Robot.populaţie -= 1

        if Robot.populaţie == 0:
            print('{0} a fost ultimul.'.format(self.nume))
        else:
            print('Mai există {0:d} roboţi apţi de lucru.'.format(Robot.populaţie))

    def ziSalut(self):
        '''Salutare de la robot.

        Da, pot să facă şi asta.'''
        print('Salut. Stăpânii mei îmi zic {0}.'.format(self.nume))

    def câţi():
        '''Tipăreşte populaţia curentă.'''
        print('Avem {0:d} roboţi.'.format(Robot.populaţie))
    câţi = staticmethod(câţi)

droid1 = Robot('R2-D2')
droid1.ziSalut()
Robot.câţi()

droid2 = Robot('C-3PO')
droid2.ziSalut()
Robot.câţi()

print("nRoboţii pot să facă nişte treabă aici.n")

print("Roboţii au terminat treaba. Deci să-i distrugem.")
del droid1
del droid2

Robot.câţi()

Rezultat:

   (Iniţializez robotul R2-D2)
   Salut. Stăpânii mei îmi zic R2-D2.
   Avem 1 roboţi.
   (Iniţializez robotul  C-3PO)
   Salut. Stăpânii mei îmi zic C-3PO.
   Avem 2 roboţi.

   Roboţii pot să facă nişte treabă aici.

   Roboţii au terminat treaba. Deci să-i distrugem.
   R2-D2 este dezmembrat!
   Mai există 1 roboţi apţi de lucru.
   C-3PO este dezmembrat!
   C-3PO a fost ultimul.
   Avem 0 roboţi.

Cum funcţionează:

Este un exemplu lung, dar ajută la evidenţierea naturii variabilelor de clasă şi de instanţă. Aici câmpul populaţie aparţine clasei Robot şi este deci o variabilă de clasă. Variabila nume aparţine obiectului (îi este atribuită folosind self.) deci este o variabilă de obiect/instanţă.

Asadar ne referim la variabila de clasă populaţie cu notaţia Robot.populaţie şi nu cu self.populaţie. Ne referim la variabila de instanţă nume cu notaţia self.nume în metodele acelui obiect. Amintiţi-vă această diferenţă simplă între variabilele de clasă şi de instanţă. Mai observaţi şi că o variabilă de obiect cu acelaşi nume ca o variabilă de clasă, va ascunde variabila de clasă faţă de metodele clasei!

Metoda câţi este în fapt o metodă a clasei şi nu a instanţei. Asta înseamnă că trebuie să o definim cu declaraţia classmethod sau staticmethod Dacă vrem să ştim cărui spaţiu de nume îi aparţine. Întrucât nu vrem aceasta informaţie, o vom defini cu staticmethod.

Am fi putut obţine acelaşi lucru folosind decoratori:

    @staticmethod
    def câţi():
        '''Tipăreşte populaţia curentă.'''
        print('Avem {0:d} roboti.'.format(Robot.populaţie))

Decoratorii pot fi concepuţi ca scurtături pentru apelarea unor declaraţii explicite, aşa cum am văzut în acest exemplu.

Observaţi că metoda __init__ este folosită pentru a iniţializa cu un nume instanţa clasei Robot. În această metodă, mărim populaţie cu 1 intrucât a fost creat încă un robot. Mai observaţi şi că valoarea self.nume este specifică fiecărui obiect, ceea ce indică natura de variabilă de instanţă a variabilei.

Reţineţi că trebuie să vă referiţi la variabilele şi metodele aceluiaşi obiect numai cu self. Acest mod de indicare se numeşte referinţă la atribut.

În acest program mai vedem şi docstrings pentru clase şi metode. Putem accesa docstringul clasei în runtime (rom. timpul execuţiei) folosind notaţia Robot.__doc__ şi docstring-ul metodei ziSalut cu notaţia Robot.ziSalut.__doc__

Exact ca şi metoda __init__, mai există o metodă specială, __del__, care este apelată atunci când un obiect trebuie distrus, adică nu va mai fi folosit, iar resursele lui sunt returnate sistemului. În această metodă reducem şi numărul Robot.populaţie cu 1.

Metoda __del__ este executată dacă obiectul nu mai este în folosinţă şi nu există o garanţie că metoda va mai fi rulată. Dacă vreţi să o vedeţi explicit în acţiune, va trebui să folosiţi declaraţia del cum am făcut noi aici.

Notă pentru programatorii în C++/Java/C#
Toţi membrii unei clase (inclusiv membrii date) sunt publici şi toate metodele sunt virtual în Python.
O excepţie: Dacă folosiţi membrii date cu nume care încep cu dublu underscore precum __var_privată, Python exploatează acest aspect şi chiar va face variabila să fie privată.
Aşadar, convenţia este că orice variabilă care vrem să fie folosită numai în contextul clasei sau obiectului, ar trebui să fie numită cu primul caracter underscore, iar toate celelalte nume sunt publice şi pot fi folosite de alte clase/instanţe. Reţineţi că aceasta este doar o convenţie şi nu este impusă de Python (exceptând prefixul dublu underscore).

Moştenire

Un beneficiu major al programării orientate pe obiecte este refolosirea codului şi o cale de a obţine asta este mecanismul de moştenire. Moştenirea poate fi descrisă cel mai bine ca şi cum ar implementa o relaţie între un tip şi un subtip între clase.

Să zicem că scrieţi un program în care trebuie să ţineţi evidenţa profesorilor şi studenţilor într-un colegiu. Ei au unele caracteristici comune, precum nume, adresă, vârstă. Ei au şi caracteristici specifice, cum ar fi salariul, cursurile şi perioadele de absenţă, pentru profesori, respectiv notele şi taxele pentru studenţi.

Puteţi crea două clase independente pentru fiecare tip şi procesa aceste clase prin adăugarea de caracteristici noi. Aşa programul devine repede un hăţis necontrolabil.

O cale mai bună ar fi să creaţi o clasă comună numită MembruAlŞcolii şi să faceţi clasele student şi profesor să moştenească de la această clasă, devenind astfel subclase ale acesteia, şi apoi să adăugaţi caracteristici la aceste subtipuri.

Această abordare are multe avantaje. Dacă facem vreo schimbare la funcţionalitatea clasei MembruAlŞcolii, ea este automat reflectată şi în subtipuri. De exemplu, puteţi adăuga un nou câmp card ID şi pentru studenţi şi pentru profesori prin simpla adăugare a acestuia la clasa MembruAlŞcolii. Totuşi, schimbările din subtipuri nu afectează alte subtipuri. Alt avantaj este că puteţi face referire la un profesor sau student ca obiect MembruAlŞcolii object, ceea ce poate fi util în anumite situaţii precum calculul numărului de membri ai şcolii. Acest comportament este numit polimorfism, în care un subtip poate fi folosit în orice situatie în care se aşteaptă folosirea unui tip părinte adică obiectul poate fi tratat drept instanţă a tipului părinte.

Observaţi şi că refolosim codul clasei părinte şi nu e nevoie să-l repetăm în subclase cum am fi fost nevoiţi dacă am fi creat clase independente.

Clasa MembruAlŞcolii în această situaţie este numită clasa bază sau superclasa. Clasele profesor şi student sunt numite clase derivate sau subclase.

Vom vedea acest exemplu pe un program.

#!/usr/bin/python
# Fişier: inherit.py

class MembruAlŞcolii:
    '''Reprezintă orice membru al şcolii.'''
    def __init__(self, nume, vârstă):
        self.nume = nume
        self.varsta = vârstă
        print('(Iniţializez MembruAlŞcolii: {0})'.format(self.nume))

    def descrie(self):
        '''Afişează detaliile mele.'''
        print('Nume:"{0}" Vârstă:"{1}"'.format(self.nume, self.vârstă), end=" ")

class profesor(MembruAlŞcolii):
    '''Reprezintă un profesor.'''
    def __init__(self, nume, vârstă, salariu):
        MembruAlŞcolii.__init__(self, nume, vârstă)
        self.salariu = salariu
        print('(Iniţializez Profesor: {0})'.format(self.nume))

    def descrie(self):
        MembruAlŞcolii.descrie(self)
        print('Salariu: "{0:d}"'.format(self.salariu))

class student(MembruAlŞcolii):
    '''Reprezintă un student.'''
    def __init__(self, nume, vârstă, note):
        MembruAlŞcolii.__init__(self, nume, vârstă)
        self.note = note
        print('(Iniţializez student: {0})'.format(self.nume))

    def descrie(self):
        MembruAlŞcolii.descrie(self)
        print('Note: "{0:d}"'.format(self.note))

p = profesor('D-na. Shrividya', 40, 30000)
s = student('Swaroop', 25, 75)

print() # Afişează o linie goală

membri = [p, s]
for membru in membri:
    membru.descrie() # Funcţionează şi pentru profesor şi pentru student

Rezultat:

   $ python inherit.py
   (Iniţializez MembruAlŞcolii: Mrs. Shrividya)
   (Iniţializez profesor: D-na. Shrividya)
   (Iniţializez MembruAlŞcolii: Swaroop)
   (Iniţializez student: Swaroop)

   Nume:"D-na. Shrividya" Vârstă:"40" Salariu: "30000"
   Nume:"Swaroop" Vârstă:"25" Note: "75"

Cum funcţionează:

Pentru a folosi moştenirea, specificăm clasele bază întrun tuplu care urmează numele în definiţia clasei. Apoi observăm că metoda __init__ a clasei bază este apelată explicit folosind variabila self ca să putem iniţializa partea din obiect care provine din clasa bază. Este foarte important de reţinut – Python nu apeleaza automat constructorul clasei bază, trebuie să faceţi asta explicit.

Mai observăm că apelurile către clasa bază se fac prefixind numele clasei apelului metodelor şi punând variabila self împreună cu celelalte argumente.

Se confirmă că folosim instanţele claselor profesor şi student ca şi cum ar fi instanţe ale clasei MembruAlŞcolii când folosim metoda descrie a clasei MembruAlŞcolii.

În plus, observaţi că este apelată metoda descrie a subtipului nu metoda descrie a clasei MembruAlŞcolii. O cale de a întelege asta este că Python începe întotdeauna căutarea metodelor în tipul curent, ceea ce face în acest caz. Dacă nu ar fi găsit metoda, ar fi căutat în clasele părinte, una câte una, în ordinea specificată în tuplul din definiţia clasei.

O notă asupra terminologiei – daca în tuplul de moştenire este listată mai mult de o clasă, acest caz este de moştenire multiplă.

Rezumat

Am explorat diverse aspecte ale claselor şi obiectelor precum şi diferite terminologii asociate cu acestea. Am văzut de asemenea beneficiile şi punctele slabe ale programării orientate pe obiecte. Python este puternic orientat pe obiecte şi înţelegerea acestor concepte vă va ajuta enorm în cariera de programator.

Mai departe vom învăţa să tratăm cu intrările şi ieşirile şi cum să accesam fişiere în Python.


Advertisements