13

简单的复制:

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_setattroB.v = 3.

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

4

3 回答 3

7

您是正确的,B.v = 3只需用整数覆盖描述符(应该如此)。在描述符协议中,__get__被设计为作为实例属性或类属性被调用,但__set__被设计为仅作为实例属性被调用。

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

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

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

还调用 getter的原因B.v是允许用户自定义所做的事情B.v,而与所做的事情无关B().v。一种常见的模式是允许直接访问描述符实例,方法是在使用类属性访问时返回描述符本身:

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只是常见的模式。Aclassmethod是一个描述符示例,它在此处执行不同的操作,请参阅描述符操作指南以了解classmethod.

除非属性访问在实例上,否则不调用__get__可用于自定义B().vB.v独立的不同。__set__我认为自定义B().v = otherB.v = other使用相同描述符的目标v并不常见或有用,不足以使描述符协议进一步复杂化,特别是因为后者仍然可以使用元类描述符,如上所示BMeta.v

于 2019-10-16T19:28:53.767 回答
4

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

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

B.v = 3另一方面,等价于type.__setattr__(B, "v", 3),它调用__set__

于 2019-10-16T19:38:30.283 回答
0

我认为当前的答案都没有真正回答您的问题。

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

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

在拥有描述符(例如cls.descr)的类(或类的子类)上获取属性不会覆盖该描述符,因为它不会阻止更改错误的描述符(例如descr.__get__(None, cls)引发异常)。因此__get__,要么传递拥有描述符的类的实例,要么传递类(或类的子类)本身,这就是它具有owner参数的原因。

此答案中的更多信息。

于 2021-04-13T08:40:35.507 回答