Понимание суперметода Python, почему D().test() вернет «B->C», а не «B->A»

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

Я также смотрю на пример в книге про питон

Пример, на который ссылаются, есть

class A(object):
     def test(self):
         return 'A'

class B(A):
     def test(self):
         return 'B-->' + super(B, self).test()

class C(A):
     def test(self):
         return 'C'

class D(B, C):
      pass

>>> A().test()
'A'
>>> B().test()
'B-->A'
>>> C().test()
'C'
>>> D().test()
'B-->C'

>>> A.__mro__
(__main__.A, object)

>>> B.__mro__
(__main__.B, __main__.A, object)

>>> C.__mro__
(__main__.C, __main__.A, object)

>>> D.__mro__
(__main__.D, __main__.B, __main__.C, __main__.A, object)

Почему, выполняя D().test(), мы получаем вывод как «B -> C» вместо «B -> A»

Объяснение в книге есть

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

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

Понимание Python super() с методами __init__()

Что делает 'super' в Python?

python, наследование, метод super()

[python]: смущен super()


person Saad    schedule 11.09.2013    source источник
comment
Вы хотите понять (1) каково правило для определения порядка, (2) почему это правило было изобретено или (3) почему Python выбрал это правило вместо альтернатив?   -  person abarnert    schedule 11.09.2013
comment
Теперь я знаю о [1], все еще ищу [2] и [3].   -  person Saad    schedule 11.09.2013


Ответы (3)


Если вы хотите узнать, почему Python выбрал именно этот алгоритм MRO, обсуждение находится в списке рассылки . архивы и кратко изложены в Порядке разрешения методов Python 2.3.

Но на самом деле все сводится к следующему: разрешение методов Python 2.2 было нарушено при работе с множественным наследованием, и первое, что кто-либо предложил исправить, — это позаимствовать алгоритм C3 у Дилана, и никто не имел с ним никаких проблем и не предлагал ничего лучше, и поэтому Python использует C3.


Если вас больше интересуют общие преимущества (и недостатки) C3 по сравнению с другими алгоритмами…

Ответы BrenBarn и florquake дают основу для этого вопроса. Python super() считается супер! из блога Рэймонда Хеттингера гораздо длиннее и подробнее подробное обсуждение в том же духе, и, безусловно, стоит прочитать.

Монотонная линеаризация суперкласса для Дилана — это оригинальный документ, описывающий дизайн. Конечно, язык Dylan сильно отличается от Python, и это академическая статья, но обоснование все равно довольно хорошее.

Наконец, Порядок разрешения методов Python 2.3 (те же документы, ссылки на которые приведены выше) содержит обсуждение преимуществ .

И вам нужно много узнать об альтернативах и о том, как они подходят и не подходят для Python, чтобы идти дальше. Или, если вам нужна более подробная информация о SO, вам нужно будет задать более конкретные вопросы.


Наконец, если вы задаете вопрос «как»:

Когда вы вызываете D().test(), очевидно, что он вызывает код, определенный вами в методе test B. А B.__mro__ это (__main__.B, __main__.A, object). Итак, как этот super(B, self).test() может вызвать метод test C вместо A?

Ключевым моментом здесь является то, что MRO основан на типе self, а не на типе B, где был определен метод test. Если бы вы print(type(self)) внутри функций test, вы бы увидели, что это D, а не B.

Итак, super(B, self) на самом деле получает self.__class__.__mro__ (в данном случае D.__mro__), находит B в списке и возвращает следующее за ним. Довольно просто.

Но это не объясняет, как работает MRO, а только то, что оно делает. Как D().test() вызывает метод из B, но с self это D?

Во-первых, обратите внимание, что D().test, D.test и B.test не являются одной и той же функцией, потому что это вообще не функции; это методы. (Я предполагаю, что это Python 2.x. В 3.x все немного по-другому — в основном проще.)

Метод — это, по сути, объект с im_func, im_class и im_self элементами. Когда вы вызываете метод, все, что вы делаете, это вызываете его im_func с его im_self (если не None) вставленным в качестве дополнительного аргумента в начале.

Итак, все три наших примера имеют один и тот же im_func, который на самом деле является функцией, которую вы определили внутри B. Но первые два имеют D, а не B для im_class, а первый также имеет экземпляр D вместо None для im_self. Итак, вот как его вызов заканчивается передачей экземпляра D как self.

Итак, как D().test оказывается с этими im_self и im_class? Где это создается? Это самое интересное. Полное описание можно найти в Руководстве по дескриптору, но вкратце:

Всякий раз, когда вы пишете foo.bar, то, что на самом деле происходит, эквивалентно вызову getattr(foo, 'bar'), который делает что-то вроде этого (игнорируя атрибуты экземпляра, __getattr__, __getattribute__, слоты, встроенные функции и т. д.):

def getattr(obj, name):
    for cls in obj.__class__.__mro__:
        try:
            desc = cls.__dict__[name]
        except KeyError:
            pass
        else:
            return desc.get(obj.__class__, obj)

Это .get() в конце — магический бит. Если вы посмотрите на функцию, скажем, B.test.im_func, вы увидите, что она на самом деле имеет метод get. И что он делает, так это создает связанный метод с im_func в качестве самого себя, im_class в качестве класса obj.__class__ и im_self в качестве объекта obj.

person abarnert    schedule 11.09.2013
comment
Большое спасибо за это подробное объяснение. - person Saad; 11.09.2013

Короткий ответ заключается в том, что порядок разрешения метода примерно "сначала в ширину". То есть он проходит через все базовые классы на данном уровне предков, прежде чем перейти к любому из их суперклассов. Таким образом, если D наследуется от B и C, которые оба наследуются от A, MRO всегда имеет B и C перед A.

Другой способ думать об этом состоит в том, что если порядок идет B->A, то A.test будет вызываться перед C.test, даже если C является подклассом A. Обычно вы хотите, чтобы реализация подкласса вызывалась до реализации суперкласса (поскольку подкласс может захотеть полностью переопределить суперкласс и вообще не вызывать его).

Подробное объяснение можно найти здесь. Вы также можете найти полезную информацию, погуглив или выполнив поиск в Stackoverflow для вопроса о «порядке разрешения методов Python» или «Python MRO».

person BrenBarn    schedule 11.09.2013
comment
Обратите внимание, что на самом деле это не в ширину. Это некий порядок, который гарантирует, что переопределенные методы никогда не будут вызываться вместо методов, которые должны были их переопределить, и методы из классов, находящихся ранее в списке надклассов класса, переопределяют методы из классов, которые находятся позже в списке надклассов класса. Разрешение метода в ширину не позволило бы достичь этого. - person user2357112 supports Monica; 11.09.2013
comment
Да, вот почему это короткий ответ, но я отредактировал его, чтобы сказать примерно. - person BrenBarn; 11.09.2013
comment
Первоначальную документацию Dylan по C3 было намного легче читать, чем документацию по Python, но я больше не могу найти онлайн-документацию Dylan, только Документ ACM, на который они ссылались для получения дополнительной информации. - person abarnert; 11.09.2013

super() в основном говорит Python: «Делай то, что говорят другие классы этого объекта».

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

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

Пример (несколько бессмысленный):

class Animal(object):
    def make_sounds(self):
        pass

class Duck(Animal):
    def make_sounds(self):
        print 'quack!'
        super(Duck, self).make_sounds()

class Pig(Animal):
    def make_sounds(self):
        print 'oink!'
        super(Pig, self).make_sounds()

# Let's try crossing some animals
class DuckPig(Duck, Pig):
    pass

my_duck_pig = DuckPig()
my_duck_pig.make_sounds()
# quack!
# oink!

Вы бы хотели, чтобы ваш DuckPig говорил quack! и oink!, в конце концов, это свинья и утка, верно? Ну, для этого и нужен super().

person flornquake    schedule 11.09.2013