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


10

简单再现:

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

这个问题有一个有效的重复项,但是没有回答重复项,因此,作为学习练习,我对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无意举例说明“典型”或“推荐”描述符定义。告诉我何时调用方法是一个冗长的操作。


1
对我来说,这在最后一行输出3 ...代码工作正常
Jab

3
描述符适用于从实例而非类本身访问属性的情况。对我来说,奥秘在于为什么能__get__奏效,而不是为什么__set__没有奏效。
杰森哈珀(Jasonharper)

1
@Jab OP预计仍将调用该__get__方法。 B.v = 3有效地使用覆盖了该属性int
r.ook

2
@jasonharper属性访问判断是否__get__被调用,并且的默认的实现object.__getattribute__type.__getattribute__调用__get__使用实例或类时。通过分配__set__仅是实例。
chepner

@jasonharper我相信描述符的__get__方法应该在从类本身调用时触发。根据操作指南,这就是@classmethods和@staticmethods的实现方式。@Jab我想知道为什么B.v = 3能够覆盖类描述符。基于CPython实现,我希望B.v = 3也会触发__set__
迈克尔·卡里利

Answers:


6

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

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

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

要在上调用描述符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__')

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

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


1
为了使这个答案完整,我将添加一个__get__被设计为实例属性或类属性的名称,但__set__仅被设计为实例属性的名称。及相关文档:docs.python.org/3/reference/datamodel.html#object.__get__
sanyash

@wim宏伟!同时,我再次查看了type_setattro调用链。我看到对_PyObject_GenericSetAttrWithDict的调用提供了类型(在我的情况下为B点)。
迈克尔·卡里利

在其中_PyObject_GenericSetAttrWithDict,它将B的Py_TYPE拉为as tp,这是B的元类(type在我的情况下),然后由描述符短路逻辑处理的是元类 。如此定义直接在描述符不是由短路逻辑看出(因此在我的原代码不被调用),但在元类定义的描述符由短路逻辑看出。tpB __set__
迈克尔·卡里利

因此,在你的情况下元类有一个描述符中,__set__该描述符的方法调用。
迈克尔·卡里利

@sanyash可以直接直接编辑。
WIM

3

除非有任何替代,否则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__

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.