3

На super() очень много ресурсов, в том числе this отличное сообщение в блоге, которое всплывает много, а также много вопросов о переполнении стека. Однако я чувствую, что все они перестают объяснять, как это работает в самом общем случае (с произвольными графиками наследования), а также о том, что происходит под капотом.Как работает super() на Python в общем случае?

Рассмотрим простой пример наследования алмазов:

class A(object): 
    def foo(self): 
     print 'A foo' 

class B(A): 
    def foo(self): 
     print 'B foo before' 
     super(B, self).foo() 
     print 'B foo after' 

class C(A): 
    def foo(self): 
     print 'C foo before' 
     super(C, self).foo() 
     print 'C foo after' 

class D(B, C): 
    def foo(self): 
     print 'D foo before' 
     super(D, self).foo() 
     print 'D foo after' 

Если вы читаете на правила языка Python для метода разрешения порядка из источников, таких как this или посмотреть вверх wikipedia page для C3 линеаризация, вы увидите, что MRO должен be (D, B, C, A, object). Это, конечно, подтверждается D.__mro__:

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

И

d = D() 
d.foo() 

печатает

D foo before 
B foo before 
C foo before 
A foo 
C foo after 
B foo after 
D foo after 

который соответствует MRO. Однако учтите, что выше super(B, self).foo() в B фактически вызывает C.foo, тогда как в b = B(); b.foo() он просто переходит прямо к A.foo. Ясно, что использование super(B, self).foo() - это не просто ярлык для A.foo(self), как это иногда учит.

super() тогда очевидно осведомлен о предыдущих вызовах перед ним и об общем MRO, к которому стремится цепочка. Я вижу два способа, которым это может быть достигнуто. Во-первых, нужно сделать что-то вроде передачи объекта super как аргумент self в следующий метод в цепочке, который будет действовать как исходный объект, но также содержит эту информацию. Однако это также похоже на то, что он сломает много вещей (super(D, d) is d - это ложь), и, проведя небольшое экспериментирование, я вижу, что это не так.

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

  1. В настоящее время у нас с нами контекст: Если нет, создайте ту, которая содержит очередь. Получите MRO для аргумента класса, нажмите все элементы, кроме первого в очередь.
  2. Выполнить следующий элемент из очереди MRO текущего контекста, использовать его в качестве текущего класса при построении экземпляра super.
  3. Когда метод доступен из экземпляра super, просмотрите его в текущем классе и вызовите его, используя тот же контекст.

Однако это не объясняет странные вещи, как, используя другой базовый класс в качестве первого аргумента вызова super, или даже вызова другого метода на нем. Я хотел бы знать общий алгоритм для этого. Кроме того, если этот контекст существует где-то, я могу его проверить? Могу ли я с этим справиться? Ужасная идея, конечно, но Python обычно ожидает, что вы взрослый взрослый, даже если вы этого не сделаете.

Это также представляет много соображений дизайна.Если бы я писал B думать только о своем отношении к A, а потом кто-то пишет C и третий человек пишет D, мой B.foo() метод должен вызывать super таким образом, который совместим с C.foo(), даже если бы его не существовало в то время Я написал это! Если я хочу, чтобы мой класс был легко расширяемым, мне нужно будет учитывать это, но я не уверен, что это сложнее, чем просто убедиться, что все версии foo имеют одинаковые подписи. Также возникает вопрос, когда поставить код до или после вызова до super, даже если это не имеет никакого значения, учитывая только базовые классы B.

+0

«Я не уверен, что это сложнее, чем просто убедиться, что все версии foo имеют одинаковые подписи», что является требованием использования супер в целом (хотя вы можете любопытно обойти его с помощью kwargs) –

+0

Вам нужно либо иметь идентичные подписи или использовать '* args, ** kwargs', чтобы вытереть все остальное, что проходит. Первый аргумент 'super' - это класс *, над которым * он должен искать метод - в общем, вы хотите тот, который находится выше текущего, и, следовательно,' super (ThisClass, self) '. * "' super (B, self) .foo() 'не просто ярлык для' A.foo (self) '" * - no, он вызывает следующую реализацию 'foo' в MRO после' B', привязки это «сам». Вы посмотрели ** связанные вопросы ** на боковой панели, некоторые из них кажутся релевантными. – jonrsharpe

+0

@jonrsharpe - Я рассмотрел связанные вопросы, но, как я уже сказал, они действительно рассматривают только упрощенные версии, а не общий случай. Недавно я узнал, что 'super (B, self) .foo()' не является ярлыком для 'A.foo (self)', поскольку я больше учусь об этом, но его часто преподают именно таким образом , Конечно, это не имеет значения в большинстве случаев только с единственным наследованием, но это означает, что в общем случае нецелесообразно использовать 'super (cls, self) .__ init__' с явными аргументами, поскольку он возвращается к' объект' в конечном счете. – JaredL

ответ

7

super() is then obviously aware of the previous calls before it

Это не так. Когда вы делаете super(B, self).foo, super знает MRO, потому что это всего лишь type(self).__mro__, и он знает, что он должен начать поиск foo в точке MRO сразу после B. Грубый чисто Python эквивалент будет

class super(object): 
    def __init__(self, klass, obj): 
     self.klass = klass 
     self.obj = obj 
    def __getattr__(self, attrname): 
     classes = iter(type(self.obj).__mro__) 

     # search the MRO to find self.klass 
     for klass in classes: 
      if klass is self.klass: 
       break 

     # start searching for attrname at the next class after self.klass 
     for klass in classes: 
      if attrname in klass.__dict__: 
       attr = klass.__dict__[attrname] 
       break 
     else: 
      raise AttributeError 

     # handle methods and other descriptors 
     try: 
      return attr.__get__(self.obj, type(self.obj)) 
     except AttributeError: 
      return attr 

If I wrote B thinking only of its relation to A, then later someone else writes C and a third person writes D, my B.foo() method has to call super in a way that is compatible with C.foo() even though it didn't exist at the time I wrote it!

Там нет ожидания, что вы должны быть в состоянии множественного наследования от произвольных классов. Если foo специально разработан для перегрузки классами братьев и сестер в ситуации с множественным наследованием, D не должен существовать.

+0

Эта двойная петля на одном итераторе очень тонкая. Я думаю, что я был бы более явным и использовал бы один цикл 'for' с флагом, говорящим, что если целевой класс был найден еще (случаи' return' также могут быть заправлены внутри цикла): 'found_klass = False; для класса в типе (self.obj) .__ mro__: if not found_klass: found_klass = klass is self.klass; elif attrname в классе .__ dict__: attr = klass .__ dict __ [attrname]; try: return attr .__ get __ (self.obj, type (self.obj)); кроме AttributeError: return attr; raise AttributeError' (с новыми символами и отступом вместо точек с запятой, если необходимо). – Blckknght

+0

Спасибо за отличный ответ, этот код действительно помог мне понять, что происходит. Я не уверен, как я это пропустил, но ясно, что вызов 'B.foo' с использованием' super' - это не то же самое, что вызов 'b.foo()' непосредственно, потому что 'self' будет экземпляром' D', а не 'B', так что вы можете получить весь правильный MRO по типу (self.obj) .__ mro__' вместо' self.klass .__ mro__'. Конечно, глобального государства не требуется, я не знаю, почему я думал иначе. Это также объясняет, что происходит, когда переданный аргумент 'klass' не является владельцем метода. – JaredL

+0

Что касается второй части, я согласен с тем, что не предполагается, что вы должны иметь возможность множественного наследования от произвольных классов. Однако это может быть полезно при определенных обстоятельствах. Я делаю некоторые классы «mixin», которые должны быть многонаследованными вместе с декларативной базой SQLAlchemy, и теперь я понимаю, что мне, возможно, придется вернуться и внести некоторые изменения, чтобы вещи не приходили назад и кусать меня в задницу позже. – JaredL

Смежные вопросы