Наследование. Атрибуты private и protected

Курс по Python ООП: https://stepik.org/a/116336

Мы продолжаем изучение темы «наследование». На этом занятии мы увидим, как влияет режим доступа private и protected атрибутов при наследовании классов.

Ранее мы с вами об этом уже говорили и в частности отмечали, что:

  • _attribute (с одним подчеркиванием) – режим доступа protected (служит для обращения внутри класса и во всех его дочерних классах)
  • __attribute (с двумя подчеркиваниями) – режим доступа private (служит для обращения только внутри класса).

Давайте посмотрим, как ведут себя атрибуты с этими режимами доступа при наследовании. Возьмем пример из предыдущего занятия с двумя классами:

class Geom:
    name = 'Geom'
 
    def __init__(self, x1, y1, x2, y2):
        print(f"инициализатор Geom для {self.__class__}")
        self.__x1 = x1
        self.__y1 = y1
        self.__x2 = x2
        self.__y2 = y2
 
 
class Rect(Geom):
    def __init__(self, x1, y1, x2, y2, fill='red'):
        super().__init__(x1, y1, x2, y2)
        self.__fill = fill

Здесь мы пытаемся в инициализаторе базового класса Geom сформировать приватные локальные свойства с координатами прямоугольника. Дополнительно в инициализаторе самого класса создается приватное свойство __fill.

Ниже создадим объект класса Rect:

r = Rect(0, 0, 10, 20)

и выведем все его локальные атрибуты в консоль:

print(r.__dict__)

После запуска программы увидим следующие строчки:

инициализатор Geom для <class '__main__.Rect'>
{'_Geom__x1': 0, '_Geom__y1': 0, '_Geom__x2': 10, '_Geom__y2': 20, '_Rect__fill': 'red'}

Смотрите, локальные свойства с координатами имеют префикс _Geom, то есть, префикс того класса, в котором они непосредственно были прописаны. Несмотря на то, что параметр self является ссылкой на объект класса Rect. Это особенность поведения (формирования) приватных атрибутов в базовых классах. У них всегда добавляется префикс именно базового класса, а не класса объекта self. А вот последнее свойство __fill имеет ожидаемый префикс _Rect, так как оно было создано в классе Rect.

Что из этого следует? Во-первых, мы, конечно же, не можем обратиться в свойствам-координатам в дочернем классе Rect. Если в нем прописать метод get_coords():

    def get_coords(self):
        return (self.__x1, self.__y1, self.__x2, self.__y2)

а, затем, вызвать через объект класса Rect:

r.get_coords()

то увидим ошибку AttributeError. Но если перенести этот метод в базовый класс Geom, то все сработает без ошибок, так как приватным свойствам будет добавлен правильный префикс _Geom.

Возможно, вам кажется это немного запутанным? Но давайте вспомним, а для чего вообще нужны и когда используются приватные атрибуты. Мы говорили, что это закрытые от внешнего вмешательства свойства или методы текущего класса, доступные только внутри этого класса и недоступные из других, в том числе и из дочерних классов. Именно поэтому приватные атрибуты жестко привязываются к текущему классу, в котором они создаются, так как по логике предполагается их использовать только внутри этого класса и больше нигде.

Если же нам нужно определить закрытые атрибуты, доступные в текущем классе и во всех его дочерних классах, то для этого следует использовать метод определения protected – одно нижнее подчеркивание. Поэтому правильнее было бы создавать свойства-координаты в базовом инициализаторе в режиме protected:

class Geom:
    name = 'Geom'
 
    def __init__(self, x1, y1, x2, y2):
        print(f"инициализатор Geom для {self.__class__}")
        self._x1 = x1
        self._y1 = y1
        self._x2 = x2
        self._y2 = y2
 
 
class Rect(Geom):
    def __init__(self, x1, y1, x2, y2, fill='red'):
        super().__init__(x1, y1, x2, y2)
        self._fill = fill
 
    def get_coords(self):
        return (self._x1, self._y1, self._x2, self._y2)

Тогда никаких проблем с доступом уже не возникает:

r = Rect(0, 0, 10, 20)
print(r.__dict__)
r.get_coords()

После запуска программы увидим следующие строчки:

инициализатор Geom для <class '__main__.Rect'>
{'_x1': 0, '_y1': 0, '_x2': 10, '_y2': 20, '_fill': 'red'}

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

print(r._x1)

Никаких ошибок не будет. Нижнее подчеркивание лишь предупреждает (сигнализирует) программиста о защищенном атрибуте, к которому напрямую лучше не обращаться. Этот атрибут был создан для внутренней логики работы алгоритма в классе и не предназначен для обращения извне. В дальнейшем это может привести к проблемам, например, при изменении версии класса, в котором такого атрибута уже не будет, или он будет играть другую роль и т.п.

Атрибуты private и protected на уровне класса

Все также работает и с атрибутами уровня класса. Например, сейчас мы совершенно спокойно можем обратиться к свойству name класса Geom через объект класса Rect:

print(r.name)

Добавив одно нижнее подчеркивание, функционал останется прежним, мы лишь отметим, что к этой переменной извне лучше не обращаться:

print(r._name)

Но, если прописать два подчеркивания, то доступ будет закрыт всюду, кроме самого класса Geom:

print(r.__name)

или так:

class Rect(Geom):
    def __init__(self, x1, y1, x2, y2, fill='red'):
        super().__init__(x1, y1, x2, y2)
        self._fill = fill
        self._name = self.__name

Но в Geom мы можем к ней обращаться:

class Geom:
    __name = 'Geom'
 
    def __init__(self, x1, y1, x2, y2):
        print(f"инициализатор {self.__name}")
        self._x1 = x1
        self._y1 = y1
        self._x2 = x2
        self._y2 = y2

Те же ограничения доступа можно накладывать и на методы. Если в базовом классе Geom определить приватный метод, например, для проверки корректности значений координат:

class Geom:
    ...
 
    def __verify_coord(self, coord):
        return 0 <= coord <= 100

то он будет доступен только внутри этого класса и вызвать его, скажем, в дочернем классе Rect уже не получится:

class Rect(Geom):
    def __init__(self, x1, y1, x2, y2, fill='red'):
        super().__init__(x1, y1, x2, y2)
        super().__verify_coord(x1)
        self._fill = fill

Кстати, этот пример также показывает, что приватность запрещает переопределение методов в дочерних классах. Если же у метода прописать только одно подчеркивание, то его можно будет вызывать во всех дочерних классах.

Я, надеюсь, вы теперь знаете, как работают режимы доступа атрибутов при наследовании классов и сможете их грамотно применять в своих программах.

Курс по Python ООП: https://stepik.org/a/116336

Видео по теме