这是由于属性查找的工作方式造成的。在尝试检索
实例中的属性,Python 会:
-
调用实例的类__getattribute__
(不是元类__getattribute__
),这又会:
-
按照方法解析顺序检查实例的类及其超类的属性。它不会继续到类的类(元类)——它遵循继承链。
- if the attribute is found in the class and it has a
__get__
method, making it a descriptor: the __get__
method is called with the instance and its class as parameters - the returned value is used as the attribute value
-
note:对于使用的类
__slots__
,每个实例属性都记录在一个特殊的描述符中 - 它存在于类本身中并且具有__get__
方法,因此在这一步检索槽类的实例属性
- 如果没有
__get__
方法,它只是跳过类中的搜索。
-
检查实例本身:该属性应作为实例中的条目存在__dict__
属性。如果是,则返回相应的值。 (__dict__
是一个特殊属性,可以在 cPython 中直接访问,但否则会遵循上面的开槽属性的描述符规则)
-
再次检查该类(及其继承层次结构)的属性,这一次,无论它是否具有__get__
方法。如果找到,则使用它。类中的属性检查直接在类及其超类中执行__dict__
,而不是通过调用自己的__getattribute__
以递归方式。 (*)
-
类(或超类)__getattr__
如果存在,则使用属性名称调用方法。它可能返回一个值,或者引发 AttributeError(__getattr__
和低水平是不同的__getattribute__
,并且更容易定制)
-
引发属性错误。
(*) 这是回答您的问题的步骤:不会在元类中搜索实例中的属性。在上面的代码中,如果您尝试使用A.cls_prop
作为财产,而不是A().cls_prop
它会起作用:当直接从类中检索属性时,它在上面的检索算法中扮演“实例”的角色。
(**) 注意。该属性检索算法描述相当完整,但是对于属性分配和删除,而不是检索,描述符存在一些差异,具体取决于它是否具有属性__set__
(or __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)
and:
In [44]: class A(Base):
...: pass
...:
In [45]: a = A()
In [46]: a.cls_prop
Out[46]: True