【问题标题】:Why does setting a descriptor on a class overwrite the descriptor?为什么在类上设置描述符会覆盖描述符?
【发布时间】:2019-10-16 19:09:08
【问题描述】:

简单的复制:

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,但没有回答重复的问题,我在 CPython 源代码中挖掘了更多作为学习练习。警告:我进入了杂草。我真的希望我能从a captain who knows those waters 那里得到帮助。为了我自己和未来读者的利益,我试图尽可能明确地追踪我正在查看的电话。

我已经看到很多墨水溢出了 __getattribute__ 应用于描述符的行为,例如查找优先级。 "Invoking Descriptors" 中的 Python sn-p 就在 For classes, the machinery is in type.__getattribute__()... 下方,在我看来与我认为 type_getattro 中对应的 CPython source 大致一致,我通过查看 "tp_slots" 然后查看 where tp_getattro is populated 来追踪它。 B.v 最初打印 __get__, obj=None, objtype=&lt;class '__main__.B'&gt; 的事实对我来说很有意义。

我不明白的是,为什么赋值B.v = 3会盲目地覆盖描述符,而不是触发v.__set__?我试图追踪 CPython 调用,再次从"tp_slots" 开始,然后查看where tp_setattro is populated,然后查看type_setattrotype_setattroappears to be_PyObject_GenericSetAttrWithDict 周围的薄包装。这就是我困惑的症结所在:_PyObject_GenericSetAttrWithDict 似乎有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 并非旨在举例说明“典型”或“推荐”描述符定义。告诉我何时调用方法是一个冗长的无操作。

【问题讨论】:

  • 对我来说,这在最后一行打印 3... 代码工作正常
  • 实例访问属性时应用描述符,而不是类本身。对我来说,奥秘在于为什么 __get__ 会起作用,而不是为什么 __set__ 不起作用。
  • @Jab OP 预计仍会调用 __get__ 方法。 B.v = 3 已经用int 有效地覆盖了属性。
  • @jasonharper 属性访问决定是否调用__get__object.__getattribute__type.__getattribute__ 的默认实现在使用实例或类时调用__get__。通过__set__ 分配 仅限实例。
  • @jasonharper 我相信描述符的__get__ 方法应该在从类本身调用时触发。根据how-to guide,这就是@classmethods 和@staticmethods 的实现方式。 @Jab 我想知道为什么 B.v = 3 能够覆盖类描述符。基于 CPython 实现,我预计 B.v = 3 也会触发 __set__

标签: python cpython python-descriptors


【解决方案1】:

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

要让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 的原因是允许用户自定义 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 将返回一些实例,例如&lt;mymodule.VocalDescriptor object at 0xdeadbeef&gt;,您可以与之交互。它实际上是描述符对象,定义为类属性,其状态B.v.__dict__B 的所有实例之间共享。

当然,由用户的代码来准确定义他们想要B.v 做什么,返回self 只是常见的模式。 classmethod 是一个描述符示例,它在此处执行不同的操作,请参阅 Descriptor HowTo Guide 了解 classmethod 的纯 Python 实现。

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

【讨论】:

  • 为了使这个答案完整,我要补充一点,__get__ 被设计为被称为实例属性或类属性,但__set__ 被设计为仅作为实例属性被调用。及相关文档:docs.python.org/3/reference/datamodel.html#object.__get__
  • @wim 太棒了!!与此同时,我再次查看了 type_setattro 调用链。我看到对 _PyObject_GenericSetAttrWithDict 的调用提供了类型(在我的情况下,在 B 点)。
  • _PyObject_GenericSetAttrWithDict 中,pulls the Py_TYPE of Btp,这是 B 的元类(在我的情况下为 type),那么它就是 元类 tpdescriptor short-circuiting logic 处理。所以直接在B 上定义的描述符 没有 被那个短路逻辑看到(因此在我的原始代码中 __set__ 没有被调用),但是在元类 上定义的描述符是 被短路逻辑看到。
  • 因此,在元类具有描述符的情况下,该描述符的__set__ 方法调用。
  • B.v 调用 getter 的原因是允许返回描述符实例本身。”我不同意这一点。如果描述符没有__get__ 方法,则无论如何都会返回描述符实例,因此没有为此引入对类描述符的__get__ 调用。在这种情况下调用__get__ 的原因正是为了允许与返回描述符实例不同的行为,例如返回绑定到类的方法,就像classmethod 所做的那样(参见this answer)。
【解决方案2】:

除非有任何覆盖,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__

【讨论】:

    【解决方案3】:

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

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

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

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

    this answer 中的更多信息。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-06-08
      • 2020-04-12
      • 2012-08-20
      相关资源
      最近更新 更多