Ordre de résolution de méthode (MRO) dans les classes de style nouveau?


94

Dans le livre Python in a Nutshell (2e édition), il y a un exemple qui utilise
des classes de style ancien pour montrer comment les méthodes sont résolues dans l'ordre de résolution classique et en
quoi est-ce différent avec le nouvel ordre.

J'ai essayé le même exemple en réécrivant l'exemple dans un nouveau style mais le résultat n'est pas différent de ce qui a été obtenu avec des classes de style ancien. La version python que j'utilise pour exécuter l'exemple est 2.5.2. Voici l'exemple:

class Base1(object):  
    def amethod(self): print "Base1"  

class Base2(Base1):  
    pass

class Base3(object):  
    def amethod(self): print "Base3"

class Derived(Base2,Base3):  
    pass

instance = Derived()  
instance.amethod()  
print Derived.__mro__  

L'appel instance.amethod()s'imprime Base1, mais selon ma compréhension du MRO avec un nouveau style de classes, la sortie aurait dû être Base3. L'appel Derived.__mro__s'imprime:

(<class '__main__.Derived'>, <class '__main__.Base2'>, <class '__main__.Base1'>, <class '__main__.Base3'>, <type 'object'>)

Je ne sais pas si ma compréhension de la MRO avec les nouvelles classes de style est incorrecte ou si je fais une erreur stupide que je ne suis pas capable de détecter. Veuillez m'aider à mieux comprendre la MRO.

Réponses:


184

La différence cruciale entre l'ordre de résolution des classes héritées et des classes de style nouveau survient lorsque la même classe d'ancêtre apparaît plus d'une fois dans l'approche "naïve", profondeur d'abord - par exemple, considérons un cas d '"héritage de diamant":

>>> class A: x = 'a'
... 
>>> class B(A): pass
... 
>>> class C(A): x = 'c'
... 
>>> class D(B, C): pass
... 
>>> D.x
'a'

ici, dans le style hérité, l'ordre de résolution est D - B - A - C - A: donc lors de la recherche de Dx, A est la première base de résolution afin de le résoudre, cachant ainsi la définition en C.

>>> class A(object): x = 'a'
... 
>>> class B(A): pass
... 
>>> class C(A): x = 'c'
... 
>>> class D(B, C): pass
... 
>>> D.x
'c'
>>> 

ici, nouveau style, l'ordre est:

>>> D.__mro__
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, 
    <class '__main__.A'>, <type 'object'>)

avec Aforcé de venir dans l'ordre de résolution une seule fois et après toutes ses sous-classes, de sorte que les remplacements (c'est-à-dire le remplacement du membre par C x) fonctionnent réellement de manière raisonnable.

C'est l'une des raisons pour lesquelles les classes à l'ancienne doivent être évitées: l'héritage multiple avec des motifs «en forme de diamant» ne fonctionne tout simplement pas de manière raisonnable avec eux, alors qu'il le fait avec le nouveau style.


2
"[la classe ancêtre] A [est] forcée de venir dans l'ordre de résolution une seule fois et après toutes ses sous-classes, de sorte que les remplacements (c'est-à-dire le remplacement du membre x par C) fonctionnent réellement." - Epiphanie! Grâce à cette phrase, je peux refaire du MRO dans ma tête. \ o / Merci beaucoup.
Esteis

23

L'ordre de résolution des méthodes de Python est en fait plus complexe que la simple compréhension du modèle de diamant. Pour vraiment le comprendre, jetez un œil à la linéarisation C3 . J'ai trouvé qu'il était vraiment utile d'utiliser des instructions d'impression lors de l'extension de méthodes pour suivre la commande. Par exemple, que pensez-vous de la sortie de ce modèle? (Remarque: le 'X' est supposé être deux arêtes croisées, pas un nœud et ^ signifie des méthodes qui appellent super ())

class G():
    def m(self):
        print("G")

class F(G):
    def m(self):
        print("F")
        super().m()

class E(G):
    def m(self):
        print("E")
        super().m()

class D(G):
    def m(self):
        print("D")
        super().m()

class C(E):
    def m(self):
        print("C")
        super().m()

class B(D, E, F):
    def m(self):
        print("B")
        super().m()

class A(B, C):
    def m(self):
        print("A")
        super().m()


#      A^
#     / \
#    B^  C^
#   /| X
# D^ E^ F^
#  \ | /
#    G

Avez-vous eu ABDCEFG?

x = A()
x.m()

Après de nombreux essais d'erreur, j'ai proposé une interprétation informelle de la théorie des graphes de la linéarisation C3 comme suit: (Quelqu'un s'il vous plaît laissez-moi savoir si c'est faux.)

Prenons cet exemple:

class I(G):
    def m(self):
        print("I")
        super().m()

class H():
    def m(self):
        print("H")

class G(H):
    def m(self):
        print("G")
        super().m()

class F(H):
    def m(self):
        print("F")
        super().m()

class E(H):
    def m(self):
        print("E")
        super().m()

class D(F):
    def m(self):
        print("D")
        super().m()

class C(E, F, G):
    def m(self):
        print("C")
        super().m()

class B():
    def m(self):
        print("B")
        super().m()

class A(B, C, D):
    def m(self):
        print("A")
        super().m()

# Algorithm:

# 1. Build an inheritance graph such that the children point at the parents (you'll have to imagine the arrows are there) and
#    keeping the correct left to right order. (I've marked methods that call super with ^)

#          A^
#       /  |  \
#     /    |    \
#   B^     C^    D^  I^
#        / | \  /   /
#       /  |  X    /   
#      /   |/  \  /     
#    E^    F^   G^
#     \    |    /
#       \  |  / 
#          H
# (In this example, A is a child of B, so imagine an edge going FROM A TO B)

# 2. Remove all classes that aren't eventually inherited by A

#          A^
#       /  |  \
#     /    |    \
#   B^     C^    D^
#        / | \  /  
#       /  |  X    
#      /   |/  \ 
#    E^    F^   G^
#     \    |    /
#       \  |  / 
#          H

# 3. For each level of the graph from bottom to top
#       For each node in the level from right to left
#           Remove all of the edges coming into the node except for the right-most one
#           Remove all of the edges going out of the node except for the left-most one

# Level {H}
#
#          A^
#       /  |  \
#     /    |    \
#   B^     C^    D^
#        / | \  /  
#       /  |  X    
#      /   |/  \ 
#    E^    F^   G^
#               |
#               |
#               H

# Level {G F E}
#
#         A^
#       / |  \
#     /   |    \
#   B^    C^   D^
#         | \ /  
#         |  X    
#         | | \
#         E^F^ G^
#              |
#              |
#              H

# Level {D C B}
#
#      A^
#     /| \
#    / |  \
#   B^ C^ D^
#      |  |  
#      |  |    
#      |  |  
#      E^ F^ G^
#            |
#            |
#            H

# Level {A}
#
#   A^
#   |
#   |
#   B^  C^  D^
#       |   |
#       |   |
#       |   |
#       E^  F^  G^
#               |
#               |
#               H

# The resolution order can now be determined by reading from top to bottom, left to right.  A B C E D F G H

x = A()
x.m()

Vous devez corriger votre deuxième code: vous avez mis la classe "I" en première ligne et également utilisé super donc il trouve la super classe "G" mais "I" est la première classe donc il ne pourra jamais trouver la classe "G" car il n'est pas "G" supérieur "I". Mettez la classe "I" entre "G" et "F" :)
Aaditya Ura

L'exemple de code est incorrect. supera des arguments requis.
danny

2
Dans une définition de classe, super () ne nécessite pas d'arguments. Voir https://docs.python.org/3/library/functions.html#super
Ben

Votre théorie des graphes est inutilement compliquée. Après l'étape 1, insérez les arêtes des classes de gauche aux classes de droite (dans n'importe quelle liste d'héritage), puis effectuez un tri topologique et vous avez terminé.
Kevin

@Kevin Je ne pense pas que ce soit correct. Suivant mon exemple, ACDBEFGH ne serait-il pas un tri topologique valide? Mais ce n'est pas l'ordre de résolution.
Ben

5

Le résultat que vous obtenez est correct. Essayez de changer la classe de base de Base3to Base1et de comparer avec la même hiérarchie pour les classes classiques:

class Base1(object):
    def amethod(self): print "Base1"

class Base2(Base1):
    pass

class Base3(Base1):
    def amethod(self): print "Base3"

class Derived(Base2,Base3):
    pass

instance = Derived()
instance.amethod()


class Base1:
    def amethod(self): print "Base1"

class Base2(Base1):
    pass

class Base3(Base1):
    def amethod(self): print "Base3"

class Derived(Base2,Base3):
    pass

instance = Derived()
instance.amethod()

Maintenant, il sort:

Base3
Base1

Lisez cette explication pour plus d'informations.


1

Vous voyez ce comportement parce que la résolution de la méthode est la profondeur d'abord, pas la largeur d'abord. L'héritage de Dervied ressemble à

         Base2 -> Base1
        /
Derived - Base3

Alors instance.amethod()

  1. Vérifie Base2, ne trouve pas de méthode.
  2. Voit que Base2 a hérité de Base1 et vérifie Base1. Base1 a un amethod, donc il est appelé.

Cela se reflète dans Derived.__mro__. Répétez simplement Derived.__mro__et arrêtez lorsque vous trouvez la méthode recherchée.


Je doute que la raison pour laquelle j'obtiens "Base1" comme réponse soit parce que la résolution de la méthode est d'abord la profondeur, je pense qu'il y a plus qu'une approche de la profondeur d'abord. Voir l'exemple de Denis, si c'était d'abord la profondeur, o / p aurait dû être "Base1". Reportez-vous également au premier exemple dans le lien que vous avez fourni, là aussi le MRO indiqué indique que la résolution de la méthode n'est pas seulement déterminée en parcourant dans le premier ordre de profondeur.
sateesh

Désolé, le lien vers le document sur MRO est fourni par Denis. Veuillez vérifier cela, je me suis trompé en disant que vous m'aviez fourni le lien vers python.org.
sateesh

4
C'est généralement la profondeur d'abord, mais il y a de l'intelligence pour gérer l'héritage de type diamant, comme Alex l'a expliqué.
jamessan
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.