Шаг 136.
Python: сборник рецептов.
Классы и объекты. Вызов метода родительского класса

    На этом шаге мы рассмотрим особенности использования функции super().

Задача

    Вы хотите вызвать метод родительского класса вместо метода, который был переопределен в подклассе.

Решение

    Чтобы вызвать метод из родительского класса (суперкласса), используйте функцию super(). Например:

class A:
    def spam(self):
        print('A.spam')

class B(A):
    def spam(self):
        print('B.spam')
        super().spam()      # Вызов spam() родителя

    Очень распространенный случай использования super() - это применение ее к методу __init__(), чтобы убедиться в правильной инициализации родителей:

class A:
    def __init__(self):
        self.x = 0

class B(A):
    def __init__(self):
        super().__init__()
        self.y = 1

    Также super() часто используется в коде, который переопределяет один из специальных методов Python. Например:

class Proxy:
    def __init__(self, obj):
        self._obj = obj

    # Передача поиска атрибута внутреннему obj
    def __getattr__(self, name):
        return getattr(self._obj, name)

    # Передача присвоения атрибута
    def __setattr__(self, name, value):
        if name.startswith('_'):
            super().__setattr__(name, value)    # Вызов изначального __setattr__
        else:
            setattr(self._obj, name, value)

    В этом коде реализация __setaddr__() включает проверку имени. Если имя начинается с нижнего подчеркивания (_), он вызывает изначальную реализацию __setaddr__() через использование super(). В противном случае оно делегируется внутреннему объекту self._obj. Это выглядит немного странно, но super() работает, даже если явно не указан базовый класс.

Обсуждение

    Правильное использование функции super() - это один из самых трудных для понимания аспектов Python. Вы наверняка встретите код, который напрямую вызывает метод родительского класса:

class Base:
    def __init__(self):
        print('Base.__init__')

class A(Base):
    def __init__(self):
        Base.__init__(self)
        print('A.__init__')

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

>>> class Base:
    def __init__(self):
        print('Base.__init__')

>>> class A(Base):
    def __init__(self):
        Base.__init__(self)
        print('A.__init__')

>>> class B(Base):
    def __init__(self):
        Base.__init__(self)
        print('B.__init__')

>>> class C(A, B):
    def __init__(self):
        A.__init__(self)
        B.__init__(self)
        print('C.__init__')

    Если вы запустите эту программу, то увидите, что метод Base.__init__() вызывается дважды:

>>> c = C()
Base.__init__
A.__init__
Base.__init__
B.__init__
C.__init__
>>> 

    Дублирование вызова Base.__init__() может не нанести вреда, но может и все сломать. Если же вы измените код так, чтобы он использовал super(), все будет работать:

>>> class Base:
    def __init__(self):
        print('Base.__init__')

>>> class A(Base):
    def __init__(self):
        super().__init__()
        print('A.__init__')

>>> class B(Base):
    def __init__(self):
        super().__init__()
        print('B.__init__')

>>> class C(A, B):
    def __init__(self):
        super().__init__()     # Здесь вызов только super()
        print('C.__init__')

        

    При использовании этой новой версии вы обнаружите, что каждый метод __ init__() вызывается только один раз:

>>> c = C()
Base.__init__
B.__init__
A.__init__
C.__init__
>>> 

    Чтобы понять, почему это работает, мы должны сделать шаг назад и обсудить, как в Python реализовано наследование. Для каждого класса, который вы определите, Python вычисляет так называемый список порядка разрешения методов (ПРМ) (method resolution order (MRO)). Список ПРМ - это просто линейно упорядоченные базовые классы. Например:

>>> C.__mro__
(<class '__main__.C'>, <class '__main__.A'>, 
  <class '__main__.B'>, <class '__main__.Base'>, <class 'object'>)
>>>

    Чтобы реализовать наследование, Python начинает с самого левого класса и проходит по классам в списке ПРМ слева направо, пока не найдет нужный атрибут.

    Определение самого списка ПРМ происходит путем использования C3-линеаризации.


https://ru.wikipedia.org/wiki/C3-линеаризация.

    Не будем погружаться в математические тонкости, но по сути это сортировка слиянием ПРМ родительских классов с тремя условиями:

    Если честно, вам нужно знать только то, что порядок классов в списке ПРМ имеет значение для практически любой классовой иерархии, которую вы реализуете.

    Когда вы используете функцию super(), Python продолжает свой поиск, начиная со следующего класса в ПРМ. Пока каждый переопределенный метод использует super() и вызывает ее один раз, поток управления найдет свой путь через весь список ПРМ, и каждый метод будет вызван единожды. Вот почему вы не должны делать двойные вызовы Base.__init__() во втором примере.

    Аспект super(), который может удивить, - это то, что она не обязательно обращается к прямому родителю следующего в ПРМ класса, а также то, что вы можете использовать ее даже с классом, не имеющим прямого родителя. Рассмотрим, например, такой класс:

>>> class A:
	def spam(self):
		print('A.spam')
		super().spam()

		
>>>

    Если вы попробуете его использовать, то обнаружите, что он не работает:

>>> a = A()
>>> a.spam()
A.spam
Traceback (most recent call last):
  File "<pyshell#23>", line 1, in <module>
    a.spam()
  File "<pyshell#21>", line 4, in spam
    super().spam()
AttributeError: 'super' object has no attribute 'spam'
>>> 

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

>>> class B:
	def spam(self):
		print('B.spam')

		
>>> class C(A, B):
	pass

>>> c = C()
>>> c.spam()
A.spam
B.spam
>>> 

    Здесь вы видите, что использование super().spam() в классе A на самом деле вызвало метод spam() в классе B - классе, абсолютно никак не связанном с A! Это объясняется ПРМ класса C:

>>> C.__mro__
(<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>)
>>> 

    Использование super() таким способом наиболее часто можно встретить в классах-миксинах (mixin).

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

    Использование super() иногда становится источником дебатов в сообществе Python. Однако, при всех прочих равных, вам стоит использовать ее в современном коде. Рэймонд Хеттингер написал отличный пост "Python's super() Considered Super!", в котором вы найдете еще больше примеров того, почему super() может быть суперкрутой штукой.


https://rhettinger.wordpress.com/2011/05/26/super-considered-super/.

    На следующем шаге мы рассмотрим расширение свойства в подклассе.




Предыдущий шаг Содержание Следующий шаг