По умолчанию атрибуты в классах являются общедоступными, а это значит, что из любого места программы мы можем получить атрибут объекта и изменить его. Например:
class Person:
def __init__(self, name, age):
self.name = name # устанавливаем имя
self.age = age # устанавливаем возраст
def print_person(self):
print(f"Имя: {self.name}\tВозраст: {self.age}")
tom = Person("Tom", 39)
tom.name = "Человек-паук" # изменяем атрибут name
tom.age = -129 # изменяем атрибут age
tom.print_person() # Имя: Человек-паук Возраст: -129
Но в данном случае мы можем, к примеру, присвоить возрасту или имени человека некорректное значение, например, указать отрицательный возраст. Подобное поведение нежелательно, поэтому встает вопрос о контроле за доступом к атрибутам объекта.
С данной проблемой тесно связано понятие инкапсуляции. Инкапсуляция является фундаментальной концепцией объектно-ориентированного программирования, которая предполагает скрытие функционала и предотвращение прямого доступа извне к нему.
Язык программирования Python позволяет определить приватные или закрытые атрибуты. Для этого имя атрибута должно начинаться с двойного подчеркивания - __name.
Например, перепишем предыдущую программу, сделав оба атрибута - name и age приватными:
class Person:
def __init__(self, name, age):
self.__name = name # устанавливаем имя
self.__age = age # устанавливаем возраст
def print_person(self):
print(f"Имя: {self.__name}\tВозраст: {self.__age}")
tom = Person("Tom", 39)
tom.__name = "Человек-паук" # пытаемся изменить атрибут __name
tom.__age = -129 # пытаемся изменить атрибут __
tom.print_person() # Имя: Tom Возраст: 39
В принципе мы также можем попытаться установить для атрибутов __name и __age новые значения:
tom.__name = "Человек-паук" # пытаемся изменить атрибут __name tom.__age = -129 # пытаемся изменить атрибут __
Но вывод метода print_person покажет, что атрибуты объекта не изменили свои значения:
tom.print_person() # Имя: Tom Возраст: 39
Как это работает? При объявлении атрибута, имя которого начинается с двух прочерков, например, __attribute, Python в реальности определяет атрибута, который называется по шаблону
_ClassName__atribute. То есть в случае выше будут создаваться атрибуты _Person__name и _Person__age. Поэтому к такому атрибуту мы сможем обратиться только из того же класса.
Но не сможем обратиться вне этого класса. Например, присвоение значения этому атрибуту ничего не даст:
tom.__age = 43
Потому что в данном случае просто определяется динамически новый атрибут __age, но это он не имеет ничего общего с атрибутом self.__age или точнее self._Person__age.
А попытка получить его значение приведет к ошибке выполнения (если ранее не была определена переменная __age):
print(tom.__age)
Тем не менее приватность атрибутов тут довольно относительна. Например, мы можем использовать полное имя атрибута:
class Person:
def __init__(self, name, age):
self.__name = name # устанавливаем имя
self.__age = age # устанавливаем возраст
def print_person(self):
print(f"Имя: {self.__name}\tВозраст: {self.__age}")
tom = Person("Tom", 39)
tom._Person__name = "Человек-паук" # изменяем атрибут __name
tom.print_person() # Имя: Человек-паук Возраст: 39
Тем нее менее автор внешнего кода еще должен угадать, как называются атрибуты.
Может возникнуть вопрос, как обращаться к подобным приватным атрибутам. Для этого обычно применяются специальные методы доступа. Геттер позволяет получить значение атрибута, а сеттер установить его. Так, изменим выше определенный класс, определив в нем методы доступа:
class Person:
def __init__(self, name, age):
self.__name = name # устанавливаем имя
self.__age = age # устанавливаем возраст
# сеттер для установки возраста
def set_age(self, age):
if 0 < age < 110:
self.__age = age
else:
print("Недопустимый возраст")
# геттер для получения возраста
def get_age(self):
return self.__age
# геттер для получения имени
def get_name(self):
return self.__name
def print_person(self):
print(f"Имя: {self.__name}\tВозраст: {self.__age}")
tom = Person("Tom", 39)
tom.print_person() # Имя: Tom Возраст: 39
tom.set_age(-3486) # Недопустимый возраст
tom.set_age(25)
tom.print_person() # Имя: Tom Возраст: 25
Для получения значения возраста применяется метод get_age:
def get_age(self):
return self.__age
Для изменения возраста определен метод set_age:
def set_age(self, age):
if 0 < age < 110:
self.__age = age
else:
print("Недопустимый возраст")
Причем опосредование доступа к атрибутам через методы позволяет задать дополнительную логику. Так, в зависимости от переданного возраста мы можем решить, надо ли переустанавливать возраст, так как переданное значение может быть некорректным.
Также необязательно создавать для каждого приватного атрибута подобную пару методов. Так, в примере выше имя человека мы можем установить только из конструктора. А для получение определен метод get_name.
Выше мы рассмотрели, как создавать методы доступа. Но Python имеет также еще один - более элегантный способ - свойства. Этот способ предполагает использование аннотаций, которые предваряются символом @.
Для создания свойства-геттера над свойством ставится аннотация @property.
Для создания свойства-сеттера над свойством устанавливается аннотация имя_свойства_геттера.setter.
Перепишем класс Person с использованием аннотаций:
class Person:
def __init__(self, name, age):
self.__name = name # устанавливаем имя
self.__age = age # устанавливаем возраст
# свойство-геттер
@property
def age(self):
return self.__age
# свойство-сеттер
@age.setter
def age(self, age):
if 0 < age < 110:
self.__age = age
else:
print("Недопустимый возраст")
@property
def name(self):
return self.__name
def print_person(self):
print(f"Имя: {self.__name}\tВозраст: {self.__age}")
tom = Person("Tom", 39)
tom.print_person() # Имя: Tom Возраст: 39
tom.age = -3486 # Недопустимый возраст (Обращение к сеттеру)
print(tom.age) # 39 (Обращение к геттеру)
tom.age = 25 # (Обращение к сеттеру)
tom.print_person() # Имя: Tom Возраст: 25
Во-первых, стоит обратить внимание, что свойство-сеттер определяется после свойства-геттера.
Во-вторых, и сеттер, и геттер называются одинаково - age. И поскольку геттер называется age, то над сеттером устанавливается аннотация @age.setter.
После этого, что к геттеру, что к сеттеру, мы обращаемся через выражение tom.age.
При этом можно определить только геттер, как в случае с свойством name - его нельзя изменить, а можно лишь получить значение.