Ordem de resolução de método (MRO) em classes de novo estilo?

94

No livro Python in a Nutshell (2ª edição), há um exemplo que usa
classes de estilo antigo para demonstrar como os métodos são resolvidos na ordem de resolução clássica e
como é diferente com a nova ordem.

Tentei o mesmo exemplo reescrevendo o exemplo no novo estilo, mas o resultado não é diferente do que foi obtido com as classes do estilo antigo. A versão python que estou usando para executar o exemplo é 2.5.2. Abaixo está o exemplo:

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__  

A chamada é instance.amethod()impressa Base1, mas de acordo com meu entendimento do MRO com novo estilo de classes, a saída deveria ter sido Base3. A chamada é Derived.__mro__impressa:

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

Não tenho certeza se meu entendimento de MRO com novas classes de estilo está incorreto ou se estou cometendo um erro bobo que não sou capaz de detectar. Por favor, me ajude a entender melhor o MRO.

sateesh
fonte

Respostas:

183

A diferença crucial entre a ordem de resolução para classes legadas e classes de novo estilo surge quando a mesma classe ancestral ocorre mais de uma vez na abordagem "ingênua" de profundidade - por exemplo, considere um caso de "herança de diamante":

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

aqui, no estilo legado, a ordem de resolução é D - B - A - C - A: então, ao procurar Dx, A é a primeira base na ordem de resolução para resolvê-lo, ocultando assim a definição em C. Enquanto:

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

aqui, novo estilo, a ordem é:

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

com Aforçado a entrar na ordem de resolução apenas uma vez e depois de todas as suas subclasses, de modo que as substituições (ou seja, a substituição do membro por C x) realmente funcionam de maneira sensata.

É uma das razões pelas quais as classes de estilo antigo devem ser evitadas: herança múltipla com padrões "semelhantes a losangos" simplesmente não funciona de maneira sensata com eles, mas funciona com o estilo novo.

Alex Martelli
fonte
2
"[a classe ancestral] A [é] forçada a entrar na ordem de resolução apenas uma vez e depois de todas as suas subclasses, de modo que as substituições (isto é, a substituição do membro x por C) realmente funcionam de maneira sensata." - Epifania! Graças a esta frase, posso fazer MRO na minha cabeça novamente. \ o / Muito obrigado.
Esteis
23

A ordem de resolução do método do Python é na verdade mais complexa do que apenas entender o padrão do diamante. Para realmente entendê-lo, dê uma olhada na linearização C3 . Descobri que realmente ajuda usar instruções de impressão ao estender métodos para rastrear o pedido. Por exemplo, qual você acha que seria a saída desse padrão? (Nota: o 'X' é suposto ser duas arestas cruzadas, não um nó e ^ significa métodos que chamam 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

Você conseguiu ABDCEFG?

x = A()
x.m()

Depois de muitas tentativas e erros, cheguei a uma interpretação informal da teoria dos grafos da linearização C3 da seguinte maneira: (Alguém, por favor, me avise se estiver errado.)

Considere este exemplo:

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()
Ben
fonte
Você deve corrigir o seu segundo código: você colocou a classe "I" como primeira linha e também usou super para encontrar a super classe "G", mas "I" é a primeira classe, então nunca será capaz de encontrar a classe "G" porque há não há "G" superior "I". Coloque a classe "I" entre "G" e "F" :)
Aaditya Ura
O código de exemplo está incorreto. supertem argumentos necessários.
danny
2
Dentro de uma definição de classe, super () não requer argumentos. Consulte https://docs.python.org/3/library/functions.html#super
Ben
Sua teoria dos gráficos é desnecessariamente complicada. Após a etapa 1, insira arestas de classes à esquerda para classes à direita (em qualquer lista de herança) e, em seguida, faça uma classificação topológica e pronto.
Kevin
@Kevin Não acho isso correto. Seguindo meu exemplo, ACDBEFGH não seria uma classificação topológica válida? Mas essa não é a ordem de resolução.
Ben
5

O resultado obtido está correto. Tente alterar a classe base de Base3para Base1e compare com a mesma hierarquia para classes clássicas:

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()

Agora ele produz:

Base3
Base1

Leia esta explicação para mais informações.

Denis Otkidach
fonte
1

Você está vendo esse comportamento porque a resolução do método prioriza a profundidade, não a amplitude. A herança de Dervied parece

         Base2 -> Base1
        /
Derived - Base3

assim instance.amethod()

  1. Verifica Base2, não encontra um método.
  2. Vê que Base2 herdou de Base1 e verifica Base1. Base1 tem um amethod, por isso é chamado.

Isso se reflete em Derived.__mro__. Simplesmente repita Derived.__mro__e pare quando encontrar o método que está sendo procurado.

Jamessan
fonte
Duvido que a razão pela qual recebo "Base1" como resposta seja porque a resolução do método prioriza a profundidade, acho que há mais do que uma abordagem em primeiro lugar. Veja o exemplo de Denis, se fosse a profundidade, primeiro o / p deveria ser "Base1". Consulte também o primeiro exemplo no link que você forneceu; lá também o MRO mostrado indica que a resolução do método não é determinada apenas pela passagem em profundidade de primeira ordem.
sábado
Desculpe, o link para o documento sobre MRO é fornecido por Denis. Por favor, verifique, eu confundi que você me forneceu o link para python.org.
sábado
4
Geralmente é a profundidade em primeiro lugar, mas há inteligência para lidar com a herança de diamante, como Alex explicou.
Jamessan