为什么在 class 上设置描述符会覆盖描述符?

Why does setting a descriptor on a class overwrite the descriptor?

简单复制:

class VocalDescriptor(object):
    def __get__(self, obj, objtype):
        print('__get__, obj={}, objtype={}'.format(obj, objtype))
    def __set__(self, obj, val):
        print('__set__')

class B(object):
    v = VocalDescriptor()

B.v # prints "__get__, obj=None, objtype=<class '__main__.B'>"
B.v = 3 # does not print "__set__", evidently does not trigger descriptor
B.v # does not print anything, we overwrote the descriptor

这个问题有一个effective duplicate, but the duplicate was not answered, and I dug a bit more into the CPython source as a learning exercise. Warning: i went into the weeds. I'm really hoping I can get help from a captain who knows those waters。为了我自己和未来读者的利益,我试图尽可能明确地追踪我正在查看的电话。

我看到很多关于 __getattribute__ 应用于描述符的行为的问题,例如查找优先级。 "Invoking Descriptors" just below For classes, the machinery is in type.__getattribute__()... roughly agrees in my mind with what I believe is the corresponding CPython source in type_getattro, which I tracked down by looking at "tp_slots" then where tp_getattro is populated 中的 Python 片段。 B.v 最初打印 __get__, obj=None, objtype=<class '__main__.B'> 这一事实对我来说很有意义。

我不明白的是,为什么赋值B.v = 3会盲目覆盖描述符,而不是触发v.__set__?我试图追踪 CPython 调用,再次从 "tp_slots", then looking at where tp_setattro is populated, then looking at type_setattro. type_setattro appears to be a thin wrapper around _PyObject_GenericSetAttrWithDict. And there's the crux of my confusion: _PyObject_GenericSetAttrWithDict appears to have logic that gives precedence to a descriptor's __set__ method 开始!!考虑到这一点,我无法弄清楚为什么 B.v = 3 盲目覆盖 v 而不是触发 v.__set__.

免责声明 1:我没有使用 printfs 从源代码重建 Python,所以我不是 完全确定 type_setattro 是在 B.v = 3.

期间调用的内容

免责声明 2:VocalDescriptor 并非旨在举例说明 "typical" 或 "recommended" 描述符定义。告诉我何时调用方法是一个冗长的空操作。

你是对的,B.v = 3 只是用一个整数覆盖了描述符(这是应该的)。在描述符协议中,__get__ is designed to be called as instance attribute or class attribute,但__set__被设计为仅作为实例属性被调用。

为了 B.v = 3 调用描述符,描述符应该在元class 上定义,即在 type(B).

上定义
>>> class BMeta(type): 
...     v = VocalDescriptor() 
... 
>>> class B(metaclass=BMeta): 
...     pass 
... 
>>> B.v = 3 
__set__

要在 B 上调用描述符,您可以使用实例:B().v = 3 即可。

B.v 也调用 getter 的原因是允许用户自定义 B.v 的功能,独立于 B().v 的功能。一种常见的模式是允许直接访问描述符实例,方法是在使用 class 属性访问时 returning 描述符本身:

class VocalDescriptor(object):
    def __get__(self, obj, objtype):
        if obj is None:
            return self
        print('__get__, obj={}, objtype={}'.format(obj, objtype))
    def __set__(self, obj, val):
        print('__set__')

现在 B.v 会 return 像 <mymodule.VocalDescriptor object at 0xdeadbeef> 这样的实例,您可以与之交互。它实际上是描述符对象,定义为 class 属性,其状态 B.v.__dict__B.

的所有实例之间共享

当然,用户的代码可以准确定义他们想要 B.v 做什么,returning self 只是常见的模式。用于 classmethod.

的纯 python 实现的 classmethod is an example of a descriptor which does something different here, see the Descriptor HowTo Guide

与可用于独立自定义 B().vB.v__get__ 不同,除非对实例进行属性访问,否则不会调用 __set__。我认为使用相同的描述符 v 自定义 B().v = otherB.v = other 的目标并不普遍或有用,不足以使描述符协议进一步复杂化,特别是因为后者仍然可以通过无论如何,metaclass 描述符,如上文 BMeta.v 所示。

除非有任何覆盖,B.v 等同于 type.__getattribute__(B, "v"),而 b = B(); b.v 等同于 object.__getattribute__(b, "v")。如果定义,这两个定义都会调用结果的 __get__ 方法。

请注意,对 __get__ 的调用在每种情况下都不同。 B.v 传递 None 作为第一个参数,而 B().v 传递实例本身。在这两种情况下,B 作为第二个参数传递。

另一方面,

B.v = 3 等同于 type.__setattr__(B, "v", 3),它 调用 __set__.

我认为当前答案中 none 确实回答了您的问题。

Why does setting a descriptor on a class overwrite the descriptor?

在拥有描述符(例如 cls.descr = 3del cls.descr) 覆盖该描述符,因为否则无法更改错误的描述符(例如 descr.__set__(None, cls, 3)descr.__delete__(None, cls) 引发异常),因为 class 字典(例如 cls.__dict__)是只读 types.MappingProxyType。如果您想覆盖 class 上的设置或删除属性,您始终可以在元 class 上定义描述符,该 class 是该元 class 的一个实例。所以 __set____delete__ 总是传递一个拥有描述符的 class 的实例,这就是为什么它们没有 owner 参数。

在拥有描述符(例如 cls.descr)的 class(或 class 的子class)上获取属性不会覆盖该描述符,因为它不会阻止更改错误的描述符(例如 descr.__get__(None, cls) 引发异常)。因此 __get__ 被传递给拥有描述符的 class 的实例,或者 class(或 class 的子class)本身,即为什么它有一个 owner 参数。

更多信息见