229 votes

Appeler la classe parente __init__ avec un héritage multiple, quelle est la bonne méthode ?

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 C 's __init__ :

  1. (ancien style) ParentClass.__init__(self)
  2. (style plus récent) super(DerivedClass, self).__init__()

Toutefois, dans les deux cas, si les classes parentes ( A y B ) ne suivent pas la même convention, alors le code ne fonctionnera pas correctement (certains peuvent être manqués, ou être appelés plusieurs fois).

Alors quelle est la bonne façon de faire, déjà ? Il est facile de dire "soyez cohérent, suivez l'un ou l'autre", mais si A o B proviennent d'une bibliothèque tierce, que faire alors ? Existe-t-il une approche permettant de garantir que tous les constructeurs de la classe mère sont appelés (dans le bon ordre et une seule fois) ?

Edit : pour voir ce que je veux dire, si je le 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, j'obtiens :

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

Notez que B init est appelé deux fois. Si je le 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")

Puis j'obtiens :

Entering C
Entering A
Leaving A
Leaving C

Notez que B init n'est jamais appelé. Il semble donc qu'à moins de connaître/contrôler les init des classes dont j'hérite ( A y B ) Je ne peux pas faire un choix sûr pour le cours que je suis en train d'écrire ( C ).

172voto

Aran-Fey Points 20414

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 indépendantes, sans lien entre elles.

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

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

    Important : Remarquez que ni l'un ni l'autre Foo ni Bar appelle super().__init__() ! C'est pourquoi votre code n'a pas fonctionné correctement. A cause de la façon dont l'héritage des diamants fonctionne en python, les classes dont la classe de base est object ne doit pas appeler super().__init__() . Comme vous l'avez remarqué, cela romprait l'héritage multiple, car vous finissez par appeler la classe __init__ plutôt que object.__init__() . _( Avis de non-responsabilité : Éviter le site super().__init__() en object -est ma recommandation personnelle et ne fait en aucun cas l'objet d'un consensus au sein de la communauté python. Certaines personnes préfèrent utiliser super dans chaque classe, en argumentant que vous pouvez toujours écrire une adaptateur si la classe ne se comporte pas comme vous l'attendez)._

    Cela signifie également que vous ne devriez jamais écrire une classe qui hérite de object et n'a pas de __init__ méthode. Ne pas définir une __init__ a le même effet qu'un appel à la méthode 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 manuellement chaque constructeur parent. Il y a deux façons de le faire :

    • Sans 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 avantages et ses inconvénients. Si vous utilisez super votre classe supportera injection de dépendances . D'un autre côté, il est plus facile de faire des erreurs. Par exemple, si vous changez l'ordre de Foo y Bar (comme class FooBar(Bar, Foo) ), vous devrez mettre à jour le fichier super appels pour correspondre. Sans super vous n'avez pas à vous soucier de cela, et le code est beaucoup plus lisible.

  2. L'une des classes est un mixin.

    A mixin est une classe qui conçu pour être utilisé avec un héritage multiple. Cela signifie que nous n'avons pas à appeler les deux constructeurs parents manuellement, car le mixin appelle automatiquement le deuxième constructeur pour nous. Puisque nous ne devons appeler qu'un seul constructeur cette fois-ci, nous pouvons le faire avec la commande super pour é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 :

    • Le mixin appelle super().__init__() et transmet les arguments qu'il reçoit.
    • La sous-classe hérite de la mixine premièrement : 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 un 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 tout aussi bien hériter de CoopBar d'abord, et le code fonctionnerait toujours de la même manière. Mais cela n'est vrai que si tous les arguments sont passés comme des arguments de type mot-clé. En utilisant des arguments positionnels, il serait facile de se tromper dans l'ordre des arguments, c'est pourquoi il est d'usage que les classes coopératives n'acceptent que des arguments de type mot-clé.

    Il s'agit également d'une exception à la règle que j'ai mentionnée précédemment : Les deux sites CoopFoo y CoopBar hériter de object mais ils appellent toujours super().__init__() . S'ils ne le faisaient pas, il n'y aurait pas d'héritage coopératif.

La ligne du bas : 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 tel, il est prudent de supposer que la classe n'est pas conçu pour l'héritage multiple coopératif.

90voto

Raymond Hettinger Points 50330

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

Dans l'approche par appel direct, C.__init__ peut appeler à la fois A.__init__ y B.__init__ .

Lorsque vous utilisez super() les classes doivent être conçues pour l'héritage multiple coopératif où C appelle super qui invoque A qui appellera également super qui invoque B Le code de l'entreprise. Voir http://rhettinger.wordpress.com/2011/05/26/super-considered-super pour plus de détails sur ce qui peut être fait avec super .

[Question de réponse telle que modifiée ultérieurement]

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

L'article référencé montre comment gérer cette situation en ajoutant une classe enveloppante autour de A y B . Vous trouverez un exemple concret dans la section intitulée "Comment incorporer une classe non coopérative".

On pourrait souhaiter que l'héritage multiple soit plus facile, en vous permettant de composer sans effort les classes Car et Airplane pour obtenir une FlyingCar, mais la réalité est que les composants conçus séparément ont souvent besoin d'adaptateurs ou de wrappers avant de s'assembler de manière aussi transparente que nous le souhaiterions :-)

Une autre idée : si vous n'êtes pas satisfait de la composition des fonctionnalités à l'aide de l'héritage multiple, vous pouvez utiliser la composition pour avoir un contrôle total sur les méthodes qui sont appelées à chaque occasion.

6 votes

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 la commande super().__init__() approche. Si j'appelle A.__init__() y B.__init__() directement, alors (si A et B appellent super ) Je comprends que l'esprit de B soit appelé plusieurs fois.

0 votes

@AdamParkin Pour utiliser super() avec l'héritage multiple, les classes doivent être conçu pour l'héritage multiple coopératif en utilisant les principes décrits dans l'article. Votre autre choix est de laisser C appeler les deux A y B directement.

3 votes

@AdamParkin (concernant votre question telle que modifiée) : Si l'une des classes parentales n'est pas conçue pour être utilisée avec super() il est généralement possible de l'emballer de manière à ajouter l'élément d'information sur l'état de santé. super appel. L'article référencé montre un exemple élaboré dans la section intitulée "Comment incorporer une classe non coopérative".

24voto

Nathaniel Jones Points 438

Les deux approches ("nouveau style" ou "ancien style") fonctionneront. si vous avez le contrôle du code source de A y B . 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 la méthode (MRO) impose ce qui suit :

  • C(A, B) dicte A d'abord, puis B . MRO est C -> A -> B -> object .
  • super(A, self).__init__() se poursuit le long de la chaîne MRO initiée dans C.__init__ a B.__init__ .
  • super(B, self).__init__() se poursuit le long de la chaîne MRO initiée dans C.__init__ a object.__init__ .

On pourrait dire que cette affaire est conçu pour un héritage multiple .

Code source accessible : Utilisation correcte du "vieux 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, puisque A.__init__ y B.__init__ sont appelés explicitement. class C(B, A): fonctionnerait tout 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 reste possible.


Maintenant, et si A y B proviennent d'une bibliothèque tierce - c'est-à-dire, vous n'avez aucun contrôle sur le code source de A y B ? La réponse courte : Vous devez concevoir une classe d'adaptateur qui implémente les fonctions nécessaires à l'exécution de l'application. super puis utilisez une classe vide pour définir le MRO (voir L'article de Raymond Hettinger sur super - notamment la section "Comment incorporer une classe non coopérative").

Les parents tiers : A ne met pas en œuvre super ; B hace

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

Classe Adapter met en œuvre super de sorte que C peut définir le MRO, qui entre en jeu lorsque super(Adapter, self).__init__() est exécuté.

Et si c'était l'inverse ?

Les parents tiers : A met en œuvre super ; B n'est 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 schéma ici, sauf que l'ordre d'exécution est interverti dans Adapter.__init__ ; super appelez 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's des classes dont j'hérite ( A y B ) Je ne peux pas faire un choix sûr pour le cours que je suis en train d'écrire ( C ).

Bien que vous puissiez gérer les cas où vous n'avez pas contrôle le code source de A y B en utilisant une classe d'adaptateur, il est vrai que vous devez connaître comment les init's des classes parentes implémentent super (si tant est qu'il y en ait) pour le faire.

8voto

Jundiaius Points 1661

Comme l'a dit Raymond dans sa réponse, un appel direct au A.__init__ y B.__init__ fonctionne bien, et votre code serait lisible.

Cependant, elle n'utilise pas le lien d'héritage entre C et ces cours. L'exploitation de ce lien permet d'obtenir une plus grande cohérence et rend les remaniements éventuels plus faciles et moins sujets aux erreurs. Un exemple de la façon de 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")

3voto

Kelvin Points 5810

Cet article permet d'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 super en A le super L'appel se poursuit en MRO. La classe suivante dans l'ordre est B c'est pourquoi B s 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/

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X