web-dev-qa-db-fra.com

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

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 pour l'écriture de __init__ De C:

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

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

Alors, quel est le bon chemin encore? Il est facile de dire "soyez juste cohérents, suivez l'un ou l'autre", mais si A ou B proviennent d'une bibliothèque tierce, que faire alors? Existe-t-il une approche permettant de s’assurer que tous les constructeurs de la classe parent sont appelés (et 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")

Alors je reçois:

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

Notez que l'initialisation de B est appelée 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")

Alors je reçois:

Entering C
Entering A
Leaving A
Leaving C

Notez que l'initialisation de B n'est jamais appelée. Donc, il semble que si je ne connais pas/ne contrôle pas 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).

130
Adam Parkin

Les deux manières 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__.

Lorsque vous utilisez super(), les classes doivent être conçues pour un héritage multiple coopératif où C appelle super, qui appelle le code de A, qui appellera également super qui invoque le code de B. 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 modifiée ultérieurement]

Il semble donc que si je ne connais pas/ne contrôle pas les init des classes dont j’ai hérité (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 de A et B. Vous trouverez un exemple élaboré dans la section intitulée "Comment incorporer une classe non coopérative".

On peut souhaiter que l'héritage multiple soit plus facile, vous permettant de composer sans effort des 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'emboîter aussi facilement que nous le voudrions :-)

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

59

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 non liées.

    Si vos classes de base sont des entités distinctes capables de fonctionner indépendamment et ne se connaissant pas, elles 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 Foo ni Bar n'appelle super().__init__()! C'est pourquoi votre code n'a pas fonctionné correctement. En raison de la façon dont fonctionne l'héritage de diamant en python, les classes dont la classe de base est object ne doivent pas appeler super().__init__(). Comme vous l'avez remarqué, cela romprait l'héritage multiple, car vous finirez par appeler le __init__ D'une autre classe plutôt que object.__init__(). ( Disclaimer: Éviter super().__init__() dans object- sous-classes est ma recommandation personnelle et nullement un consensus convenu dans la communauté python. Certaines personnes préfèrent utiliser super dans chaque classe, arguant que vous pouvez toujours écrire un adaptateur si la classe ne se comporte pas comme prévu.)

    Cela signifie également que vous ne devriez jamais écrire une classe qui hérite de object et qui n'a pas de méthode __init__. Ne pas définir une méthode __init__ A le même effet que d'appeler super().__init__(). Si votre classe hérite directement de object, veillez à ajouter un constructeur vide comme celui-ci:

    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 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épendance . D'autre part, il est plus facile de faire des erreurs. Par exemple, si vous changez l'ordre de Foo et Bar (comme class FooBar(Bar, Foo)), vous devrez mettre à jour les appels super pour qu'ils correspondent. Sans super vous n'avez pas à vous en préoccuper, et le code est beaucoup plus lisible.

  2. Une des classes est un mixin.

    A mixin est une classe qui conçue doit être utilisée avec un héritage multiple. Cela signifie que nous n'avons pas à appeler manuellement les deux constructeurs parents, car le mixin appellera automatiquement le deuxième constructeur pour nous. Comme nous n'avons à appeler qu'un seul constructeur cette fois, nous pouvons le faire avec 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 passe tous les arguments reçus.
    • La sous-classe hérite du mixin first: 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 à mixins: elles transmettent tous les arguments non utilisés à la classe suivante. Comme auparavant, nous devons simplement 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 de CoopBar en premier, 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 que mots clés. L'utilisation d'arguments de position permettrait d'obtenir facilement l'ordre des arguments erroné. Il est donc habituel que les classes coopératives acceptent uniquement des arguments de mots clés.

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

Conclusion: la mise en œuvre 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 un héritage coopératif, cela doit être documenté. Si la documentation ne mentionne rien de la sorte, il est prudent de supposer que la classe n'est pas conçue pour l'héritage multiple coopératif.

32
Aran-Fey

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 indique l'ordre de résolution de la méthode. Dans votre deuxième exemple, où vous appelez super dans A, l'appel super continue dans MRO. La classe suivante dans l'ordre est B, c'est pourquoi init de B est appelée pour la première fois.

Voici un article plus technique du site officiel python:

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

3
Kelvin

Soit l’approche ("nouveau style" ou "ancien style") fonctionnera si vous avez le contrôle sur le code source de A et 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) dicte ce qui suit:

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

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

Code source accessible: utilisation correcte du "style ancien"

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, MRO importe peu, puisque 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 le précédent, l'héritage multiple est toujours possible.


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

Tiers parents: A n'implémente pas super; B fait

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 Adapter implémente super pour que C puisse définir le MRO, qui entre en jeu lorsque super(Adapter, self).__init__() est exécuté.

Et si c'est l'inverse?

Tiers parents: A implémente super; B ne le 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é dans Adapter.__init__; super appel en premier, puis appel explicite. Notez que chaque cas avec des parents tiers nécessite une classe d'adaptateur unique.

Donc, il semble que si je ne connais pas/ne contrôle pas 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).

Bien que vous puissiez gérer les cas où vous ne control le code source de A et B à l'aide d'une classe d'adaptateur, il est vrai que vous devez know comment les init des classes parent implémentent super (le cas échéant) pour le faire.

2
Nathaniel Jones

Si vous multipliez les classes de sous-classe à partir de bibliothèques tierces, alors non, il n’ya pas d’approche aveugle pour appeler la classe de base __init__ méthodes (ou toute autre méthode) qui fonctionnent réellement quelle que soit la manière dont les classes de base sont programmées.

super permet possible d'écrire des classes conçues pour implémenter de manière coopérative des méthodes dans le cadre d'arborescences d'héritage multiples complexes qui n'ont pas besoin d'être connues 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 utiliser super.

Essentiellement, qu'une classe soit conçue pour être sous-classée à l'aide de super ou avec des appels directs à la classe de base est une propriété qui fait partie de "l'interface publique" de la classe et doit être documentée en tant que telle. Si vous utilisez des bibliothèques tierces de la manière attendue par l'auteur de la bibliothèque et que celle-ci dispose d'une documentation raisonnable, elle vous indiquera normalement ce que vous devez faire pour sous-classer des éléments particuliers. Sinon, vous devrez examiner le code source des classes que vous sous-classerez et voir quelle est leur convention d'invocation de classe de base. Si vous combinez plusieurs classes d'une ou de plusieurs bibliothèques tierces d'une manière que les auteurs de bibliothèques ne l'ont pas attendent, il ne sera peut-être pas possible d'invoquer de manière cohérente les méthodes de super-classes du tout; Si la classe A fait partie d'une hiérarchie utilisant super et que la classe B fait partie d'une hiérarchie qui n'utilise pas super, alors aucune option n'est garantie. Vous devrez trouver une stratégie qui fonctionne pour chaque cas particulier.

2
Ben

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

Cependant, il n'utilise pas le lien d'héritage entre C et ces classes. Exploiter ce lien vous donne plus de consistance et rend les refactorisations éventuelles plus faciles et moins sujettes aux erreurs. Un exemple de comment faire cela:

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