Python Multiple Inheritance: вызов супер на всех

У меня есть следующие два суперкласса:

class Parent1(object):
    def on_start(self):
        print('do something')

class Parent2(object):
    def on_start(self):
        print('do something else')

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

class Child(Parent1, Parent2):
    def on_start(self):
        # super call on both parents

Как Pythonic способ сделать это? Благодарю.

Ответ 1

Резюме Exec:

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

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

Метод, представленный ниже, используя __bases__ с try/except, ближе всего подходит для полного ответа на ваш вопрос о том, как вызвать каждый родительский метод с тем же именем.


super полезен в ситуации, когда вы хотите вызвать один из своих родительских методов, но вы не знаете, какой из родителей:

class Parent1(object):
    pass

class Parent2(object):
    # if Parent 2 had on_start - it would be called instead 
    # because Parent 2 is left of Parent 3 in definition of Child class
    pass

class Parent3(object):
    def on_start(self):
        print('the ONLY class that has on_start')        

class Child(Parent1, Parent2, Parent3):
    def on_start(self):
        super(Child, self).on_start()

В этом случае Child имеет трех непосредственных родителей. Только один, Parent3, имеет метод on_start. Вызов super разрешает, что только Parent3 имеет on_start, и это метод, который вызывается.

Если Child наследуется от более чем одного класса, который имеет метод on_start, порядок разрешен слева направо (как указано в определении класса) и снизу вверх (как логическое наследование). Вызывается только один из методов, а другие методы с тем же именем в иерархии классов заменяются.

Итак, чаще:

class GreatGrandParent(object):
    pass

class GrandParent(GreatGrandParent):
    def on_start(self):
        print('the ONLY class that has on_start')

class Parent(GrandParent):
    # if Parent had on_start, it would be used instead
    pass        

class Child(Parent):
    def on_start(self):
        super(Child, self).on_start()

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

class Parent1(object):
    def on_start(self):
        print('do something')

class Parent2(object):
    def on_start(self):
        print('do something else')

class Child(Parent1, Parent2):
    def on_start(self):
        for base in Child.__bases__:
            base.on_start(self)

>>> Child().on_start()
do something
do something else

Если есть возможность, у одного из базовых классов нет on_start, вы можете использовать try/except:

class Parent1(object):
    def on_start(self):
        print('do something')

class Parent2(object):
    def on_start(self):
        print('do something else')

class Parent3(object):
    pass        

class Child(Parent1, Parent2, Parent3):
    def on_start(self):
        for base in Child.__bases__:
            try:
                base.on_start(self)
            except AttributeError:
                # handle that one of those does not have that method
                print('"{}" does not have an "on_start"'.format(base.__name__))

>>> Child().on_start()
do something
do something else
"Parent3" does not have an "on_start"

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

class GGP1(object):
    def on_start(self):
        print('GGP1 do something')

class GP1(GGP1):
    def on_start(self):
        print('GP1 do something else')

class Parent1(GP1):
    pass        

class GGP2(object):
    def on_start(self):
        print('GGP2 do something')

class GP2(GGP2):
    pass

class Parent2(GP2):
    pass            

class Child(Parent1, Parent2):
    def on_start(self):
        for base in Child.__bases__:
            try:
                base.on_start(self)
            except AttributeError:
                # handle that one of those does not have that method
                print('"{}" does not have an "on_start"'.format(base.__name__))

>>> Child().on_start()
GP1 do something else
GGP2 do something
# Note that 'GGP1 do something' is NOT printed since on_start was satisfied by 
# a descendant class L to R, bottom to top

Теперь представьте себе более сложную структуру наследования:

enter image description here

Если вы хотите использовать каждый метод on_start, но можете использовать __mro__ и отфильтровывать классы, у которых нет on_start, как часть их __dict__ для этого класса. В противном случае вы потенциально получите метод on_start. Другими словами, hassattr(c, 'on_start') является True для каждого класса, который Child является потомком (кроме object в этом случае), так как Ghengis имеет атрибут on_start, и все классы являются классами потомков из Ghengis.

** Предупреждение - только демонстрация **

class Ghengis(object):
    def on_start(self):
        print('Khan -- father to all')    

class GGP1(Ghengis):
    def on_start(self):
        print('GGP1 do something')

class GP1(GGP1):
    pass

class Parent1(GP1):
    pass        

class GGP2(Ghengis):
    pass

class GP2(GGP2):
    pass

class Parent2(GP2):
    def on_start(self):
        print('Parent2 do something')

class Child(Parent1, Parent2):
    def on_start(self):
        for c in Child.__mro__[1:]:
            if 'on_start' in c.__dict__.keys():
                c.on_start(self)

>>> Child().on_start()
GGP1 do something
Parent2 do something
Khan -- father to all

Но это также имеет проблему - если Child дополнительно подклассифицируется, то дочерний элемент Child также будет циклически пересекать одну и ту же цепочку __mro__.

Как заявил Раймонд Хеттингер:

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

1) метод, вызываемый супер(), должен существовать

2) вызывающий и вызываемый должны иметь соответствующую подпись аргумента и

3) для каждого случая метода необходимо использовать super()

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

Ответ 2

class Parent1(object):
    def on_start(self):
        print('do something')

class Parent2(object):
    def on_start(self):
        print('do something else')

class Child(Parent1, Parent2):
    def on_start(self):
        super(Child, self).on_start()
        super(Parent1, self).on_start()

c = Child()

c.on_start()
do something
do something else

Или без супер:

class Child(Parent1, Parent2):
    def on_start(self):
        Parent1.on_start(self)
        Parent2.on_start(self)

Ответ 3

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

class Child(Parent1, Parent2):
    def on_start (self):
        Parent1.on_start()
        Parent2.on_start()