【问题标题】:Why do Python descriptors copy?为什么 Python 描述符会复制?
【发布时间】:2014-10-26 04:26:39
【问题描述】:

(我编辑了这个问题,因为我认为它基本上仍然是我要问的问题,尽管我从 cmets 获得了一些理解。我不知道这是否允许,或者我应该问一个新的.)

以下代码

class A: c = lambda:0
a = A()
print(a.c is a.c)

打印 False。我知道这是因为 Python 认为 A.c 是一种方法,因为 c 在类级别被分配了一个函数。我有两个问题:

  • (不太重要)Python 如何判断某事物是否为函数?我认为如果它要成为一种方法,就必须明确定义它。 “任意可调用”显然不是标准:例如,不接受内置函数。

  • (更重要的是)我了解到“每当您通过 class.name 或 instance.name 查找方法时,都会创建一个新的方法对象”。有任何独立于实现的原因吗?也就是说,如果不制作副本,是否有任何语言功能无法正常工作? (当然,我知道a1.c不是a2.c,但是对于同一个对象a,ac是否总是同一个对象?或者至少,Ac总是同一个对象?)

【问题讨论】:

  • @IgnacioVazquez-Abrams 好的,抱歉。我认为以非平凡方式控制属性访问的任何东西都可以称为描述符。尽管如此,问题仍然存在:发生了什么以及如何解释它?
  • 所以,毕竟有描述符。 :-) 无论如何,是的,这回答了一些问题(如果您假设对 c 的访问通过描述符而对 b 的访问不通过),但提出了两个新问题:为什么 c 会通过描述符而 b 不会,以及为什么会描述符协议复制它检索到的东西?
  • 因为b 不是方法。
  • 当然可以,但 c 也不是。还是这样?

标签: python python-3.x lambda copy descriptor


【解决方案1】:

默认的function.__get__ method 会“复制”(创建新的方法实例):

/* Bind a function to an object */
static PyObject *
func_descr_get(PyObject *func, PyObject *obj, PyObject *type)
{
    if (obj == Py_None || obj == NULL) {
        Py_INCREF(func);
        return func;
    }
    return PyMethod_New(func, obj);
}

但你可以定义一个不复制的描述符:

from functools import partial

class D:
    def __init__(self, function, cached=False):
        self.function = function
        if cached:
            self.cache = {}
        else:
            self.cache = None

    def __get__(self, instance, klass):
        if instance is None: # C.m
            return self.function
        if self.cache is None: # no cache
            m = partial(self.function, instance)
        else:
            m = self.cache.get(instance)
            if m is None:
                m = self.cache[instance] = partial(self.function, instance)
        m.__self__ = instance
        return m # C().m

class C:
    m = D(print)
    cached = D(print, cached=True)

assert C.m is C.m
assert C.cached is C.cached
c = C()
assert c.m is not c.m
assert c.cached is c.cached

每次调用.__get__() 时重新创建方法可能比保持(可能是weakrefed)映射(实例-> 方法)和中断循环(由于__self__ = instance)更简单/更有效,以避免浪费内存。

【讨论】:

    【解决方案2】:

    以下是 Python 2.x 语言参考 says 关于实例属性的内容(也适用于 3.x)(向下滚动到“类实例”):

    当在那里没有找到一个属性,并且实例的类有一个 按该名称的属性,搜索继续类 属性。如果找到用户定义的类属性 函数对象或未绑定的用户定义方法对象,其 关联类是实例的类(称为 C) 属性引用已启动或其基础之一,它是 转换为绑定的用户定义方法对象,其 im_class 属性为 C,其 im_self 属性为实例。

    对于类的属性(仅在 2.x 中,但不在 3.x 中)(滚动到“类”):

    当一个类属性引用(比如 C 类)会产生一个 用户定义的函数对象或未绑定的用户定义的方法对象 其关联类是 C 或其基类之一,它是 转换为一个未绑定的用户定义方法对象,其 im_class 属性是 C。

    所以回答你的问题:

    1. 它是如何决定的?规范中特别提到“用户定义的函数对象”或“未绑定的用户定义的方法对象”。所以这些是这条规则适用的。这不是所有可调用类型。如果在同一页面上滚动到可调用类型部分,可调用类型的类型很多,其中“用户定义的函数”只是一种类型。

    2. (首先,这里更正一下:对于实例的属性访问,是的,创建了一个方法对象;对于类的属性访问,仅在 Python 2.x 上创建了一个方法对象——在 Python 3 中。 x 你只需把你放在那里的任何东西都拿回来,没有任何包装。)为了回答你的问题,大概 Python 实现可以返回相同的方法对象。它需要某种缓存或实习来做到这一点,这在存储方面有开销。规范没有说明这一点。并且当前的 CPython 实现不会返回相同的对象。无论哪种方式,您都不应该依赖它。

    【讨论】:

      【解决方案3】:

      我手边没有电脑。如果你输入print(id(a.b), id(a.b), id(a.c), id(a.c)) 会发生什么?如果第二对不同,则创建单独的对象,我们没有错误。

      【讨论】:

      • 不错的收获。它确实打印55981320 55981320 5065800 5065800。那么,你的暗示的反面成立吗?我们有错误吗? :-)
      • 我认为不是,但也许。我不经常使用 lambda,并且会假设我的理解有问题。
      • 另外,Ignacio Vazquez-Abrams 是对的。这些不是描述符。
      • 真的很奇怪。似乎 Python 认为 id 不应该改变,并以某种方式缓存它。 id(ac) == id(ac) 说真(这不是侥幸,len({id(ac) for _ in range(99)}) 给出 1),但如果我评估 d = id(ac) 和之后 d == id(ac),我得到 False。所以看起来我们有,如果不是一个错误,至少有一个非常奇怪的行为,这与原始问题无关(为什么 a.c 在制作副本时会制作副本)。 :-/
      • 好的,阅读@Ignacio 所指的内容,我意识到上述评论中发生了什么。但是我仍然想知道为什么“每当您通过 class.name 或 instance.name 查找方法时,都会创建一个新的方法对象”。某些功能是否必须像这样才能工作,或者它只是一个实现工件(可能是其他方式)?
      猜你喜欢
      • 1970-01-01
      • 2023-03-04
      • 1970-01-01
      • 1970-01-01
      • 2018-08-11
      • 1970-01-01
      • 2019-07-01
      相关资源
      最近更新 更多