Python super() действительно супер!

Если вас еще не поразил встроенный в Python класс super(), то вероятно вы еще не знаете всех его возможностей или примеров эффективного использования.

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

Примеры к данному посту доступны на Python 2 и Python 3
Используя синтаксис Python 3 как основной, создадим наследника для расширения одного из методов базового класса.

<code>class LoggingDict(dict):
    def __setitem__(self, key, value):
        logging.info('Setting %r to %r' % (key, value))
        return super().__setitem__(key, value)</code>
Этот класс обладает теми же возможностями, что и родитель (dict), но он дополнен методом __setitem__ для создания логов по мере обновления атрибута key. После добавления записи в лог-файл, метод использует super() для передачи параметров обновления словаря с атрибутами key/value.

До введения super() мы постоянно вызывали dict.__setitem(self,key,value). Однако super() выгодно отличается тем, что он является вычисляемой косвенной ссылкой.
Одним из преимуществ вычисляемой косвенности является то, что мы не обязаны указывать ссылку на класс по имени. Если вы измените свой код, чтобы переделать базовый класс для иного отображения, то ссылка super() автоматически последует за вами. И в таком случае вы имеете единый код. Прим. переводчика: У автора «single source of truth».

<code>class LoggingDict(SomeOtherMapping):      # новый базовый класс
    def __setitem__(self, key, value):
        logging.info('Setting %r to %r' % (key, value))
        return super().__setitem__(key, value)  # изменения не нужны</code>
В дополнение к изоляции изменений есть еще один важный пункт косвенных ссылок, который возможно не знаком программистам пришедшим со статических языков. Так как косвенность определяется во время выполнения, мы имеем возможность повлияет на вычисления так, что она будет указывать на другой класс.

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

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

<code>class LoggingOD(LoggingDict, collections.OrderedDict):
    pass</code>
Предки дерева для нашего нового класса это: LoggingOD, LoggingDict, OrderedDict, dict, object. Для наших целей важным результатом является то, что OrderedDict был вставлен после LoggingDict и до dict! Это означает, что super() вызыванный в LoggingDict.__setitem__ теперь отправляет key/value обновленными в OrderedDict вместо dict.
Задумайтесь, мы не изменяли исходный код для LoggingDict. Вместо этого мы создали подкласс в котором только логика составила два существующих класса и контролирует порядок их поиска.

Порядок поиска

То что я называю порядком поиска или деревом предков официально известно под названием Method Resolution Order или MRO. Проще всего посмотреть на MRO в написании атрибуте __mro__:

<code>>>> pprint(LoggingOD.__mro__)
(<class '__main__.LoggingOD'>,
 <class '__main__.LoggingDict'>,
 <class 'collections.OrderedDict'>,
 <class 'dict'>,
 <class 'object'>)</code>
Если наша цель заключается в создании подкласса с MRO под наши требования, то нам надо знать, как она рассчитывается. В основном все просто. Последовательность включает в себя класс, его базовые классы, и их базовые классы и так далее до достижения объекта, которые является корневым классом всех этих классов. Последовательность упорядочена так, что класс всегда появляется перед его предком и, если есть несколько предков, то они держат тот же порядок, который указан в наборе базовых классов.
MRO стоит выше этого порядка, что следует из этих ограничений:
LoggingOD предшествует его предкам, LoggingDict и OrderedDict
LoggingDict предшествует OrderedDict потому что LoggingOD.__bases__ это (LoggingDict, OrderedDict)
LoggingDict предшествует предку dict.
OrderedDict предшествует предку dict.
dict предшествует предку, который является объектом.
Процесс решения этих проблем известен как линеаризация. Есть множество хороших работ по это тебе, но нам для начала нужно знать эти два ограничения:
Потомки выполняются перед родителям и порядок их выполнения заключен в иерархии.

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

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

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

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

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

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

<code>class Shape:
    def __init__(self, **kwds):
        self.shapename = kwds.pop('shapename')
        super().__init__(**kwds)        

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

cs = ColoredShape(color='red', shapename='circle')</code>
2) Посмотрев на стратегии получения вызывающего/вызываемого аргумента с ожиданием совпадения, давайте посмотрим как убедиться, что целевой метод вообще существует.

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

В случаях, когда объект не имеет метода (метод draw() к примеру), нам необходимо написать наш собственный корневой класс, который гарантированно будет вызываться до object(). Задача этого корневого класса просто поглотить вызов метода без использования super():

<code>class Root:
    def draw(self):
        pass            # переданная цепь останавливается здесь

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

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

cs = ColoredShape(color='blue', shapename='square')
cs.draw()</code>
Если же подклассы хотят вставить другие классы в MRO, они должны также унаследовать их от Root, чтобы не идти к вызову draw() достигая объекта не будучи остановленным Root.draw(). Это должно быть четко задокументировано, так чтобы кто-то пишущий взаимодействующие классы должен знать, что подкласс идет от Root. Это ограничение не сильно отличается от собственных требований Python'a, где все новые исключения должны наследовать BaseException.

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

Как смешать необъединенные классы.

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

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

<code>class Moveable:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def draw(self):
        print('Drawing at position:', self.x, self.y)</code>
Если же мы хотим использовать этот класс с разработанной нами иерархией ColoredShape, нам надо сделать оболочку, которая отвечает за добавление вызова super():

<code>class MoveableWrapper(Root):
    def __init__(self, **kwds):
        x = kwds.pop('x')
        y = kwds.pop('y')
        self.movable = Moveable(x, y)
        super().__init__(**kwds)
    def draw(self):
        self.movable.draw()
        super().draw()

class MovableColoredShape(ColoredShape, MoveableWrapper):
    pass

MovableColoredShape(color='blue', shapename='triangle', x=10, y=10</code>
Полный пример — Для интереса.

В Python 2.7 и 3.2 модуль collections имеет оба класса Counter и OrderedDict. Это позволяет легко создать OrderedCounter:

<code>from collections import Counter, OrderedDict

class OrderedCounter(Counter, OrderedDict):
     'Counter that remembers the order elements are first encountered'

     def __repr__(self):
         return '%s(%r)' % (self.__class__.__name__,
                            OrderedDict(self))

     def __reduce__(self):
         return self.__class__, (OrderedDict(self),)

oc = OrderedCounter('abracadabra')</code>
Примечания от автора

— В данной статье используется версия Python 3. Полный рабочий исходный код может быть найден здесь.

— В Python 2 данный класс и экземпляр необходимо указывать в явном виде (полный код на Python 2):
<code>class LoggingDict(dict):
    def __setitem__(self, key, value):
        logging.info('Setting %r to %r' % (key, value))
        return super(LoggingDict, self.__setitem__(key, value))</code>
— Дополнительно рекомендую почитать статьи о линеаризации алгоритмов, которые могут быть найдены в документации Python MRO и в Википедии (EN, RU).

Прим. переводчика: Также есть великолепная статья о MRO и наследовании в Python на хабре и продолжение темы super() от еще одного блоггера


0 комментариев

Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.