Как вызвать конструктор каждого родителя ровно один раз?

#python #python-3.x #oop #multiple-inheritance

#python #python-3.x #ооп #множественное наследование

Вопрос:

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

 class A:
    def __init__(self):
        print("Started A's constructor")
        # -- Not calling: super().__init__() --
        print("Ended A's constructor")

class B:
    def __init__(self):
        print("Started B's constructor")
        super().__init__()
        print("Ended B's constructor")

class C(A, B):
    def __init__(self):
        print("Started C's constructor")
        super().__init__()
        print("Ended C's constructor")
  

При вызове

 c = C()
  

мы получаем вывод

 Started C's constructor
Started A's constructor
Ended A's constructor
Ended C's constructor
  

Если я хочу вызвать оба конструктора базового класса для каждого C объекта и не имею доступа к базовому классу для добавления super().__init__() вызова, что следует изменить или добавить к C ? Есть ли способ сделать это так, чтобы он не прерывался, если A действительно был вызван super().__init__() ?
(Редактировать: для ясности, добавление super().__init__() в A вызовет B конструктор, поскольку он следующий в MRO; чтобы было понятнее: пример кода)

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

Если это невозможно, не подорвет ли это фундаментальные объектно-ориентированные принципы? Просто в этом случае реализация A не должна влиять на поведение B .

Комментарии:

1. Вы не должны вызывать super в B.__init__ . Вы просто вызовете __init__ of object .

2. Почему разработчики не выбрали для __init__ of object ничего не делать, если они не намеревались, чтобы люди когда-либо вызывали его? Есть ли у вас какие-либо ссылки или более глубокое понимание того, почему он никогда не должен вызываться и не будет вызываться ни в каких библиотеках python? Такой ответ может быть более подходящим в качестве ответа, чем комментарий.

3. Чтобы использовать совместное множественное наследование с super , тогда каждый класс должен вызывать super . Поместите super().__init__() вызов в A.__init__

4. @PaulRooney нет, это не так, как это работает. Вам нужно вызвать super в B и A для прохождения всей цепочки MRO. super не означает вызвать непосредственный родительский класс . Это означает вызов следующего в MRO . Если ваши классы собираются использовать совместное множественное наследование, то super необходимо вызвать

5. @PaulRooney: Когда вы создаете B , суперзвонок просто вызывает object.__init__ . В ситуации множественного наследования, в чем суть этого вопроса, суперзвонок может вызывать другое, __init__ чем object.__init__ .

Ответ №1:

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

 class A:
    def __init__(self):
        print("Started A's constructor")
        print("Ended A's constructor")


# AWrapper also needs to override each method defined by A;
# those simply delegate their work to the intenral instance of A
# For example,
#
#  def some_method(self, x, y):
#      return self.a.some_method(x, y)
#
class AWrapper:
    def __init__(self, **kwargs):
        self.a = A()
        super().__init__(**kwargs)


class B:
    ...


class C(AWrapper, B):
    ...
  

Комментарии:

1. @Aran-Fey: Это помещает A в MRO, и теперь вы возвращаетесь к тому, что A __init__ не используется super .

2. @Aran-Fey: Как вы планируете исправить A __init__ ? Переопределение не заменяет его. Он все еще находится в MRO, и он все еще собирается разорвать super цепочку.

3. @Aran-Fey Это вообще не работает, потому что super необязательно возвращает прокси к первому статически определяемому базовому классу; это полностью зависит от MRO self , который может быть экземпляром класса, о котором вы еще даже не знаете.

4. Проблема в том, что это явно игрушечный пример, и я не собираюсь одобрять решение, которое работает только для этого игрушечного примера. Что, если A и B имеют общего предка и A вызывают его __init__ явно. В конечном итоге этот метод может быть вызван дважды. Весь смысл super заключается в том, чтобы гарантировать, что каждый метод вызывается один раз в том порядке, в котором они отображаются в MRO. Если вы собираетесь использовать super , делайте это правильно, и это означает, что не нужно жестко кодировать вызовы определенных методов базового класса.

5. Но если A явно вызывается object.__init__ , как вы можете избежать object.__init__ повторного вызова? То, что вы AWrapper использовали композицию вместо наследования, не означает, что object.__init__ она будет вызвана только один раз. Он будет вызван один раз из A.__init__ и один раз из B.__init__ . Я не понимаю, как композиция имеет здесь какое-либо преимущество перед наследованием.

Ответ №2:

Вы можете выполнить итерацию по __bases__ атрибуту class object для вызова __init__ метода каждого из родительских классов:

 class C(A, B):
    def __init__(self):
        print("Started C's constructor")
        for base in self.__class__.__bases__:
            base.__init__(self)
        print("Ended C's constructor")
  

так что C() выводит:

 Started C's constructor
Started A's constructor
Ended A's constructor
Started B's constructor
Ended B's constructor
Ended C's constructor
  

Комментарии:

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

2. Вы имеете в виду, что A это родительский класс B ? Если это так, вы должны определить B с class B(A): помощью.

3. Вовсе нет. Если добавить вызов к A конструктору, он вызовет B конструктор, поскольку он следующий в MRO. Именно в этом нелогичном поведении я пытаюсь разобраться здесь.

4. @blhsing нет, вовсе нет. Это не так super работает. super вызывает следующий элемент в mro , а не суперкласс. Самое простое решение — иметь A call super().__init__ in A.__init__ , тогда все это будет работать так, как вы надеетесь.

5. @blhsing Возможно, это проясняет ситуацию: repl.it/repls/MatureCaringDirectories