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
de __init__
:
- (à l'ancienne)
ParentClass.__init__(self)
- (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é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 A
ou B
proviennent 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 B
init 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 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
et B
) je ne peux pas faire un choix sûr pour la classe que j'écris ( C
).
la source
Réponses:
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 foisA.__init__
etB.__init__
.Lors de l'utilisation
super()
, les classes doivent être conçues pour l'héritage multiple coopératif oùC
appellesuper
, qui invoqueA
le code de qui appellera également celuisuper
qui invoqueB
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 avecsuper
.[Réponse à la question modifiée ultérieurement]
L'article référencé montre comment gérer cette situation en ajoutant une classe wrapper autour
A
etB
. 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.
la source
super().__init__()
approche. Si j'appelleA.__init__()
etB.__init__()
directement, alors (si A et B appellentsuper
) je reçois l'init de B appelé plusieurs fois.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:
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:
Important: notez que ni
Foo
niBar
appellesuper().__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 estobject
ne 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 queobject.__init__()
. ( Clause de non-responsabilité: éviter lessuper().__init__()
sous-object
classes est ma recommandation personnelle et en aucun cas un consensus convenu dans la communauté python. Certaines personnes préfèrent utilisersuper
dans 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'
object
une__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'appelsuper().__init__()
. Si votre classe hérite directement deobject
, assurez-vous d'ajouter un constructeur vide comme ceci: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
Avec
super
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 deFoo
etBar
(commeclass FooBar(Bar, Foo)
), vous devez mettre à jour lessuper
appels pour qu'ils correspondent. Sans cela,super
vous n'avez pas à vous en soucier et le code est beaucoup plus lisible.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
super
pour éviter d'avoir à coder en dur le nom de la classe parente.Exemple:
Les détails importants ici sont:
super().__init__()
et passe par tous les arguments qu'il reçoit.class FooBar(FooMixin, Bar)
. Si l'ordre des classes de base est incorrect, le constructeur du mixin ne sera jamais appelé.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:
Dans ce cas, l'ordre des classes parentes n'a pas d'importance. Nous pourrions aussi bien hériter du
CoopBar
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 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
CoopFoo
etCoopBar
héritent deobject
, mais ils appellent toujourssuper().__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.
la source
super().__init__(*args, **kwargs)
dans le mixin et à l'écrire en premier. Cela a tellement de sens.L'une ou l'autre approche ("nouveau style" ou "ancien style") fonctionnera si vous avez le contrôle sur le code source pour
A
etB
. Sinon, l'utilisation d'une classe d'adaptateur peut être nécessaire.Code source accessible: utilisation correcte du "nouveau style"
Ici, l'ordre de résolution de méthode (MRO) dicte ce qui suit:
C(A, B)
dicte d'A
abord, alorsB
. MRO estC -> A -> B -> object
.super(A, self).__init__()
continue le long de la chaîne MRO initiéeC.__init__
àB.__init__
.super(B, self).__init__()
continue le long de la chaîne MRO initiéeC.__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»
Ici, le MRO n'a pas d'importance, car
A.__init__
etB.__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
A
etB
proviennent d'une bibliothèque tierce - c'est-à-dire que vous n'avez aucun contrôle sur le code source deA
etB
? La réponse courte: vous devez concevoir une classe d'adaptateur qui implémente lessuper
appels 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:
A
ne met pas en œuvresuper
;B
Est-ce queLa classe
Adapter
implémente desuper
sorte 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:
A
outilssuper
;B
ne fait pasMême modèle ici, sauf que l'ordre d'exécution est activé
Adapter.__init__
;super
appelez d'abord, puis appelez explicitement. Notez que chaque cas avec des parents tiers nécessite une classe d'adaptateur unique.Bien que vous puissiez gérer les cas où vous ne contrôlez pas le code source de
A
etB
en utilisant une classe d'adaptateur, il est vrai que vous devez savoir comment les init des classes parentes implémententsuper
(le cas échéant) pour ce faire.la source
Comme Raymond l'a dit dans sa réponse, un appel direct à
A.__init__
etB.__init__
fonctionne très bien, et votre code serait lisible.Cependant, il n'utilise pas le lien d'héritage entre
C
ces 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:la source
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 appelezsuper
dansA
, l'super
appel se poursuit dans MRO. La classe suivante dans l'ordre estB
, 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/
la source
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.super
rend 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
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 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 utilisantsuper
et 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.la source