Chamando a classe pai __init__ com herança múltipla, qual é o caminho certo?

174

Digamos que eu tenha um cenário de herança múltipla:

class A(object):
    # code for A here

class B(object):
    # code for B here

class C(A, B):
    def __init__(self):
        # What's the right code to write here to ensure 
        # A.__init__ and B.__init__ get called?

Há duas abordagens típicas para escrever C's __init__:

  1. (estilo antigo) ParentClass.__init__(self)
  2. (estilo mais recente) super(DerivedClass, self).__init__()

No entanto, em ambos os casos, se as classes pai ( Ae B) não seguirem a mesma convenção, o código não funcionará corretamente (alguns podem ser perdidos ou chamados várias vezes).

Então, qual é o caminho correto de novo? É fácil dizer "apenas ser consistente, siga um ou outro", mas se Aou Bsão de uma biblioteca parte 3, o que então? Existe uma abordagem que pode garantir que todos os construtores da classe pai sejam chamados (e na ordem correta e apenas uma vez)?

Edit: para ver o que quero dizer, se eu fizer:

class A(object):
    def __init__(self):
        print("Entering A")
        super(A, self).__init__()
        print("Leaving A")

class B(object):
    def __init__(self):
        print("Entering B")
        super(B, self).__init__()
        print("Leaving B")

class C(A, B):
    def __init__(self):
        print("Entering C")
        A.__init__(self)
        B.__init__(self)
        print("Leaving C")

Então eu recebo:

Entering C
Entering A
Entering B
Leaving B
Leaving A
Entering B
Leaving B
Leaving C

Note que Bo init é chamado duas vezes. Se eu fizer:

class A(object):
    def __init__(self):
        print("Entering A")
        print("Leaving A")

class B(object):
    def __init__(self):
        print("Entering B")
        super(B, self).__init__()
        print("Leaving B")

class C(A, B):
    def __init__(self):
        print("Entering C")
        super(C, self).__init__()
        print("Leaving C")

Então eu recebo:

Entering C
Entering A
Leaving A
Leaving C

Note que Bo init nunca é chamado. Parece que, a menos que eu conheça / controle os init das classes que herdo ( Ae B), não posso fazer uma escolha segura para a classe que estou escrevendo ( C).

Adam Parkin
fonte

Respostas:

78

Ambas as formas funcionam bem. A abordagem utilizada super()leva a uma maior flexibilidade para subclasses.

Na abordagem de chamada direta, C.__init__pode chamar ambos A.__init__e B.__init__.

Ao usar super(), as classes precisam ser projetadas para herança múltipla cooperativa where Ccalls super, que chama Ao código da qual também chama superqual Bcódigo da chamada . Veja http://rhettinger.wordpress.com/2011/05/26/super-considered-super para obter mais detalhes sobre o que pode ser feito super.

[Pergunta de resposta editada posteriormente]

Parece que, a menos que eu conheça / controle os init das classes que herdo de (A e B), não posso fazer uma escolha segura para a classe que estou escrevendo (C).

O artigo mencionado mostra como lidar com essa situação adicionando uma classe de wrapper em torno de Ae B. Há um exemplo elaborado na seção intitulada "Como incorporar uma classe não cooperativa".

Pode-se desejar que a herança múltipla seja mais fácil, permitindo que você componha sem esforço as classes Car e Airplane para obter um FlyingCar, mas a realidade é que componentes projetados separadamente geralmente precisam de adaptadores ou invólucros antes de se encaixarem da maneira que gostaríamos :-)

Outro pensamento: se você está insatisfeito com a funcionalidade de composição usando herança múltipla, pode usar a composição para ter controle total sobre quais métodos são chamados em quais ocasiões.

Raymond Hettinger
fonte
4
Não, eles não. Se o init de B não chamar super, o init de B não será chamado se fizermos a super().__init__()abordagem. Se eu ligar A.__init__()e B.__init__()diretamente, então (se A e B telefonam super), recebo o init de B sendo chamado várias vezes.
Adam Parkin
3
@AdamParkin (referente à sua pergunta como editada): se uma das classes principais não for projetada para uso com super () , geralmente ela pode ser agrupada de forma a adicionar a super chamada. O artigo referenciado mostra um exemplo elaborado na seção intitulada "Como incorporar uma classe não cooperativa".
Raymond Hettinger
1
De alguma forma, consegui perder essa seção quando li o artigo. Exatamente o que eu estava procurando. Obrigado!
Adam Parkin
1
Se você está escrevendo python (esperemos que 3!) E usando herança de qualquer tipo, mas especialmente várias, então rhettinger.wordpress.com/2011/05/26/super-considered-super deve ser leitura obrigatória.
Shawn Mehan
1
Voto positivo porque finalmente sabemos por que não temos carros voadores quando tínhamos certeza de que teríamos agora.
msouth
65

A resposta para sua pergunta depende de um aspecto muito importante: suas classes base são projetadas para herança múltipla?

Existem 3 cenários diferentes:

  1. As classes base são classes independentes e não relacionadas.

    Se suas classes base são entidades separadas que são capazes de funcionar de forma independente e não se conhecem, elas não foram projetadas para herança múltipla. Exemplo:

    class Foo:
        def __init__(self):
            self.foo = 'foo'
    
    class Bar:
        def __init__(self, bar):
            self.bar = bar

    Importante: Observe que nem Foonem Barchama super().__init__()! É por isso que seu código não funcionou corretamente. Devido à maneira como a herança de diamantes funciona em python, as classes cuja classe base é objectnão devem chamarsuper().__init__() . Como você notou, fazer isso quebraria várias heranças porque você acaba chamando outra classe em __init__vez de object.__init__(). ( Disclaimer: Evitar super().__init__()em object-subclasses é a minha recomendação pessoal e não significa um acordados consenso na comunidade python Algumas pessoas preferem o uso. superEm todas as classes, argumentando que você sempre pode escrever um adaptador se a classe não se comporta como você espera.)

    Isso também significa que você nunca deve escrever uma classe que herda objecte não possui um __init__método. Não definir um __init__método tem o mesmo efeito que chamar super().__init__(). Se sua classe herdar diretamente de object, adicione um construtor vazio da seguinte maneira:

    class Base(object):
        def __init__(self):
            pass

    De qualquer forma, nessa situação, você precisará chamar cada construtor pai manualmente. Existem duas maneiras de fazer isso:

    • Sem super

      class FooBar(Foo, Bar):
          def __init__(self, bar='bar'):
              Foo.__init__(self)  # explicit calls without super
              Bar.__init__(self, bar)
    • Com super

      class FooBar(Foo, Bar):
          def __init__(self, bar='bar'):
              super().__init__()  # this calls all constructors up to Foo
              super(Foo, self).__init__(bar)  # this calls all constructors after Foo up
                                              # to Bar

    Cada um desses dois métodos tem suas próprias vantagens e desvantagens. Se você usar super, sua classe oferecerá suporte à injeção de dependência . Por outro lado, é mais fácil cometer erros. Por exemplo, se você alterar a ordem de Fooe Bar(como class FooBar(Bar, Foo)), precisará atualizar as superchamadas para que correspondam. Sem supervocê não precisa se preocupar com isso, e o código é muito mais legível.

  2. Uma das classes é um mixin.

    Um mixin é uma classe projetada para ser usada com herança múltipla. Isso significa que não precisamos chamar os dois construtores-pai manualmente, porque o mixin chamará automaticamente o segundo construtor para nós. Como só precisamos chamar um único construtor dessa vez, podemos fazer isso superpara evitar codificar o nome da classe pai.

    Exemplo:

    class FooMixin:
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)  # forwards all unused arguments
            self.foo = 'foo'
    
    class Bar:
        def __init__(self, bar):
            self.bar = bar
    
    class FooBar(FooMixin, Bar):
        def __init__(self, bar='bar'):
            super().__init__(bar)  # a single call is enough to invoke
                                   # all parent constructors
    
            # NOTE: `FooMixin.__init__(self, bar)` would also work, but isn't
            # recommended because we don't want to hard-code the parent class.

    Os detalhes importantes aqui são:

    • O mixin chama super().__init__()e passa por todos os argumentos que recebe.
    • Os herda subclasse do mixin primeiro : class FooBar(FooMixin, Bar). Se a ordem das classes base estiver incorreta, o construtor do mixin nunca será chamado.
  3. Todas as classes base são projetadas para herança cooperativa.

    Classes projetadas para herança cooperativa são muito parecidas com mixins: elas passam por todos os argumentos não utilizados para a próxima classe. Como antes, basta chamar super().__init__()e todos os construtores-pai serão chamados em cadeia.

    Exemplo:

    class CoopFoo:
        def __init__(self, **kwargs):
            super().__init__(**kwargs)  # forwards all unused arguments
            self.foo = 'foo'
    
    class CoopBar:
        def __init__(self, bar, **kwargs):
            super().__init__(**kwargs)  # forwards all unused arguments
            self.bar = bar
    
    class CoopFooBar(CoopFoo, CoopBar):
        def __init__(self, bar='bar'):
            super().__init__(bar=bar)  # pass all arguments on as keyword
                                       # arguments to avoid problems with
                                       # positional arguments and the order
                                       # of the parent classes

    Nesse caso, a ordem das classes pai não importa. Podemos herdar CoopBarprimeiro, e o código ainda funcionaria da mesma maneira. Mas isso só é verdade porque todos os argumentos são passados ​​como argumentos de palavras-chave. O uso de argumentos posicionais tornaria fácil a ordem dos argumentos errados; portanto, é habitual que as classes cooperativas aceitem apenas argumentos de palavras-chave.

    Isso também é uma exceção à regra que mencionei anteriormente: Ambos CoopFooe CoopBarherdam de object, mas eles ainda chamam super().__init__(). Se não o fizessem, não haveria herança cooperativa.

Conclusão: a implementação correta depende das classes das quais você está herdando.

O construtor faz parte da interface pública de uma classe. Se a classe for projetada como uma combinação ou herança de cooperação, isso deve ser documentado. Se os documentos não mencionarem nada disso, é seguro assumir que a classe não foi projetada para herança múltipla cooperativa.

Aran-Fey
fonte
2
Seu segundo ponto me surpreendeu. Eu só vi Mixins à direita da super classe real e pensei que eles eram bem soltos e perigosos, já que você não pode verificar se a classe em que está se misturando tem os atributos que você espera que ela tenha. Eu nunca pensei em colocar um general super().__init__(*args, **kwargs)na mixagem e escrever primeiro. faz muito sentido.
Minix
10

Qualquer abordagem ("novo estilo" ou "estilo antigo") funcionará se você tiver controle sobre o código fonte para AeB . Caso contrário, o uso de uma classe de adaptador pode ser necessário.

Código fonte acessível: uso correto de "novo estilo"

class A(object):
    def __init__(self):
        print("-> A")
        super(A, self).__init__()
        print("<- A")

class B(object):
    def __init__(self):
        print("-> B")
        super(B, self).__init__()
        print("<- B")

class C(A, B):
    def __init__(self):
        print("-> C")
        # Use super here, instead of explicit calls to __init__
        super(C, self).__init__()
        print("<- C")
>>> C()
-> C
-> A
-> B
<- B
<- A
<- C

Aqui, a ordem de resolução de método (MRO) determina o seguinte:

  • C(A, B)dita Aprimeiro, então B. MRO é C -> A -> B -> object.
  • super(A, self).__init__()continua ao longo da cadeia de MRO iniciado em C.__init__a B.__init__.
  • super(B, self).__init__()continua ao longo da cadeia de MRO iniciado em C.__init__a object.__init__.

Você poderia dizer que este caso foi projetado para herança múltipla .

Código fonte acessível: uso correto do "estilo antigo"

class A(object):
    def __init__(self):
        print("-> A")
        print("<- A")

class B(object):
    def __init__(self):
        print("-> B")
        # Don't use super here.
        print("<- B")

class C(A, B):
    def __init__(self):
        print("-> C")
        A.__init__(self)
        B.__init__(self)
        print("<- C")
>>> C()
-> C
-> A
<- A
-> B
<- B
<- C

Aqui, MRO não importa, desde A.__init__e B.__init__são chamados explicitamente. class C(B, A):funcionaria tão bem.

Embora este caso não seja "projetado" para herança múltipla no novo estilo como o anterior, ainda é possível a herança múltipla.


Agora, o que se Ae Bde uma biblioteca de terceiros - ou seja, você não tem controle sobre o código-fonte para AeB ? A resposta curta: você deve projetar uma classe de adaptador que implemente as superchamadas necessárias e, em seguida, usar uma classe vazia para definir o MRO (consulte o artigo de Raymond Hettingersuper - especialmente a seção "Como incorporar uma classe não cooperativa").

Pais de terceiros: Anão implementam super; Bfaz

class A(object):
    def __init__(self):
        print("-> A")
        print("<- A")

class B(object):
    def __init__(self):
        print("-> B")
        super(B, self).__init__()
        print("<- B")

class Adapter(object):
    def __init__(self):
        print("-> C")
        A.__init__(self)
        super(Adapter, self).__init__()
        print("<- C")

class C(Adapter, B):
    pass
>>> C()
-> C
-> A
<- A
-> B
<- B
<- C

A classe é Adapterimplementada superpara Cdefinir o MRO, que entra em ação quando super(Adapter, self).__init__()é executado.

E se for o contrário?

Pais de terceiros: Aimplementos super; Bnão

class A(object):
    def __init__(self):
        print("-> A")
        super(A, self).__init__()
        print("<- A")

class B(object):
    def __init__(self):
        print("-> B")
        print("<- B")

class Adapter(object):
    def __init__(self):
        print("-> C")
        super(Adapter, self).__init__()
        B.__init__(self)
        print("<- C")

class C(Adapter, A):
    pass
>>> C()
-> C
-> A
<- A
-> B
<- B
<- C

O mesmo padrão aqui, exceto que a ordem de execução é ativada Adapter.__init__; superprimeiro, depois a chamada explícita. Observe que cada caso com pais de terceiros requer uma classe de adaptador exclusiva.

Parece que, a menos que eu conheça / controle os init das classes que herdo ( Ae B), não posso fazer uma escolha segura para a classe que estou escrevendo ( C).

Embora você possa lidar com os casos em que você não controla o código-fonte Ae Busando uma classe de adaptador, é verdade que você deve saber como os init's das classes-pai implementam super(se houver) para fazê-lo.

Nathaniel Jones
fonte
4

Como Raymond disse em sua resposta, uma ligação direta A.__init__e B.__init__funciona bem, e seu código seria legível.

No entanto, ele não usa o link de herança entre Cessas classes. A exploração desse link oferece maior consistência e facilita eventuais refatorações e menos propensas a erros. Um exemplo de como fazer isso:

class C(A, B):
    def __init__(self):
        print("entering c")
        for base_class in C.__bases__:  # (A, B)
             base_class.__init__(self)
        print("leaving c")
Jundiaius
fonte
1
A melhor resposta é imho. Encontrei este particularmente útil, pois é mais futureproof
Stephen Ellwood
2

Se você estiver multiplicando classes de subclasse de bibliotecas de terceiros, não, não há uma abordagem cega para chamar os __init__métodos da classe base (ou quaisquer outros métodos) que realmente funcionem, independentemente de como as classes base são programadas.

super faz possível escrever classes projetadas para implementar métodos cooperativamente como parte de múltiplas árvores de herança complexas que não precisam ser conhecidas pelo autor da classe. Mas não há como usá-lo para herdar corretamente de classes arbitrárias que podem ou não ser usadas super.

Essencialmente, se uma classe é projetada para ser subclassificada usando superou com chamadas diretas para a classe base é uma propriedade que faz parte da "interface pública" da classe e deve ser documentada como tal. Se você estiver usando bibliotecas de terceiros da maneira esperada pelo autor da biblioteca e a biblioteca tiver documentação razoável, normalmente será informado o que você deve fazer para subclassificar determinadas coisas. Caso contrário, será necessário consultar o código-fonte das classes que você está subclassificando e ver qual é a convenção de invocação de classe base. Se você estiver combinando várias classes de uma ou mais bibliotecas de terceiros de uma maneira que os autores da biblioteca ; se a classe A fizer parte de uma hierarquia usando não esperar, então não pode ser possível consistentemente invocar métodos super-classe em tudosupere a classe B faz parte de uma hierarquia que não usa super, então nenhuma opção é garantida para o trabalho. Você precisará descobrir uma estratégia que funcione para cada caso específico.

Ben
fonte
@ RaymondHettinger Bem, você já escreveu e vinculou a um artigo com algumas reflexões sobre isso em sua resposta, então não acho que tenha muito a acrescentar a isso. :) Eu acho que não é possível adaptar genericamente qualquer classe que não use muito a uma super-hierarquia; você precisa criar uma solução adaptada às classes específicas envolvidas.
217 Ben