MySQL Java JavaScript PHP Python HTML-CSS C-sharp

Încapsularea, atributele și proprietățile

Implicit, atributele din clase sunt publice, ceea ce înseamnă că putem accesa și modifica atributele unui obiect din orice loc al programului. De exemplu:

class Person:
   def __init__(self, name, age):
       self.name = name    # stabilim numele
       self.age = age      # stabilim vârsta
               
   def print_person(self):
       print(f"Nume: {self.name}\tVârstă: {self.age}")
       
tom = Person("Tom", 39)
tom.name = "Omul-Păianjen"       # schimbăm atributul name
tom.age = -129                  # schimbăm atributul age
tom.print_person()              # Nume: Omul-Păianjen     Vârstă: -129

În acest caz, putem, de exemplu, să atribuim un nume sau o vârstă incorectă, cum ar fi o vârstă negativă. Un astfel de comportament este nedorit, de aceea apare necesitatea controlului accesului la atributele obiectului.

Această problemă este strâns legată de conceptul de încapsulare. Încapsularea este un concept fundamental al programării orientate pe obiecte, care presupune ascunderea funcționalității și prevenirea accesului direct din exterior.

Limbajul de programare Python permite definirea atributelor private sau închise. Pentru aceasta, numele atributului trebuie să înceapă cu două liniuțe de subliniere - __name. De exemplu, rescriem programul anterior, făcând ambele atribute - name și age private:

class Person:
   def __init__(self, name, age):
       self.__name = name    # stabilim numele
       self.__age = age      # stabilim vârsta
                 
   def print_person(self):
       print(f"Nume: {self.__name}\tVârstă: {self.__age}")
         
tom = Person("Tom", 39)
tom.__name = "Omul-Păianjen"     # încercăm să schimbăm atributul __name
tom.__age = -129                 # încercăm să schimbăm atributul __age
tom.print_person()               # Nume: Tom        Vârstă: 39

În principiu, putem încerca să stabilim pentru atributele __name și __age noi valori:

tom.__name = "Omul-Păianjen"     # încercăm să schimbăm atributul __name
tom.__age = -129                 # încercăm să schimbăm atributul __age

Dar afișarea metodei print_person va arăta că atributele obiectului nu și-au schimbat valorile:

tom.print_person()       # Nume: Tom        Vârstă: 39

Cum funcționează acest lucru? La declararea unui atribut al cărui nume începe cu două liniuțe, de exemplu __attribute, Python definește în realitate un atribut care se numește după modelul _ClassName__attribute. Deci, în cazul de mai sus, se vor crea atributele _Person__name și _Person__age. Astfel, putem accesa aceste atribute doar din aceeași clasă, nu și din exteriorul acestei clase. De exemplu, atribuirea unei valori acestui atribut nu va avea niciun efect:

tom.__age = 43

Pentru că în acest caz doar se definește dinamic un nou atribut __age, dar acesta nu are nimic de-a face cu atributul self.__age sau, mai precis, self._Person__age.

Și încercarea de a obține valoarea lui va duce la o eroare de execuție (dacă anterior nu a fost definită variabila __age):

print(tom.__age)

Totuși, intimitatea atributelor aici este relativă. De exemplu, putem folosi numele complet al atributului:

class Person:
   def __init__(self, name, age):
       self.__name = name    # stabilim numele
       self.__age = age      # stabilim vârsta
                 
   def print_person(self):
       print(f"Nume: {self.__name}\tVârstă: {self.__age}")
         
tom = Person("Tom", 39)
tom._Person__name = "Omul-Păianjen"     # schimbăm atributul __name
tom.print_person()              # Nume: Omul-Păianjen        Vârstă: 39

Totuși, autorul codului exterior trebuie să ghicească cum se numesc atributele.

Metode de acces. Getteri și setteri

Poate apărea întrebarea cum să accesăm astfel de atribute private. Pentru aceasta, se folosesc de obicei metode speciale de acces. Getterul permite obținerea valorii atributului, iar setterul permite stabilirea acestuia. Astfel, modificăm clasa definită anterior, definind metode de acces:

class Person:
   def __init__(self, name, age):
       self.__name = name    # stabilim numele
       self.__age = age      # stabilim vârsta

   # setter pentru stabilirea vârstei
   def set_age(self, age):
       if 0 < age < 110:
           self.__age = age
       else:
           print("Vârstă neacceptabilă")

   # getter pentru obținerea vârstei
   def get_age(self):
       return self.__age

   # getter pentru obținerea numelui
   def get_name(self):
       return self.__name
   
   def print_person(self):
       print(f"Nume: {self.__name}\tVârstă: {self.__age}")
         
tom = Person("Tom", 39)
tom.print_person()  # Nume: Tom  Vârstă: 39
tom.set_age(-3486)  # Vârstă neacceptabilă
tom.set_age(25)
tom.print_person()  # Nume: Tom  Vârstă: 25

Pentru obținerea valorii vârstei se folosește metoda get_age:

def get_age(self):
   return self.__age

Pentru schimbarea vârstei este definită metoda set_age:

def set_age(self, age):
   if 0 < age < 110:
       self.__age = age
   else:
       print("Vârstă neacceptabilă")

Accesul la atribute prin metode permite adăugarea unei logici suplimentare. Astfel, în funcție de vârsta transmisă, putem decide dacă trebuie să modificăm vârsta, deoarece valoarea transmisă poate fi incorectă.

De asemenea, nu este necesar să creăm o pereche de metode pentru fiecare atribut privat. Astfel, în exemplul de mai sus, numele persoanei poate fi stabilit doar din constructor. Iar pentru obținerea numelui este definită metoda get_name.

Anotări pentru proprietăți

Am văzut mai sus cum să creăm metode de acces. Dar Python are și o altă metodă - mai elegantă - de a gestiona proprietățile. Această metodă presupune folosirea unor anotări, care sunt precedate de simbolul @.

Pentru a crea o proprietate-getter, se pune anotarea @property deasupra proprietății.

Pentru a crea o proprietate-setter, se pune anotarea nume_proprietate_getter.setter deasupra proprietății.

Rescriem clasa Person folosind anotările:

class Person:
   def __init__(self, name, age):
       self.__name = name    # stabilim numele
       self.__age = age      # stabilim vârsta

   # proprietate-getter
    @property
   def age(self):
       return self.__age
   
   # proprietate-setter
    @age.setter
   def age(self, age):
       if 0 < age < 110:
           self.__age = age
       else:
           print("Vârstă neacceptabilă")

    @property
   def name(self):
       return self.__name
   
   def print_person(self):
       print(f"Nume: {self.__name}\tVârstă: {self.__age}")
         
tom = Person("Tom", 39)
tom.print_person()  # Nume: Tom  Vârstă: 39
tom.age = -3486     # Vârstă neacceptabilă  (Accesarea setter-ului)
print(tom.age)      # 39 (Accesarea getter-ului)
tom.age = 25        # (Accesarea setter-ului)
tom.print_person()  # Nume: Tom  Vârstă: 25

În primul rând, trebuie observat că proprietatea-setter se definește după proprietatea-getter.

În al doilea rând, atât setter-ul, cât și getter-ul au același nume - age. Și pentru că getter-ul se numește age, asupra setter-ului se pune anotarea @age.setter.

După aceasta, atât la getter, cât și la setter, ne referim prin expresia tom.age.

Astfel, putem defini doar getter-ul, ca în cazul proprietății name - acesta nu poate fi schimbat, ci doar obținută valoarea lui.

← Lecția anterioară Lecția următoare →