为什么从实例获取类属性会引发 AttributeError?

key*_*ian 2 python properties metaclass class attributeerror

通常,您可以从该类的实例访问常规类属性/字段。但是,当尝试访问类属性时,会引发 AttributeError。为什么实例看不到类对象的属性?

class Meta(type):

    @property
    def cls_prop(cls):
        return True


class A(metaclass=Meta):
    cls_attr = True


A.cls_attr # True
A.cls_prop # True
a = A()
a.cls_attr # True
a.cls_prop # AttributeError: 'A' object has no attribute 'cls_prop'
Run Code Online (Sandbox Code Playgroud)

jsb*_*eno 7

这是由于属性查找的工作方式造成的。当尝试检索实例中的属性时,Python 会执行以下操作:

  1. 调用实例的类__getattribute__(不是元类__getattribute__),这反过来将:

    1. 按照方法解析顺序检查实例的类及其超类的属性。它不会继续到类的类(元类)——它遵循继承链。

      1. 如果在类中找到该属性并且它有一个__get__方法,则使其成为描述符:__get__使用实例及其类作为参数调用该方法 - 返回的值用作属性值
        • 注意:对于使用的类__slots__,每个实例属性都记录在一个特殊的描述符中 - 该描述符存在于类本身中并且有一个__get__方法,因此在此步骤中检索开槽类的实例属性
      2. 如果没有__get__方法,它只是跳过类中的搜索。
    2. 检查实例本身:该属性应作为实例__dict__属性中的条目存在。如果是,则返回相应的值。(__dict__是一个特殊属性,可在 cPython 中直接访问,但否则将遵循上面的开槽属性的描述符规则)

    3. 再次检查该类(及其继承层次结构)的属性,这一次,无论它是否有方法__get__。如果找到,则使用它。类中的属性检查直接在类及其超类中执行,而不是通过以递归方式__dict__调用它们自己的属性。__getattribute__(*)

  2. __getattr__如果存在,则使用属性名称调用类(或超类)方法。它可能会返回一个值,或者引发 AttributeError(__getattr__与低级别不同__getattribute__,并且更容易定制)

  3. 引发属性错误。

(*) 这是回答您的问题的步骤:不会在元类中搜索实例中的属性。在上面的代码中,如果您尝试将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)
Run Code Online (Sandbox Code Playgroud)

和:

In [44]: class A(Base):
    ...:     pass
    ...:

In [45]: a = A()

In [46]: a.cls_prop
Out[46]: True
Run Code Online (Sandbox Code Playgroud)