Python super guide

Базовый вариант использования

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

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

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

class LoggingDict(SomeOtherMapping):            # new base class
    def __setitem__(self, key, value):
        logging.info('Setting %r to %r' % (key, value))
        super().__setitem__(key, value)         # no change needed

Какой именно класс будет вызван зависит от того, как образовано дерево родительских классов. В примере ниже оно выглядит так: LoggingOD, LoggingDict, OrderedDict, dict, object - работа будет делегирована нужному нам методу в OrderedDict

class LoggingOD(LoggingDict, collections.OrderedDict):
    pass

Мы не изменяли исходный код LoggingDict. Вместо этого мы создали подкласс, единственная логика которого состоит в том, чтобы объединить два существующих класса и управлять порядком поиска в них. Порядок поиска реализован через модель MRO

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

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

  • метод, вызываемый super(), должен существовать
  • вызывающий и вызываемый методы должны иметь совпадающую сигнатуру аргумента
  • каждое вхождение метода должно использовать super()

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

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

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

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

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)

cs = ColoredShape(color='red', shapename='circle')

Следующая проблема - как убедиться, что целевой метод существует. В примере выше мы знаем, что у object всегда есть __init__() и object - всегда последний в цепочке МРО, поэтому любая последовательность из super().__init__(**kwds) гарантированно завершается вызовом метода object.__init__(). Но так бывает далеко не всегда и object может не иметь необходимого нам метода. Для таких случаев требуется написать корневой класс, который гарантированно будет вызываться перед object и будет иметь нужный нам метод. Задача такого корневого класса - стать последним в цепочке вызовов, заменив собойс object.

class Root:
    def draw(self):
        # the delegation chain stops here
        assert not hasattr(super(), 'draw')

class Shape(Root):
    def __init__(self, shapename, **kwds):
        self.shapename = shapename
        super().__init__(**kwds)
    def draw(self):
        print('Drawing.  Setting shape to:', self.shapename)
        super().draw()

class ColoredShape(Shape):
    def __init__(self, color, **kwds):
        self.color = color
        super().__init__(**kwds)
    def draw(self):
        print('Drawing.  Setting color to:', self.color)
        super().draw()

cs = ColoredShape(color='blue', shapename='square')
cs.draw()

В данном примере, если мы захотим внедрить дополнительные классы, нам придется так-же отнаследовать их от корневого, чтобы ни один из путей в дереве не мог достичь object минуя Root.draw(). Необходимо написать об этом в документации.

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

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

class Moveable:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def draw(self):
        print('Drawing at position:', self.x, self.y)

Адаптером для этого класса будет:

class MoveableAdapter(Root):
    def __init__(self, x, y, **kwds):
        self.movable = Moveable(x, y)
        super().__init__(**kwds)
    def draw(self):
        self.movable.draw()
        super().draw()

И далее:

class MovableColoredShape(ColoredShape, MoveableAdapter):
    pass

MovableColoredShape(color='red', shapename='triangle',
                    x=10, y=20).draw()

Нюансы

  • При создании подклассов встроенной функции, такой как dict(), часто необходимо переопределить или расширить несколько методов одновременно. В приведенных выше примерах расширение __setitem__ не используется другими методами, такими как dict.update, поэтому может потребоваться их расширение. Это требование не уникально для super(); скорее, оно возникает всякий раз, когда встроенные функции являются подклассами.
  • Если класс опирается на один родительский класс, предшествующий другому (например, LoggingOD зависит от LoggingDict , следующего перед OrderedDict, который стоит перед dict), легко добавить утверждения для проверки и документирования предполагаемого порядка разрешения методов:

          position = LoggingOD.__mro__.index
          assert position(LoggingDict) < position(OrderedDict)
          assert position(OrderedDict) < position(dict)
    

Полный пример кода тут

Смотри еще: