Appeler la classe des parents init avec l'héritage multiple, Quelle est la bonne façon?

dites 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 à l'écriture C 's __init__ :

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

cependant, dans les deux cas, si les classes de base ( A et B ) ne suivent pas la même convention, alors le code ne sera pas travail correctement (certains peuvent manquer ou être appelé plusieurs fois).

alors, quelle est la bonne façon? Il est facile de dire "juste être cohérent, suivre l'un ou l'autre", mais si A ou B sont d'une bibliothèque tierce partie, Que faire alors? Y a-t-il une approche qui permet de s'assurer que tous les constructeurs de la classe mère 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")

Puis-je obtenir:

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

notez que B 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-je obtenir:

Entering C
Entering A
Leaving A
Leaving C

notez que B 's init n'est jamais appelé. Il semble donc que, à moins de connaître ou de contrôler les entrées 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 ).

89
demandé sur James Taylor 2012-03-06 03:00:31

5 réponses

ça marche dans les deux sens. 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__ et B.__init__ .

en utilisant super() , les classes doivent être conçues pour l'héritage multiple coopératif où C appelle super , qui invoque A le code de "qui appellera aussi super qui invoque B le 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 .

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

il semble donc que si Je ne connais pas/ne contrôle pas les entrées des classes I hériter de (A et B) Je ne peux pas faire un choix sûr pour la classe que je suis l'écriture de (C).

l'article référencé montre comment gérer cette situation en ajoutant une classe d'emballage autour de A et B . Il y a un exemple élaboré dans la section intitulée "Comment incorporer une catégorie Non coopérative".

on pourrait souhaiter que les héritages multiples soient plus faciles, vous permettant de composer sans effort les classes de voiture et D'avion pour obtenir une voiture volant, mais la réalité est que les composants conçus séparément ont souvent besoin d'adaptateurs ou d'enveloppements avant de s'adapter ensemble aussi facilement que nous le ferions comme par exemple :-)

une autre pensée: si vous n'êtes pas satisfait de composer la fonctionnalité en utilisant l'héritage multiple, vous pouvez utiliser la composition pour le contrôle complet sur les méthodes qui sont appelées à quelles occasions.

50
répondu Raymond Hettinger 2015-06-04 21:54:06

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 y a 3 scénarios différents:

  1. les classes de base sont indépendantes et indépendantes.

    si vos classes de base sont des entités séparées qui sont capables de fonctionner indépendamment et ils ne se connaissent pas, ils sont Non conçu pour l'héritage multiple.

    Dans ce cas, vous devrez appeler chaque constructeur parent manuellement. C'est plus facile sans super .

    exemple:

    class Foo:
        def __init__(self):
            self.foo = 'foo'
    
    class Bar:
        def __init__(self, bar):
            self.bar = bar
    
    class FooBar(Foo, Bar):
        def __init__(self, bar='bar'):
            Foo.__init__(self)  # explicit calls without super
            Bar.__init__(self, bar)
    
            # To get the same results with `super`, you'd have to do this:
            #   super().__init__()
            #   super(Foo, self).__init__(bar)
            # Which is obviously much less intuitive.
    

    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 l'héritage de diamant fonctionne en python:

    • les classes dont la classe de base est object ne doivent pas appeler super().__init__() . Comme vous l'avez remarqué, cela briserait l'héritage multiple parce que vous finissez par appeler une autre classe __init__ plutôt que object.__init__() .
    • cela signifie également que vous devez ne jamais écrire une classe qui hérite de object et n'a pas de __init__ méthode . Pas de la définition d'un La méthode __init__ 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
  1. l'une des classes est un mixin.

    Un mixin est une classe conçu pour être utilisé avec plusieurs héritage. Cela signifie que nous n'avons pas à appeler les deux constructeurs parents manuellement, parce que le mixin appellera automatiquement le 2e constructeur pour nous. Puisque nous n'avons qu'à appeler un constructeur unique cette fois, nous pouvons le faire avec super pour éviter d'avoir à coder le nom de la classe mère.

    exemple:

    class FooMixin:
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            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 par le biais de tous les arguments qu'il reçoit.
    • la sous-classe hérite du mélange première : class FooBar(FooMixin, Bar) . Si l'ordre des classes de base est erroné, le constructeur du mixin ne sera jamais appelé.
  2. 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 à des mixins: elles passent à travers toutes les arguments pour la prochaine classe. 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)
            self.foo = 'foo'
    
    class CoopBar:
        def __init__(self, bar, **kwargs):
            super().__init__(**kwargs)
            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 parent n'a pas d'importance. Nous pourrions ainsi hériter de CoopBar d'abord, et le code fonctionne encore le même. Mais c'est seulement vrai parce que tous les arguments sont passés comme des arguments de mots-clés. Utiliser des arguments de position rendrait facile d'obtenir le l'ordre des arguments est erroné, il est donc d'usage que les classes coopératives n'acceptent que les arguments par mots clés.

    il s'agit également d'une exception à la règle que j'ai mentionnée plus haut: les deux CoopFoo et CoopBar héritent de object , mais ils appellent encore super().__init__() . S'ils ne le faisaient pas, il n'y aurait pas d'héritage coopératif.

résultat: 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 un héritage coopératif, cela doit être documenté. Si les docs ne mentionnent rien de tel, il est raisonnable de supposer que la classe n'est pas conçu pour l'héritage multiple et vous devez appeler explicitement le constructeur de chaque classe mère (sans super ).

8
répondu Aran-Fey 2018-09-16 23:45:26

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 deuxième exemple, où vous appelez super dans A , l'appel super se poursuit dans MRO. La classe suivante dans l'ordre est B , c'est pourquoi B init est appelé pour la première fois.

voici un article plus technique du site officiel de python:

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

3
répondu Kelvin 2012-09-06 19:08:39

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

super rend possible d'écrire des classes conçues pour mettre en œuvre de manière coopérative des méthodes dans le cadre d'arbres complexes à héritages multiples qu'il n'est pas nécessaire que l'auteur de la classe connaisse. Mais il n'y a aucun moyen de l'utiliser pour hériter correctement de classes arbitraires qui peuvent ou ne peuvent pas utiliser super .

essentiellement, si une classe est conçue pour être sous-classée en utilisant super ou avec des appels directs à la classe de base est une propriété qui fait partie de la classe" "interface publique", et elle doit être documentée en tant que telle. Si vous utilisez des bibliothèques tierces de la manière que l'auteur de la bibliothèque attendait et la bibliothèque a une documentation raisonnable, il serait normalement vous dire ce que vous devez faire à la sous-classe des choses particulières. Si ce n'est pas le cas, alors vous devrez regarder le code source pour les classes que vous sous-classez et voir ce qu'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 n'attendaient pas , alors il ne sera peut-être pas possible d'invoquer systématiquement les méthodes de super-classe du tout ; si la classe a fait partie de une hiérarchie utilisant super et la Classe B fait partie d'une hiérarchie qui n'utilise pas super, alors aucune des deux options n'est garantie de fonctionner. Tu devras trouver une stratégie qui fonctionne pour chaque cas particulier.

2
répondu Ben 2016-01-13 16:42:59

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. L'exploitation de ce lien vous donne plus de cohérence et rend les retouches éventuelles plus faciles et moins sujettes aux erreurs. Un exemple de la façon de 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")
0
répondu Jundiaius 2018-05-22 09:15:42