Appeler la classe parent __init__ avec héritage multiple, quelle est la bonne façon?

175

Disons que j'ai un scénario d'héritage multiple:

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?

Il y a deux approches typiques de l' écriture Cde __init__:

  1. (à l'ancienne) ParentClass.__init__(self)
  2. (style plus récent) super(DerivedClass, self).__init__()

Cependant, dans les deux cas, si les classes parentes ( Aet B) ne suivent pas la même convention, le code ne fonctionnera pas correctement (certaines peuvent être manquées ou être appelées plusieurs fois).

Alors, quelle est la bonne manière encore? Il est facile de dire "soyez cohérent, suivez l'un ou l'autre", mais si Aou Bproviennent d'une bibliothèque tierce, que faire alors? Existe-t-il une approche qui puisse garantir que tous les constructeurs de classes parents sont appelés (et dans le bon ordre, et une seule fois)?

Edit: pour voir ce que je veux dire, si je fais:

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")

Ensuite, je reçois:

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

Notez que Binit est appelé deux fois. Si je fais:

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")

Ensuite, je reçois:

Entering C
Entering A
Leaving A
Leaving C

Notez que Binit n'est jamais appelé. Il semble donc qu'à moins de connaître / contrôler les init des classes dont j'hérite ( Aet B) je ne peux pas faire un choix sûr pour la classe que j'écris ( C).

Adam Parkin
la source

Réponses:

78

Les deux méthodes fonctionnent bien. L'approche utilisant super()conduit à une plus grande flexibilité pour les sous-classes.

Dans l'approche d'appel direct, C.__init__peut appeler à la fois A.__init__et B.__init__.

Lors de l'utilisation super(), les classes doivent être conçues pour l'héritage multiple coopératif où Cappelle super, qui invoque Ale code de qui appellera également celui superqui invoque Ble code de. Voir http://rhettinger.wordpress.com/2011/05/26/super-considered-super pour plus de détails sur ce qui peut être fait avec super.

[Réponse à la question modifiée ultérieurement]

Il semble donc qu'à moins de connaître / contrôler les init des classes dont j'hérite (A et B), je ne peux pas faire un choix sûr pour la classe que j'écris (C).

L'article référencé montre comment gérer cette situation en ajoutant une classe wrapper autour Aet B. Il y a un exemple élaboré dans la section intitulée «Comment incorporer une classe non coopérative».

On pourrait souhaiter que l'héritage multiple soit plus facile, vous permettant de composer sans effort des classes de voiture et d'avion pour obtenir une FlyingCar, mais la réalité est que les composants conçus séparément ont souvent besoin d'adaptateurs ou d'emballages avant de s'emboîter aussi parfaitement que nous le souhaiterions :-)

Une autre réflexion: si vous n'êtes pas satisfait de la fonctionnalité de composition utilisant l'héritage multiple, vous pouvez utiliser la composition pour un contrôle complet sur les méthodes appelées à quelles occasions.

Raymond Hettinger
la source
4
Non, ils ne le font pas. Si l'init de B n'appelle pas super, alors l'init de B ne sera pas appelé si nous faisons lesuper().__init__() approche. Si j'appelle A.__init__()et B.__init__()directement, alors (si A et B appellent super) je reçois l'init de B appelé plusieurs fois.
Adam Parkin
3
@AdamParkin (concernant votre question comme modifiée): Si l'une des classes parentes n'est pas conçue pour être utilisée avec super () , elle peut généralement être enveloppée d'une manière qui ajoute le super appel. L'article référencé montre un exemple élaboré dans la section intitulée «Comment incorporer une classe non coopérative».
Raymond Hettinger
1
J'ai réussi à manquer cette section quand j'ai lu l'article. Exactement ce que je cherchais. Merci!
Adam Parkin
1
Si vous écrivez python (espérons-le 3!) Et utilisez l'héritage de toute sorte, mais surtout multiple, alors rhettinger.wordpress.com/2011/05/26/super-consemed-super devrait être une lecture obligatoire.
Shawn Mehan
1
Vote positif parce que nous savons enfin pourquoi nous n'avons pas de voitures volantes alors que nous étions sûrs que nous en aurions déjà.
msouth
66

La réponse à votre question dépend d'un aspect très important: vos classes de base sont-elles conçues pour l'héritage multiple?

Il existe 3 scénarios différents:

  1. Les classes de base sont des classes autonomes indépendantes.

    Si vos classes de base sont des entités distinctes capables de fonctionner indépendamment et qu'elles ne se connaissent pas, elles ne sont pas conçues pour l'héritage multiple. Exemple:

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

    Important: notez que ni Fooni Barappelle super().__init__()! C'est pourquoi votre code ne fonctionnait pas correctement. En raison du fonctionnement de l'héritage de diamant en python, les classes dont la classe de base est objectne doivent pas être appeléessuper().__init__() . Comme vous l'avez remarqué, cela briserait l'héritage multiple car vous finissiez par appeler une autre classe __init__plutôt que object.__init__(). ( Clause de non-responsabilité: éviter les super().__init__()sous- objectclasses est ma recommandation personnelle et en aucun cas un consensus convenu dans la communauté python. Certaines personnes préfèrent utiliser superdans chaque classe, arguant du fait que vous pouvez toujours écrire un adaptateur si la classe ne se comporte pas comme vous vous attendez.)

    Cela signifie également que vous ne devez jamais écrire une classe qui hérite d' objectune __init__méthode et n'en possède pas . Ne pas définir du tout une __init__méthode a le même effet que l'appel super().__init__(). Si votre classe hérite directement de object, assurez-vous d'ajouter un constructeur vide comme ceci:

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

    Quoi qu'il en soit, dans cette situation, vous devrez appeler chaque constructeur parent manuellement. Il y a deux façons de faire ça:

    • Sans pour autant super

      class FooBar(Foo, Bar):
          def __init__(self, bar='bar'):
              Foo.__init__(self)  # explicit calls without super
              Bar.__init__(self, bar)
    • Avec 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

    Chacune de ces deux méthodes a ses propres avantages et inconvénients. Si vous utilisez super, votre classe prendra en charge l'injection de dépendances . D'un autre côté, il est plus facile de faire des erreurs. Par exemple, si vous modifiez l'ordre de Fooet Bar(comme class FooBar(Bar, Foo)), vous devez mettre à jour les superappels pour qu'ils correspondent. Sans cela, supervous n'avez pas à vous en soucier et le code est beaucoup plus lisible.

  2. L'une des classes est un mixin.

    Un mixin est une classe conçue pour être utilisée avec l'héritage multiple. Cela signifie que nous n'avons pas besoin d'appeler les deux constructeurs parents manuellement, car le mixin appellera automatiquement le 2ème constructeur pour nous. Comme nous n'avons à appeler qu'un seul constructeur cette fois, nous pouvons le faire avec superpour éviter d'avoir à coder en dur le nom de la classe parente.

    Exemple:

    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.

    Les détails importants ici sont:

    • Les appels mixin super().__init__() et passe par tous les arguments qu'il reçoit.
    • La sous - classe hérite de la mixin première : class FooBar(FooMixin, Bar). Si l'ordre des classes de base est incorrect, le constructeur du mixin ne sera jamais appelé.
  3. Toutes les classes de base sont conçues pour l'héritage coopératif.

    Les classes conçues pour l'héritage coopératif ressemblent beaucoup aux mixins: elles transmettent tous les arguments inutilisés à la classe suivante. Comme avant, il suffit d'appeler super().__init__()et tous les constructeurs parents seront appelés en chaîne.

    Exemple:

    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

    Dans ce cas, l'ordre des classes parentes n'a pas d'importance. Nous pourrions aussi bien hériter du CoopBarpremier, et le code fonctionnerait toujours de la même manière. Mais ce n'est vrai que parce que tous les arguments sont passés en tant qu'arguments de mot-clé. L'utilisation d'arguments positionnels faciliterait le mauvais ordre des arguments, il est donc courant que les classes coopératives n'acceptent que les arguments de mot-clé.

    C'est aussi une exception à la règle que j'ai mentionnée plus tôt: les deux CoopFooet CoopBarhéritent de object, mais ils appellent toujours super().__init__(). Sinon, il n'y aurait pas d'héritage coopératif.

Conclusion: l'implémentation correcte dépend des classes dont vous héritez.

Le constructeur fait partie de l'interface publique d'une classe. Si la classe est conçue comme un mixin ou pour l'héritage coopératif, cela doit être documenté. Si la documentation ne mentionne rien de ce genre, il est prudent de supposer que la classe n'est pas conçue pour l'héritage multiple coopératif.

Aran-Fey
la source
2
Votre deuxième point m'a époustouflé. Je n'ai jamais vu Mixins qu'à droite de la super classe réelle et j'ai pensé qu'ils étaient assez lâches et dangereux, car vous ne pouvez pas vérifier si la classe dans laquelle vous mélangez a les attributs que vous attendez d'elle. Je n'ai jamais pensé à mettre un général super().__init__(*args, **kwargs)dans le mixin et à l'écrire en premier. Cela a tellement de sens.
Minix
10

L'une ou l'autre approche ("nouveau style" ou "ancien style") fonctionnera si vous avez le contrôle sur le code source pour AetB . Sinon, l'utilisation d'une classe d'adaptateur peut être nécessaire.

Code source accessible: utilisation correcte du "nouveau style"

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

Ici, l'ordre de résolution de méthode (MRO) dicte ce qui suit:

  • C(A, B)dicte d' Aabord, alors B. MRO est C -> A -> B -> object.
  • super(A, self).__init__()continue le long de la chaîne MRO initiée C.__init__à B.__init__.
  • super(B, self).__init__()continue le long de la chaîne MRO initiée C.__init__à object.__init__.

Vous pourriez dire que ce cas est conçu pour l'héritage multiple .

Code source accessible: utilisation correcte de «l'ancien style»

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

Ici, le MRO n'a pas d'importance, car A.__init__ et B.__init__sont appelés explicitement. class C(B, A):fonctionnerait aussi bien.

Bien que ce cas ne soit pas "conçu" pour l'héritage multiple dans le nouveau style comme l'était le précédent, l'héritage multiple est toujours possible.


Maintenant, que se passe-t-il si Aet Bproviennent d'une bibliothèque tierce - c'est-à-dire que vous n'avez aucun contrôle sur le code source de AetB ? La réponse courte: vous devez concevoir une classe d'adaptateur qui implémente les superappels nécessaires , puis utiliser une classe vide pour définir le MRO (voir l'article de Raymond Hettinger sursuper - en particulier la section, "Comment incorporer une classe non coopérative").

Tiers parents: Ane met pas en œuvre super; BEst-ce que

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

La classe Adapterimplémente de supersorte queC puisse définir le MRO, qui entre en jeu lorsquesuper(Adapter, self).__init__() son exécution.

Et si c'était l'inverse?

Tiers parents: Aoutils super; Bne fait pas

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

Même modèle ici, sauf que l'ordre d'exécution est activé Adapter.__init__; superappelez d'abord, puis appelez explicitement. Notez que chaque cas avec des parents tiers nécessite une classe d'adaptateur unique.

Il semble donc qu'à moins de connaître / contrôler les init des classes dont j'hérite ( Aet B) je ne peux pas faire un choix sûr pour la classe que j'écris ( C).

Bien que vous puissiez gérer les cas où vous ne contrôlez pas le code source de Aet Ben utilisant une classe d'adaptateur, il est vrai que vous devez savoir comment les init des classes parentes implémentent super(le cas échéant) pour ce faire.

Nathaniel Jones
la source
4

Comme Raymond l'a dit dans sa réponse, un appel direct à A.__init__et B.__init__fonctionne très bien, et votre code serait lisible.

Cependant, il n'utilise pas le lien d'héritage entre Cces classes. Exploiter ce lien vous donne plus de cohérence et rend les refactorisations éventuelles plus faciles et moins sujettes aux erreurs. Un exemple de comment procéder:

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
la source
1
La meilleure réponse à mon humble avis. J'ai trouvé cela particulièrement utile car il est plus à l'épreuve du temps
Stephen Ellwood
3

Cet article aide à expliquer l'héritage multiple coopératif:

http://www.artima.com/weblogs/viewpost.jsp?thread=281127

Il mentionne la méthode utile mro()qui vous montre l'ordre de résolution de la méthode. Dans votre 2ème exemple, où vous appelez superdans A, l' superappel se poursuit dans MRO. La classe suivante dans l'ordre est B, c'est pourquoiB init est appelé la première fois.

Voici un article plus technique du site officiel de python:

http://www.python.org/download/releases/2.3/mro/

Kelvin
la source
2

Si vous multipliez les classes de sous-classes à partir de bibliothèques tierces, alors non, il n'y a pas d'approche aveugle pour appeler les __init__méthodes de classe de base (ou toute autre méthode) qui fonctionne réellement quelle que soit la façon dont les classes de base sont programmées.

superrend possible aux cours d'écriture destinés à mettre en œuvre des méthodes en collaboration dans le cadre des arbres de succession multiples complexes qui ne doivent pas être connus de l'auteur de la classe. Mais il n'y a aucun moyen de l'utiliser pour hériter correctement de classes arbitraires qui peuvent ou non utilisersuper .

Essentiellement, le fait qu'une classe soit conçue pour être sous-classée en utilisant superou avec des appels directs à la classe de base est une propriété qui fait partie de "l'interface publique" de la classe, et elle doit être documentée comme telle. Si vous utilisez des bibliothèques tierces de la manière attendue par l'auteur de la bibliothèque et que la bibliothèque dispose d'une documentation raisonnable, elle vous dira normalement ce que vous devez faire pour sous-classer des choses particulières. Sinon, vous devrez regarder le code source des classes que vous sous-classez et voir quelle est leur convention d'invocation de classe de base. Si vous combinez plusieurs classes d'une ou plusieurs bibliothèques tierces d'une manière que les auteurs de la bibliothèque ne s'attendaient pas , alors il peut ne pas être possible d'appeler de manière cohérente des méthodes de super-classe à tous; si la classe A fait partie d'une hiérarchie utilisant superet la classe B fait partie d'une hiérarchie qui n'utilise pas super, aucune option n'est garantie de fonctionner. Vous devrez trouver une stratégie qui fonctionne pour chaque cas particulier.

Ben
la source
@RaymondHettinger Eh bien, vous avez déjà écrit et lié à un article avec quelques réflexions à ce sujet dans votre réponse, donc je ne pense pas avoir grand-chose à ajouter à cela. :) Je ne pense pas qu'il soit possible d'adapter de manière générique une classe non super-utilisatrice à une super-hiérarchie; vous devez trouver une solution adaptée aux classes particulières concernées.
Ben