Сообщить об ошибке.

Функция super() в Python, доступ к унаследованным методам

Обеспечивает доступ к оригиналам наследованных методов

Синтаксис:

super(type, object-or-type)

Параметры:

  • type - необязательно, тип, от которого начинается поиск объекта-посредника
  • object-or-type - необязательно, тип или объект, определяет порядок разрешения метода для поиска

Возвращаемое значение:

  • объект-посредник, делегирующий вызовы методов родителю или собрату класса.

Описание:

Функция super(), возвращает объект-посредник, который делегирует вызовы метода родительскому или родственному классу, указанного type типа. Это полезно для доступа к унаследованным методам, которые были переопределены в классе.

object-or-type определяет порядок разрешения метода __mro__ для поиска. Поиск начинается с класса, сразу после указанного типа. Например, если __mro__ - это D -> B -> C -> A -> object, а значение type=B, то super() выполняет поиск объекта C -> A -> object.

  • Если object-or-type не указан, то возвращается несвязанный объект-посредник.
  • Если object-or-type является объектом (экземпляром), то будет получен посредник, для которого isinstance(obj, type) возвращает True.
  • Если object-or-type является типом (классом), то будет получен посредник, для которого issubclass(subtype, type) возвращает True.

Используя функцию super() с обоими аргументами, можно точно определить объекты и сделать соответствующие ссылки (смотрите самый последний пример). Без аргументов функция super() работает только внутри определения класса, а необходимые детали для идентификации класса и доступа к текущему экземпляру для методов заполняет компилятор.

В дополнение к поиску методов, функция super() также работает при поиске атрибутов. Одним из вариантов использования этого является вызов дескрипторов в родительском или родственном классе.

Обратите внимание, что super() реализована как часть процесса привязки для явного поиска по атрибутам, таких как super().__getitem__(name). Это достигается путем реализации собственного метода __getattribute__() для поиска классов в предсказуемом порядке, который поддерживает множественное наследование __mro__. Функция super() не предназначена для неявных поисков с использованием инструкций или операторов, таких как super()[name].

Типичные случаи использования super().

Функция super() используется для обращения к родительским классам в иерархиях с единичным наследованием, чтобы явно не указывать их имена, что упрощает поддержку кода в дальнейшем.

Совсем простой пример для понимания о чем речь:

>>> class A:
...     def some_method(self):
...         print('some_method A')
... 
>>> class B(A):
...     def some_method(self):
...         print('some_method B')
... 
>>> x = B()
>>> x.some_method()
# some_method B

Здесь перегружен метод родительского класса. Но что если необходимо только дополнить родительский метод (изменить его поведение), не копируя его полностью? Тут и нужна функция super():

class A:
    def some_method(self):
        print('some_method A')

class B(A):
    def some_method(self):
        # вызываем метод родительского класса
        super().some_method()
        # Добавляем свое поведение
        print('some_method B')

>>> x = B()
>>> x.some_method()
# some_method A
# some_method B

Более практичный пример.

class LoggingDict(dict):
      def __setitem__(self, key, value):
          logging.info('Setting %r to %r' % (key, value))
          super().__setitem__(key, value)

Класс LoggingDict имеет все те же возможности, что и его родитель dict, плюс расширяет метод __setitem__ для логирования каждого обновлении ключа. После внесения записи в журнал метод использует super() для делегирования работы по фактическому обновлению словаря с помощью пары ключ/значение.

До появления super() нужно было жестко связывать вызов с dict.__setitem__(self, key, value). Функция super() лучше, т. к. это вычисляемая косвенная ссылка.

Одним из преимуществ косвенности является то, что не нужно указывать класс делегата по имени. Чтобы переключить базовый класс на какое-либо другое сопоставление (вместо dict), то ссылка super() автоматически переключится на новый родительский класс.

# используем новый базовый 
# класс SomeOtherMapping
class LoggingDict(SomeOtherMapping):
    def __setitem__(self, key, value):
        logging.info('Setting %r to %r' % (key, value))
        # никаких изменений не требуется
        super().__setitem__(key, value)

В дополнение к изоляции изменений, у вычисляемой косвенности есть еще одно важное преимущество, которое может быть незнакомо людям, пришедшим из статических языков. Так как косвенность вычисляется во время выполнения, то есть свобода влиять на вычисление, чтобы косвенность указывала на какой-то другой класс.

Расчет зависит как от класса, в котором вызывается super(), так и от "дерева предков" экземпляра. Первый компонент определяется исходным кодом этого класса, в котором вызывается super(). В примере, функция super() вызывается в методе LoggingDict.__setitem__. Этот компонент фиксирован. Второй компонент, более интересный и носит переменный характер (можно создавать новые подклассы с богатым деревом предков).

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

class LoggingOD(LoggingDict, collections.OrderedDict):
    pass

И это все, исходный код не изменялся. "Дерево-предок" для нового класса: LoggingOD, LoggingDict, OrderedDict, dict, object. Важным результатом является то, что OrderedDict был вставлен после LoggingDict и перед dict! Это означает, что вызов super() в LoggingDict.__setitem__ теперь отправляет обновление ключа/значения в OrderedDict, а не в dict.

Задумайтесь как удобно, был создан подкласс LoggingOD, единственная логика которого состоит в том, чтобы правильно составить два существующих класса и управлять "порядком их поиска".

Порядок поиска или дерево предков - это официально известно как порядок разрешения методов или MRO. Легко просмотреть MRO, распечатав атрибут __mro__:

>>> pprint(LoggingOD.__mro__)
# (<class '__main__.LoggingOD'>,
#  <class '__main__.LoggingDict'>,
#  <class 'collections.OrderedDict'>,
#  <class 'dict'>,
#  <class 'object'>)

Практические советы.

Функция super() занимается делегированием вызовов методов некоторому классу в дереве предков экземпляра. Чтобы переупорядочиваемые вызовы методов работали, классы должны разрабатываться совместно. При этом возникают три легко решаемые практические задачи:

  1. Метод, вызываемый super(), должен существовать.
  2. Вызывающий и вызываемый должны иметь совпадающую подпись аргументов.
  3. Каждое вхождение метода должно использовать super().

Рассмотрим стратегии получения аргументов вызывающего объекта, чтобы они соответствовали сигнатуре вызываемого метода. Это немного сложнее, чем традиционные вызовы методов, когда вызываемый объект известен заранее. При использовании super() вызываемый объект неизвестен во время написания класса (т.к. подкласс, написанный позже, может ввести новые классы в "MRO").

Один из подходов состоит в том, чтобы придерживаться фиксированной подписи, используя позиционные аргументы. Это хорошо работает с такими методами, как __setitem__, которые имеют фиксированную сигнатуру из двух аргументов, ключа и значения. Этот метод показан в примере выше LoggingDict, где __setitem__ имеет одинаковую подпись как в LoggingDict, так и в dict.

Более гибкий подход состоит в том, чтобы каждый метод в дереве предков был разработан совместно. Другими словами, должен принимать ключевые аргументы и словарь ключевых аргументов **kwargs, удалять любые аргументы, которые ему нужны, и перенаправлять оставшиеся аргументы с помощью **kwargs, в конечном итоге оставляя словарь пустым для последнего вызова в цепочке.

Каждый уровень удаляет ключевые аргументы, которые ему нужны, чтобы окончательный пустой словарь можно было отправить методу, который вообще не ожидает аргументов (например, у метода object.__init__ нет аргументов):

# test.py
class Shape:
    def __init__(self, shapename, **kwds):
        self.shapename = shapename
        super().__init__(**kwds)        

class ColoredShape(Shape):
    def __init__(self, color, **kwds):
        self.color = color
        super().__init__(**kwds)

# $ python -i test.py
>>> cs = ColoredShape(color='red', shapename='circle')

С сигнатурой разобрались, а как убедиться, что целевой метод существует?

В приведенном выше примере показан простейший случай. Известно, что у любого объекта в Python есть метод object.__init__, и этот объект всегда является последним классом в цепочке MRO, поэтому любая последовательность вызовов super().__init__ гарантированно заканчивается вызовом метода object.__init__. Другими словами, есть гарантия того, что цель вызова super().__init__() существует и не вызовет ошибку AttributeError.

Для случаев, когда объект не имеет интересующего метода (например, метода .draw()), необходимо написать некий класс Root, который гарантированно будет вызываться перед object. Ответственность класса Root состоит в том, чтобы просто съесть вызов метода без переадресации вызова с помощью super().

Также метод Root.draw() может использовать утверждение, которое будет гарантировать, что он не маскирует какой-либо другой метод .draw() позже в цепочке. Это может произойти, если подкласс ошибочно включает класс, который имеет метод .draw(), но не наследуется от Root:

# test.py
class Root:
    def draw(self):
        # цепочка делегирования остановится здесь
        assert not hasattr(super(), 'draw')

class Shape(Root):
    def __init__(self, shapename, **kwds):
        self.shapename = shapename
        super().__init__(**kwds)
    def draw(self):
        print('Рисование. Установка формы на:', self.shapename)
        super().draw()

class ColoredShape(Shape):
    def __init__(self, color, **kwds):
        self.color = color
        super().__init__(**kwds)
    def draw(self):
        print('Рисование. Установка цвета на:', self.color)
        super().draw()

# $ python -i test.py
>>> cs = ColoredShape(color='blue', shapename='square')
>>> cs.draw()
# Рисование. Установка цвета на: blue
# Рисование. Установка формы на: square

Если подклассы внедряют другие классы в MRO, эти другие классы также должны наследоваться от Root, чтобы ни один путь поиска вызова .draw() не мог достичь object, не будучи остановленным Root.draw(). Это должно быть четко задокументировано, чтобы тот, кто пишет новые взаимодействующие классы, знал, что это подкласс от Root. Это ограничение мало чем отличается от собственного требования Python, согласно которому все новые исключения должны наследоваться от BaseException.

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

Примеры получения доступа к унаследованным методам.

Использование функции super() c единичным наследовании:

class Computer():
    def __init__(self, computer, ram, ssd):
        self.computer = computer
        self.ram = ram
        self.ssd = ssd

# Если создать дочерний класс `Laptop`, то будет доступ 
# к свойству базового класса благодаря функции super().
class Laptop(Computer):
    def __init__(self, computer, ram, ssd, model):
        super().__init__(computer, ram, ssd)
        self.model = model


lenovo = Laptop('lenovo', 2, 512, 'l420')

print('This computer is:', lenovo.computer)
print('This computer has ram of', lenovo.ram)
print('This computer has ssd of', lenovo.ssd)
print('This computer has this model:', lenovo.model)
# Вывод
# This computer is: lenovo
# This computer has ram of 2
# This computer has ssd of 512
# This computer has this model: l420

В следующем примере класс Rectangle является суперклассом, а Square является подклассом, поскольку методы Square наследуются от Rectangle, то мы можем вызвать метод __init __() суперкласса (Rectangle.__ init __()) из класса Square используя функцию super(). Далее просто пользоваться методами родителя, не написав ни строчки кода. В данном случае квадрат - это частный случай прямоугольника...

class Rectangle:
    def __init__(self, length, width):
        self.length = length
        self.width = width

    def area(self):
        return self.length * self.width

    def perimeter(self):
        return 2 * self.length + 2 * self.width

class Square(Rectangle):
    def __init__(self, length):
        # Для квадрата просто нужно передать один параметр length.
        # При вызове 'super().__init__()' установим атрибуты 'length' и 'width'.
        super().__init__(length, length)


# Класс 'Square' явно не реализует метод 'area()' и 
# будет использовать его из суперкласса 'Rectangle'
sqr = Square(4)
print("Area of Square is:", sqr.area())
# Area of Square is: 16

rect = Rectangle(2, 4)
print("Area of Rectangle is:", rect.area())
# Area of Rectangle is: 8

И наконец пример работы функции super() при использовании совместного множественного наследования в динамическом окружении:

class A:
    def __init__(self):
        print('Initializing: class A')

    def sub_method(self, b):
        print('sub_method from class A:', b)


class B(A):
    def __init__(self):
        print('Initializing: class B')
        super().__init__()

    def sub_method(self, b):
        print('sub_method from class B:', b)
        super().sub_method(b + 1)

class X(B):
    def __init__(self):
        print('Initializing: class X')
        super().__init__()

    def sub_method(self, b):
        print('sub_method from class X:', b)
        super().sub_method(b + 1)


class Y(X):
    def __init__(self):
        print('Initializing: class Y')
        # super() с параметрами
        super(X, self).__init__()

    def sub_method(self, b):
        print('sub_method from class Y:', b)
        super().sub_method(b + 1)


x = X()
x.sub_method(1)
print('Обратите внимание как происходит инициализация')
print('классов при указании аргументов в функции super()')
y = Y()
y.sub_method(5)

# Вывод

# Initializing: class X
# Initializing: class B
# Initializing: class A
# sub_method from class X: 1
# sub_method from class B: 2
# sub_method from class A: 3
# обратите внимание как происходит инициализация
# классов при указании аргументов в функции super()
# Initializing: class Y
# Initializing: class B
# Initializing: class A
# sub_method from class Y: 5
# sub_method from class X: 6
# sub_method from class B: 7
# sub_method from class A: 8