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

Why does getting a class property from an instance raise an AttributeError?

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

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'

您看到的错误不是因为属性和 属性 行为之间的任何差异,而是因为您在 Meta 中定义了 属性,在 A class 中定义了属性。如果您在 A 中定义 属性 它将正常工作。

这是由于属性查找的工作方式所致。在试图检索一个 实例中的属性,Python 做:

  1. 调用实例的class__getattribute__(不是元class__getattribute__),这反过来会:

    1. 检查实例的 class 及其超classes 是否遵循方法解析顺序,以获取属性。它不会继续到 class 的 class(metaclass)——它遵循继承链..

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

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

  2. class(或 superclasses)__getattr__ 方法被调用,如果它存在,带有属性名。可能return一个值,或者引发AttributeError(__getattr__与低级__getattribute__不同,更容易定制)

  3. 引发了 AttributeError。

(*) 这是回答您问题的步骤:未在元class 中搜索实例中的属性。在上面的代码中,如果您尝试将 A.cls_prop 用作 属性,而不是 A().cls_prop,它将起作用:当直接从 class 检索属性时,它需要“实例”在上述检索算法中的作用。

(**) 注意。这个属性检索算法描述的比较完整,但是对于属性的赋值和删除,而不是检索,描述符有一些区别,基于它是否具有__set__(或__del__)方法,使得它是否是“数据描述符”:非数据描述符(例如在实例的 class 主体中定义的常规方法)直接分配给实例的字典,因此覆盖和“关闭”方法只是为了那个例子。数据描述符将调用其 __set__ 方法。

如何使元class中定义的属性适用于实例:

如您所见,属性访问是非常可定制的,如果您想在元class 中定义“class 属性”,该元class 将在实例中运行,则很容易定制你的代码,以便它工作。一种方法是添加到你的 baseclass(不是 metaclass),一个 __getattr__ 将在 metaclass 上查找自定义描述符并调用它们:

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