User Tools

Site Tools


poo

Programare Orientată Obiect

Prezentare: py4school_-_poo.pdf

În capitolul anterior am văzut cum declarăm funcții individual, pentru a lucra într-o manieră procedurală. În continuare ne vom referi la organizarea acestor funcții în cadrul unor clase, programare orientată pe obiecte (OOP).

Vom prezenta pe scurt cum se utilizează clasele, obiectele, moștenirea.

Pentru o scurtă introducere despre modul de funcționare al claselor și obiectelor, precum și o diferențiere clara între programarea (clasică) procedurală și cea orientată pe obiect, consultati paragraful de aici.

Clase și obiecte

E important de stiut ca totul in python este un obiect. O functie de exemplu este tot un obiect, sa vedem rapid de ce:

def a():
    """Un text descriptiv al functiei"""
    pass
>>> a.__doc__
'Un text descriptiv al functiei'

Asadar vedeti ca o functie are anumite atribuite precum __doc__, si multe altele interne (dir(a) va arata alte metode interne ale obiectului a). E important sa retineti ca totul este un obiect.

Definirea unei clase

Se definește prin cuvântul rezervat class. Este recomandat ca numele clasei să fie cu litere mari, dar nu este o eroare dacă numiți o clasă cu litere mici.

class NumeClasa:
    """Adăugarea unei descrieri (doc string) despre scopul clasei este recomandat."""
    <declaratie-1>
    .
    .
    .
    <declaratie-N>

Codul definit in cadrul unei clase trebuie, ca si in cadrul functiilor (cu declaratia def), trebuie executat inainte sa aiba vreun efect. O clasa are propriul namespace (spatiu de nume), deci orice atribuire de variabile in cadrul unei clase intra in spatiul de nume local acelei functii.

Obiectul Clasa

class MyClass:
    """Un exemplu de clasa simpla"""
    i = 12345
    def f(self):
        return 'hello world'

Apoi MyClass.i si MyClass.f sunt referentieri valide (returnand un intreg si respectiv un obiect de tip functie), pentru ca atributele i si f au fost definite in spatiul de nume al clasei MyClass. Putem atribui alte valori variabilelor claselor, deci putem schimba valoarea lui MyClass.i daca dorim.

>>> MyClass.i
12345
>>> MyClass.f
<unbound method MyClass.f>
>>> MyClass.i = 1
>>> MyClass.i
1
>>> MyClass.__doc__
'Un exemplu de clasa simpla'

Instantierea unei clase

Instantierea unei clase in python se face ca apelul unei functii. Exemplul de cod:

>>> x = MyClass()
>>> x.i
1
>>> x.f
<bound method MyClass.f of <__main__.MyClass instance at 0x10049cd40>>

creeaza o noua instanta a clasei si atribuie acest obiect unei variabile locale x. Observati ca atributul i este un atribut al clasei, deci va fi partajat tuturor instantelor clasei.

Instantierea va produce un obiect x gol insa, fara variabile interne proprii. De multe ori clasele vor sa personalizeze obiectele create cu o stare initiala anume. Astfel o clasa poate defini metoda speciala __init__:

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

Cand o clasa defineste o metoda __init__, instantierea clasei apeleaza aceasta metoda. Aceasta metoda poate avea orice numar de argumente, bineinteles:

class Complex:
    def __init__(self, realpart, imagpart):
        self.r = realpart
        self.i = imagpart
>>> complex = Complex(3.0, -4.5)
>>> complex.r, complex.i
(3.0, -4.5)

Metode și atribute ale clasei

Reluam exemplul

class MyClass:
    """Un exemplu de clasa simpla"""
    i = 12345
    def f(self):
        return 'hello world'

In python, o instanta poate referi doua tipuri de atribute: atribute de date si metode.

Atributele de date nu trebuie declarate inainte, ele sunt create atunci cand le este atribuita o valoare prima oara. De exemplu, daca folosim instanta x a clasei MyClass, codul urmator va printa 16 fara a lasa vreo urma (pentru ca in final stergem atributul counter abia creat):

>>> x.counter = 1
>>> while x.counter < 10:
    x.counter = x.counter * 2
>> print x.counter
16
>> del x.counter
>> print x.counter
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: MyClass instance has no attribute 'counter'

Al doilea tip de atribut este o metoda, care nu este altceva decat o functie ce apartine unui obiect. Asta inseamna ca o metoda trebuie sa contina cumva si o referinta catre obiect.

O functie pe o clasa determina la randul sau o metoda in cadrul unei instante a acelei clase. Deci MyClass.f fiind o functie, determina sa existe o metoda pe x.f, in timp ce x.i nu este o metoda, pentru ca nici MyClass.i nu este. x.f este o metoda obiect, in timp ce MyClass.f este o functie obiect.

Metode obiect

O metoda poate fi chemata imediat, iar in exemplul anterior x.f() ar produce 'hello world'. Putem insa amana acest lucru pentru mai tarziu:

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

va continua sa printeze “hello world” pentru totdeauna. De ce putem face asta?

Cand o metoda obiect este apelata, obiectul (referinta catre obiect) este pasat automat de catre python pentru noi. Astfel ca x.f() fiind o metoda, este perfect echivalent cu a face MyClass.f(x), care este apelul unei functii, dar careia ii pasam noi manual referinta catre acel obiect.

>>> x.f()
'hello world'
>>> MyClass.f(x)
'hello world'

Acel cuvant self din cadrul unei metode reprezinta chiar instanta curenta ce este pasata automat de python cand apelam o metoda obiect. Ca o analogie, poate fi acelasi lucru ca this din C++. Numele de self este o conventie, insa prin nerespectarea ei, codul poate deveni mai greu de inteles si de citit.

Instantiere clasa, constructor __init__, atribute instanta

class Persoana:
    def __init__(self, nume):
        self.nume = nume
    def salut(self):
        print('Salut, numele meu este %s.' % self.nume)
 
>>> p = Persoana()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() takes exactly 2 arguments (1 given)
>>> p = Persoana('Gigel')
>>> p.salut()
Salut, numele meu este Gigel.

Daca incercam sa instantiem clasa fara parametrul nume, vom primi o eroare. Aici atributul nume este la un atribut al instantei, fiind propriu obiectului, nu clasei (spre deosebire de MyClass.i).

Manipularea unui obiect

Fiindca o clasa este si ea un obiect, aceste lucruri se aplica si claselor.

class MyClass:
    i = 'ceva'
>>> MyClass.i
'ceva'
>>> del MyClass.i
>>> MyClass.i
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: class MyClass has no attribute 'i'
 
>>> MyClass.j = 'altceva'
>>> MyClass.j
'altceva'
 
# Putem defini o functie si apoi s-o atribuim clasei.
# Observati ca trebui sa primeasca parametrul self.
def f1(self, x, y):
    return min(x,y)
 
>>> MyClass.minim = f1
>>> a = MyClass()
>>> a.minim(2,3)
2
>>> a.minim = 'schimbam functia cu un string'
>>> a.minim
'schimbam functia cu un string'
# Insa atributul clasei ramane tot functia f1, nu e schimbat in string;
# ce se intampla? vedem imediat mai jos.
>>> MyClass.minim
<unbound method MyClass.f1>

Practic trebui sa retineti ca atat clasele cat si instantele nu sunt altceva decat niste dictionare, deci pot fi modificate oricand.

Spatiul de nume al atributelor

Vom defini o instanta a clasei MyClass, pentru a vedea distinctiile intre atribute ale clasei si cele ale instantei. Vom folosi metoda vars() pentru a inspecta ce atribute locale contine un obiect.

class MyClass:
    i = 1
 
>>> MyClass.i
1
>>> vars(MyClass)
# Observati ca numele cu dublu underscore sunt atribute interne.
{'i': 1, '__module__': '__main__', '__doc__': None}
>>> a = MyClass()
>>> a.i
1
>>> vars(a)
{}

Ce se intampla aici? Vedem ca MyClass contine atributul i, in timp ce instanta a nu il contine. Cu toate astea a.i afisaza valoarea corecta.

Pentru a intelege mai bine, sa incercam sa modificam variabila clasei, iar apoi a instantei:

>>> MyClass.i += 1
>>> MyClass.i, a.i
(2, 2)
>>> vars(a)
{}
>>> a.i = 42
>>> vars(a)
{'i': 42}
>>> vars(MyClass)
{'i': 2, '__module__': '__main__', '__doc__': None}

Deci putem vedea ca instanta a tocmai fiindca nu avea un atribut cu numele i, incearca sa il gaseasca in cadrul clasei MyClass. Insa odata ce am creat un atribut i pe instanta cu a.i = 42, obiectul va gasi valoarea in atributele interne, si nu va mai cauta pe atributul clasei (care a ramas neschimbat, observati!).

Un ultim exemplu ar trebui sa faca tot mai clara distinctia intre atributele claselor si cele ale obiectelor.

class A:
    def __init__(self):
        self.i = 2
 
>>> vars(A)
{'__module__': '__main__', '__doc__': None, '__init__': <function __init__ at 0x1004a32a8>}
>>> a = A()
>>> vars(a)
{'i': 2}

Mostenire

Un avantaj al OOP este reutilizarea codului. Putem face asta usor cu mostenirea claselor.

class Persoana(object):
    def __init__(self, nume):
        self.nume = nume
    def salut(self):
        print('Salut, numele meu este %s.' % self.nume)
 
# Mostenirea se face prin ClasaNoua(ClasaVeche), unde ClasaNoua extinde
# ClasaVeche.
class Profesor(Persoana):
    def __init__(self, nume, liceu):
        # Apelare constructor pentru Persoana va
        # seta numele pe instanta.
        Persoana.__init__(self, nume)
        self.liceu = liceu
    def preda(self):
        print('Voi preda python in liceul %s.' % self.liceu)

In python extindem o clasa folosind paranteze, ca in exemplul in care Profesor extinde Persoana.

Observati cum clasa Persoana extinde clasa object. Este din nou o recomandare in python ca clasa de baza sa extinda object.

Totodata important de observat este ca python NU va apela automat constructorul clasei de baza (pe cel al clasei Persoana), trebuie sa facem noi asta. Iar in acest caz trebuie pasat si parametrul self, in Persoana.__init__(self, nume).

>>> profesor = Profesor('Andrei', 'Sincai')
# Profesor extinde clasa Persoana, deci are acces si la metoda salut.
>>> profesor.salut()
Salut, numele meu este Andrei.
# Metoda definita in cadrul clasei Profesor.
>>> profesor.preda()
Voi preda python in liceul Sincai.

In aceasta metoda nu e nevoie sa pasam noi parametrul self, si nici sa stim pe cine extinde clasa Profesor.

Functii builtin

Vom folosi urmatorul cod pentru demonstrare. pass ne permite sa nu definim conținutul unei clase sau al unei functii, altfel am primi erori fiindca python se bazeaza pe indentare, nu putem lasa locul gol complet.

class Person(object):
    pass
 
class Person1(Person):
    pass
 
class Person2(Person):
    pass

isinstance

isinstance(object, type) → boolean

Adevarat daca object este o instanta a tipului type sau a oricarei subclase a tipului type.

>>> isinstance(Person(), Person)
True
>>> isinstance(Person1(), Person)
True
>>> isinstance(Person1(), Person2)
False
>>> isinstance(Person(), Person1)
False

issubclass

issubclass(class, base) → boolean

Adevarat daca clasa class este subclasa a clasei base.

>>> issubclass(Person1, Person)
True
>>> issubclass(Person, Person1)
False
>>> issubclass(Person1, Person2)
False

super

super(type) → type

Va returna superclasa tipului type. Mai multe detalii aici.

class A(object):
    def show(self):
        print 'show from A()'
 
class B(A):
    def show(self):
        print 'show from B()'
 
>>> A().show()
show from A()
>>> B().show()
show from B()
>>> super(B, B()).show()
show from A()

Duck Typing

Acest concept se refera la modul dinamic de “tipare” (dynamic typing) in python, in care metodele si proprietatile unui obiect determina semantica acestuia, si nu mostenirea. Pe scurt, “nu verifica daca este o rata, verifica daca face ca o rata, daca merge ca o rata etc.”. Mai multe pe Wikipedia.

class Duck:
    def quack(self):
        print "Quaaaaaack!"
 
    def feathers(self):
        print "Rata are pene albe si gri."
 
class Person:
    def quack(self):
        print "Persoana imita rata."
 
    def feathers(self):
        print "Persoana ia o pana de pe jos si o arata."
 
def in_the_forest(duck):
    duck.quack()
    duck.feathers()

Si dupa ce definim doua clase, Duck si Person care ambele implementeaza aceleasi metode quack si feathers, putem observa faptul ca metodei in_the_forest nu-i pasa ce tip de obiect/instanta primeste, atata timp cat are acele metode implementate.

>>> donald = Duck()
>>> in_the_forest(donald)
Quaaaaaack!
Rata are pene albe si gri.
>>> john = Person()
>>> in_the_forest(john)
Persoana imita rata.
Persoana ia o pana de pe jos si o arata.

Alte resurse

Exercitii

1. Dandu-se aceste definitii de scos si de adaugat bani la un cont bancar, pastrati un comportament similar prin folosirea paradigmei de programare OOP. Definiti o clasa numita BankAccount.

def make_account():
    return {'balance': 0}
 
def deposit(account, amount):
    account['balance'] += amount
    return account['balance']
 
def withdraw(account, amount):
    account['balance'] -= amount
    return account['balance']
 
>>> a = make_account()
>>> b = make_account()
>>> deposit(a, 100)
100
>>> deposit(b, 50)
50
>>> withdraw(b, 10)
40
>>> withdraw(a, 10)
90

2. Peste BankAccount sa adaugam o clasa ChargingAccount care adauga o taxa in plus (de 3 lei sa zicem) la fiecare retragere de numerar. Ar trebui sa se comporte:

class ChargingAccount(BankAccount):
    def __init__(self):
        # TODO
 
    def withdraw(self, amount):
        # TODO
 
>>> a = ChargingAccount()
>>> a.deposit(100)
100
>>> a.withdraw(10)
87 # am fost taxati 3 lei in plus
 
>>> b = BankAccount()
>>> b.deposit(50)
50
>>> b.withdraw(10)
40
  • Soluție
    class ChargingAccount(BankAccount):
        def __init__(self):
            BankAccount.__init__(self)
            self.fee = 3
     
        def withdraw(self, amount):
            # Taxeaza la retragere cu o taxa in plus de 3.
            return BankAccount.withdraw(self, amount+self.fee)

3. Stiind ca o clasa Point ce primeste doua coordonate (x, y) are metode interne pentru operatii aritmetice (__add__, __sub__), definiti aceste metode astfel incat sa putem aduna/scadea doua puncte.

class Point(object):
    def __init__(self, x, y):
        pass
    def __add__(self, other):
        pass
    def __sub__(self, other):
        pass
 
# Ar trebui sa se comporte astfel:
>>> p1 = Point(0,1)
>>> p2 = Point(1,2)
>>> p3 = p1+p2
>>> p3
<__main__.Point object at 0x1004a4e10>
>>> p3.x
1
>>> p3.y
3
  • Soluție (se bazează pe exemplul cu RationalNumber):
    class Point(object):
        def __init__(self, x, y):
            self.x = x
            self.y = y
        def __add__(self, other):
            return Point(self.x + other.x, self.y + other.y)
        def __sub__(self, other):
            return Point(self.x - other.x, self.y - other.y)

4. Implementați clasa Complex care definește un număr complex. Clasa va permite realizarea următoarelor operații:

>>> x = Complex(2, 3)
>>> y = Complex(3, 6)
>>> x + y
5 + 9i
>>> y - x
1 + 3i
>>> x / y
0 + 0i
>>> x = Complex(2.0, 3.0)
>>> y = Complex(3.0, 6.0)
>>> x / y
0.0329218106996 + 0.00411522633745i
>>> abs(x)
3.1462643699419726
>>> x * y
-12.0 + 21.0i
>>> 
  • Soluție (se bazează pe exemplul cu RationalNumber):
    class Complex:
        def __init__(self, real, imaginar = 0):
            self.real = real
            self.imaginar = imaginar
     
        def __add__(self, other):
            return Complex(self.real + other.real, self.imaginar + other.imaginar)
     
        def __sub__(self, other):
            return Complex(self.real - other.real, self.imaginar - other.imaginar)
     
        def __mul__(self, other):
            real = self.real * other.real - self.imaginar * other.imaginar
            imaginar = self.real * other.imaginar + self.imaginar * other.real
            return Complex(real, imaginar)
     
        def __div__(self, other):
            other_conj = other.real ** other.imaginar
            real = (self.real * other.real + self.imaginar * other.imaginar) / other_conj
            imaginar = (self.real * other.imaginar - self.imaginar * other.real) / other_conj
            return Complex(real, imaginar)
     
        def __abs__(self):
            return self.real ** .5 + self.imaginar ** .5
     
        def __str__(self):
            return "%s + %si" % (self.real, self.imaginar)
     
        __repr__ = __str__

5. Avand o clasa Persoana, faceti in asa fel incat atunci cand este printata sau reprezentata la consola instanta unei clase, sa afiseze un mesaj ca cel de mai jos. In mod implicit python va afisa ceva de genul <__main__.Person object at 0x1004a94d0>.

class Person(object):
    def __init__(self, name):
        pass
    #TODO
 
>>> p = Person('gicu')
>>> p
My name is gicu
>>> print p
My name is gicu
  • Soluție (se bazează pe exemplul cu RationalNumber):
    class Person(object):
        def __init__(self, name):
            self.name = name
        def __str__(self):
            return 'My name is %s' % self.name
        __repr__ = __str__

6. Implementati o clasa ale caror instante sa se comporte ca mai jos.

(Hint: citi mai multe despre __call__ aici, aici)

class Persoana(object):
    def __init__(self, name):
        pass
    # TODO
 
>>> p = Persoana('Gigel')
>>> p()
Da? Ma numesc Gigel
  • Soluție
    class Persoana(object):
        def __init__(self, name):
            self.name = name
        def __call__(self):
            print('Da? Ma numesc %s' % self.name)

7. Creati o clasa `Zar` care intoarce un numar random intre 1 si 6 la apelarea metodei `roll`. Creati o alta clasa `ZarNecinstit` care intoarce intotdeauna ceva intre 1 si 6 care nu e random, ci fix, la apelarea metodei `roll`. Creati o lista de 5 zaruri atat corecte si incorecte si iterati pe ele apeland metoda `roll` (duck typing).

Bonus!

8. Implementat o clasa care de oricate ori e instantiata, se va intoarce aceeasi instanta mereu (singleton). Pentru asta ar fi bine sa cititi ce este un __metaclass__ in python aici.

class Person(object):
    # TODO
 
>>> p1 = Person()
>>> p2 = Person()
>>> p1
<__main__.Person object at 0x1004a94d0>
# Aceeasi zona de memorie 0x1004a94d0!
>>> p2
<__main__.Person object at 0x1004a94d0>
>>> p1 == p2
True
  • Soluție
    class Singleton(type):
        _instance = {}
        def __call__(cls, *args, **kwargs):
            if cls not in cls._instance:
                cls._instance[cls] = super(Singleton, cls).__call__(*args, **kwargs)
            return cls._instance[cls]
     
    class Person(object):
        __metaclass__ = Singleton
poo.txt · Last modified: 2013/12/27 09:32 by mihait