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

Mic*_*lli 10 python cpython python-descriptors

简单再现:

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
Run Code Online (Sandbox Code Playgroud)

这个问题有一个有效的重复项,但是没有回答重复项,作为学习练习,我对CPython源码进行了更多研究。警告:我进入了杂草。我真希望我能从知道这些水域的船长那里得到帮助。为了我自己的未来利益和未来读者的利益,我试图尽可能明确地追踪正在寻找的电话。

我已经看到很多墨水溅到了__getattribute__应用于描述符的行为上,例如查找优先级。Python的片断在“援引描述符”下方For classes, the machinery is in type.__getattribute__()...大致在我的脑海里同意我认为是相应的CPython的源type_getattro,我找到了通过看“tp_slots”然后tp_getattro填充其中B.v最初打印的事实__get__, obj=None, objtype=<class '__main__.B'>对我来说很有意义。

我不明白的是,为什么分配会B.v = 3盲目地覆盖描述符,而不是触发v.__set__?我尝试跟踪CPython调用,从“ tp_slots”再次开始,然后查看tp_setattro的填充位置,然后查看type_setattrotype_setattro 似乎是_PyObject_GenericSetAttrWithDict的薄包装。而且我困惑的症结在于: _PyObject_GenericSetAttrWithDict似乎有逻辑优先于描述符的__set__方法!考虑到这一点,我想不出为什么B.v = 3盲目地重写v而不是触发v.__set__

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

免责声明2: VocalDescriptor无意举例说明“典型”或“推荐”描述符定义。告诉我何时调用方法是一个冗长的操作。

wim*_*wim 6

正确的是,B.v = 3用一个整数覆盖描述符是正确的(应该这样做)。

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

>>> class BMeta(type): 
...     v = VocalDescriptor() 
... 
>>> class B(metaclass=BMeta): 
...     pass 
... 
>>> B.v = 3 
__set__
Run Code Online (Sandbox Code Playgroud)

要在上调用描述符B,您可以使用一个实例:B().v = 3将其执行。

B.v调用getter 的原因是允许返回描述符实例本身。通常,您会这样做,以允许通过类对象访问描述符:

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__')
Run Code Online (Sandbox Code Playgroud)

现在B.v将返回一些<mymodule.VocalDescriptor object at 0xdeadbeef>您可以与之交互的实例。它实际上是描述符对象,定义为类属性,并且其状态B.v.__dict__在的所有实例之间共享B

当然,完全由用户代码定义他们想要B.v做什么,返回self只是常见的模式。

  • 为了使这个答案完整,我要补充一点,“__get__”被设计为作为实例属性或类属性调用,但“__set__”被设计为仅作为实例属性调用。以及相关文档:https://docs.python.org/3/reference/datamodel.html#object.__get__ (2认同)