__mro__与其他双下划线名称有何不同?

4

我遇到了一个关于双下划线名称的行为,但我不理解:

class A:
    pass

class B:
    pass

class C(A,B):
    __id__ = 'c'

c = C()
print(C.__mro__)  # print the method resolution order of class C
#print(c.__mro__) # AttributeError: 'C' object has no attribute '__mro__'
print(C.__id__)   # print 'c'
print(c.__id__)   # print 'c'

我知道 __name__ 的名称修饰是可用于重载运算符方法的,而对于 __name 则不适用。__id__ 表现得就像一个普通的类变量一样,可以通过类名称以及实例访问。

然而,__mro__ 只能通过类名称访问,实际上我甚至可以在 C 中显式地引入 __mro__

class C(A,B):
    __mro__ = 'bla'

print(C.__mro__) # print the method resolution order of class C
print(c.__mro__) # print 'bla'

我希望了解这个行为是Python内部的魔法还是可以在常规的Python代码中实现。[Python版本3.4.3]
1个回答

4

这与查找顺序有关。

不考虑描述符,Python首先检查对象的__dict__以查找属性。如果找不到,则会查找对象的类和类的基类以查找属性。如果还找不到,则会引发AttributeError。

这可能不容易理解,让我们通过一个简短的例子来说明:

#!/usr/bin/python3

class Foo(type):
    X = 10

class Bar(metaclass=Foo):
    Y = 20

baz = Bar()

print("X on Foo", hasattr(Foo, "X")) 
print("X on Bar", hasattr(Bar, "X")) 
print("X on baz", hasattr(baz, "X")) 

print("Y on Foo", hasattr(Foo, "Y")) 
print("Y on Bar", hasattr(Bar, "Y")) 
print("Y on baz", hasattr(baz, "Y")) 

输出结果为:
X on Foo True
X on Bar True
X on baz False
Y on Foo False
Y on Bar True
Y on baz True

如您所见,X已在元类Foo上声明。它可以通过元类的实例(类Bar)访问,但不能通过Barbaz__dict__访问,因为它只存在于Foo__dict__中,而不是Barbaz__dict__中。Python仅在“元”层次结构中检查一步。
有关元类魔法的更多信息,请参见问题“Python中的元类是什么?”上的优秀答案。
然而,这并不足以描述行为,因为每个Foo实例(即每个类)的__mro__都不同。
这可以使用描述符实现。在将属性名称查找对象的__dict__之前,Python会检查类及其基类的__dict__,以查看是否将描述符对象分配给名称。描述符是任何具有__get__方法的对象。如果是这种情况,则调用描述符对象的__get__方法,并从属性查找返回结果。通过将描述符分配给元类的属性,可以实现所见行为:描述符可以基于instance参数返回不同的值,但是仍然只能通过类和元类访问该属性,而不是类的实例。
描述符的一个主要示例是property。这里是一个简单的示例,其中具有与__mro__相同的行为的描述符:
class Descriptor:
   def __get__(self, instance, owner):
      return "some value based on {}".format(instance)


class OtherFoo(type):
   Z = Descriptor()

class OtherBar(metaclass=OtherFoo):
   pass

other_baz = OtherBar()

print("Z on OtherFoo", hasattr(OtherFoo, "Z"))
print("Z on OtherBar", hasattr(OtherBar, "Z"))
print("Z on other_baz", hasattr(other_baz, "Z"))

print("value of Z on OtherFoo", OtherFoo.Z)
print("value of Z on OtherBar", OtherBar.Z)

输出结果如下:
Z on OtherFoo True
Z on OtherBar True
Z on other_baz False
value of Z on OtherFoo some value based on None
value of Z on OtherBar some value based on <class '__main__.OtherBar'>

如您所见,OtherBarOtherFoo都可以访问 Z 属性,但other_baz不能。不过每个 OtherFoo 实例仍可以为 Z 属性赋不同的值,也就是每个使用 OtherFoo 元类的类。

元类一开始可能比较难理解,当涉及到描述符时更加复杂。建议阅读关于元类的相关问题以及Python中描述符的相关知识。


2
最后一个输出框看起来像是你复制粘贴了错误的代码块。 :) - acdr
@乔纳斯,我之前不了解元类的概念,所以花了一些时间才理解你的回答。总结一下,__mro__ 是一个在元类上定义的属性,只能通过该元类的实例类访问。此外,它被定义为一个描述符,因此优先于在类本身上显式定义的 __mro__ 属性。对吗? - Kevin S.
@KevinS。基本上正确!然而,描述符部分的原因是,对于元类的每个类,“__mro__”具有不同的值,即使它不能通过类的实例访问。通常,要为每个类获取不同的值,您必须在类的“__dict__”中设置属性,这将使其在类的实例中可访问。使用元类上的描述符,您可以获得每个类的不同值,而无需在类的实例上访问它。 - Jonas Schäfer
一个(希望有用的)例子:cls.__mro__ == type(cls).__dict__['__mro__'].__get__(cls) - Solomon Ucko

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接