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