Инкапсуляция, атрибуты и свойства

Последнее обновление: 27.03.2024

По умолчанию атрибуты в классах являются общедоступными, а это значит, что из любого места программы мы можем получить атрибут объекта и изменить его. Например:

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 - его нельзя изменить, а можно лишь получить значение.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850