Язык программирования Python

Функцию super() в Python – как эффективно использовать

Если вас не заинтересовала встроенная функция super(), вы, вероятно, не знаете, на что она действительно способна и как ее эффективно использовать.

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

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

Давайте начнем с базового варианта использования: подкласс должен расширить метод одного из встроенных классов [словарей].

class LoggingDict(dict): def __setitem__(self, key, value): logging.info('Установка %r' % (key, value)) super().__setitem__(key, value)

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

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

До появления super() нам пришлось бы передавать вызов через dict.__setitem__(self, key, value).

Однако super() лучше, потому что это вычисляемая косвенная ссылка.

Одним из преимуществ косвенности является то, что нам не нужно указывать родительский класс по имени.

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

У вас есть единственный источник истины:

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

Помимо изоляции изменений, есть еще одно важное преимущество вычислительной непрямолинейности, которое может быть не знакомо тем, кто работает со статическими языками.

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

Расчет зависит как от класса, из которого вызывается super(), так и от дерева предков экземпляра.

Первый компонент, класс, из которого он вызывается, определяется исходным кодом этого класса.

В нашем примере super() вызывается в методе LoggingDict.__setitem__.

Этот компонент исправлен.

Второй и более интересный компонент – переменная (мы можем создавать новые подклассы с обширным деревом предков).

Рассмотрим это в качестве примера, чтобы создать аккуратный, записываемый словарь без изменения существующих классов:

class LoggingOD(LoggingDict, collections.OrderedDict): pass
Code language: CSS (css)

Дерево предков нашего нового класса выглядит так: LoggingOD, LoggingDict, OrderedDict, dict, object.

Для наших целей важным результатом является то, что OrderedDict был вставлен после LoggingDict и перед dict!

Это означает, что вызов super() в LoggingDict.__setitem__ теперь отправляет пару ключ/значение в OrderedDict вместо dict.

Задумайтесь об этом на секунду.

Мы не изменяли исходный код LoggingDict.

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

Порядок разрешения

То, что я называл порядком разрешения или деревом предков, официально называется Method Resolution Order или MRO.

В этом легко убедиться, распечатав атрибут __mro__.

print(LoggingOD.__mro__)
Code language: CSS (css)
(<class '__main__.LoggingOD'>, <class '__main__.LoggingDict'>, <class 'collections.OrderedDict'>, <class 'dict'>, <class 'object'>)
Code language: HTML, XML (xml)

Если наша цель – создать подкласс с MRO в соответствии с нашими запросами, нам нужно знать, как он рассчитывается.

Принципы просты.

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

Последовательность упорядочена таким образом, что класс всегда появляется перед своими родителями, а если родителей несколько, то они сохраняют тот же порядок, что и кортеж родительских классов [__bases__].

Приведенный выше MRO – это порядок, который следует этим правилам:

  • LoggingOD предшествует своим родителям, LoggingDict и OrderedDict.
  • LoggingDict предшествует OrderedDict, поскольку LoggingOD.__bases__ es (LoggingDict, OrderedDict).
  • LoggingDict предшествует своему родителю, который является dict.
  • OrderedDict предшествует своему родителю, который является dict.
  • dict предшествует своему родителю, который является объектом.

Процесс решения этих ограничений известен как линеаризация.

На эту тему написано множество статей, но для создания подклассов с MRO в соответствии с нашими требованиями нам достаточно знать два правила: дочерние классы предшествуют своим родителям и порядок их появления в __bases__ должен соблюдаться.

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

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

Для работы переупорядочиваемых обращений к методам классы должны быть спроектированы совместно.

Это создает три легко разрешимые практические трудности:

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

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

Это немного сложнее, чем традиционное решение, когда метод, который будет вызван, известен заранее.

В случае super(), последний не известен на момент написания класса (потому что подкласс, написанный позже, может ввести новые классы в MRO).

Одно из решений – придерживаться фиксированной структуры позиционных аргументов.

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

Данная техника показана в примере LoggingDict, где __setitem__ имеет одинаковую структуру как в LoggingDict, так и в dict.

Другое более гибкое решение заключается в том, чтобы каждый метод в дереве предков совместно принимал позиционные аргументы и словарь аргументов по имени, получая необходимые ему аргументы, а затем перенаправлял оставшиеся с помощью **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='Красный', shapename='Круг')

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

В приведенном выше примере показан самый простой случай.

Мы знаем, что у объекта есть метод __init__ и что объект всегда является последним классом в цепочке разрешения методов (MRO), поэтому каждая последовательность вызовов super().__init__ гарантированно заканчивается вызовом метода object.__init__.

Другими словами, мы уверены, что цель вызова super() существует и поэтому не произойдет ошибки AttributeError.

Для случаев, когда у объекта нет интересующего нас метода (например, метода draw()), мы должны написать магистральный класс [root], который будет вызываться перед объектом.

Задача магистрального класса – просто “съесть” вызов метода без перенаправления с помощью super().

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

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

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() cs = ColoredShape(color='Синий', shapename='Квадрат') cs.draw()

3) Показанные выше приемы гарантируют, что super() вызывает метод, о существовании которого известно, и что структура будет правильной.

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

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

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

Как подключить класс, который не работает совместно с другими

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

Эту ситуацию легко исправить, создав класс адаптера, который следует правилам.

Например, следующий класс Moveable не делает вызовов super(), его метод __init__() имеет структуру, несовместимую с object.__init__, и он не наследуется от Root.

class Moveable: def __init__(self, x, y): self.x = x self.y = y def draw(self): print('Рисование на позиции:', self.x, self.y)

Если мы хотим использовать этот класс с нашим классом ColoredShape, который был разработан совместно, нам нужно создать адаптер с необходимыми вызовами super().

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='Красный', shapename='Треугольник', 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)

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *