这是由于属性查找的工作方式。在尝试检索
实例中的属性,Python 会这样做:
-
调用实例的类__getattribute__(不是元类__getattribute__),这反过来会:
-
按照方法解析顺序检查实例的类及其超类的属性。它不会继续到类的类(元类) - 它遵循继承链..
- 如果在类中找到该属性并且它具有
__get__ 方法,使其成为描述符:以实例及其类作为参数调用__get__ 方法 - 返回值用作属性值
-
注意:对于使用
__slots__ 的类,每个实例属性都记录在一个特殊的描述符中——该描述符存在于类本身中并且有一个__get__ 方法,因此插槽类的实例属性在此检索步骤
- 如果没有
__get__ 方法,它只是跳过类的搜索。
-
检查实例本身:该属性应作为实例__dict__ 属性中的条目存在。如果是,则返回相应的值。 (__dict__ 是一个特殊的属性,可以在 cPython 中直接访问,否则将遵循上面开槽属性的描述符规则)
-
再次检查类(及其继承层次结构)的属性,这一次,不管它是否具有__get__ 方法。如果找到,则使用它。类中的此属性检查直接在类及其超类__dict__ 中执行,而不是通过以递归方式调用它们自己的__getattribute__。 (*)
-
类(或超类)__getattr__ 方法被调用,如果它存在的话,带有属性名称。它可能会返回一个值,也可能会引发 AttributeError(__getattr__ 与低级 __getattribute__ 不同,更容易自定义)
-
引发了AttributeError。
(*) 这是回答您问题的步骤:不会在元类中搜索实例中的属性。在上面的代码中,如果您尝试使用A.cls_prop 作为属性,而不是A().cls_prop,它将起作用:直接从类中检索属性时,它在上面的检索算法中扮演“实例”的角色。
(**) 注意。这个属性检索算法描述比较完整,但是对于属性赋值和删除,不是检索,而是一个描述符有一些区别,根据它是否具有__set__(或__del__)方法,使其成为“数据描述符”与否:非数据描述符(例如在实例的类主体中定义的常规方法)直接分配在实例的字典上,因此仅针对该实例覆盖和“关闭”方法。数据描述符将调用其__set__ 方法。
如何使元类中定义的属性适用于实例:
如您所见,属性访问是非常可定制的,如果您想在可从实例工作的元类中定义“类属性”,则可以轻松自定义代码以使其正常工作。一种方法是添加到您的基类(而不是元类)中,__getattr__ 将在元类上查找自定义描述符并调用它们:
class Base(metaclass=Meta):
def __getattr__(self, name):
metacls = type(cls:=type(self))
if hasattr(metacls, name):
metaattr = getattr(metacls, name)
if isinstance(metaattr, property): # customize this check as you want. It is better not to call it for anything that has a `__get__`, as it would retrieve metaclass specific stuff, such as its __init__ and __call__ methods, if those were not defined in the class.
attr = metaattr.__get__(cls, metacls)
return attr
return super().__getattr__(name)
和:
In [44]: class A(Base):
...: pass
...:
In [45]: a = A()
In [46]: a.cls_prop
Out[46]: True