По умолчанию атрибуты в классах являются общедоступными, а это значит, что из любого места программы мы можем получить атрибут объекта и изменить его. Например:
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 - его нельзя изменить, а можно лишь получить значение.