为什么Python没有instancemethod函数?

2024-04-20 12:07:55 发布

您现在位置:Python中文网/ 问答频道 /正文

为什么Python没有类似于staticmethodclassmethodinstancemethod函数?你知道吗

我就是这样想的。假设我有一个对象,我知道它经常被散列,并且它的散列计算代价很高。在这种假设下,计算一次哈希值并将其缓存是合理的,如下例所示:

class A:
    def __init__(self, x):
        self.x = x
        self._hash_cache = hash(self.x)

    def __hash__(self):
        return self._hash_cache

这个类中的__hash__函数做的很少,只是一个属性查找和一个返回。天真地说,它应该等同于写:

class B:
    def __init__(self, x):
        self.x = x
        self._hash_cache = hash(self.x)

    __hash__ = operator.attrgetter('_hash_cache')

根据the documentationoperator.attrgetter返回一个可调用对象,该对象从其操作数中获取给定的属性。如果它的操作数是self,那么它将返回self._hash_cache,这是所需的结果。不幸的是,这不起作用:

>>> hash(A(1))
1
>>> hash(B(1))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: attrgetter expected 1 arguments, got 0

原因如下。如果读取the descriptor HOWTO,就会发现类字典将方法存储为函数;函数是非数据描述符,其__get__方法返回绑定方法。但是operator.attrgetter不返回函数;它返回可调用对象。实际上,它是一个没有__get__方法的可调用对象:

>>> hasattr(operator.attrgetter('_hash_cache'), '__get__')
False

缺少__get__方法,这当然不会自动变成绑定方法。我们可以使用types.MethodType从它生成一个绑定方法,但是在我们的类B中使用它需要为每个对象实例创建一个绑定方法并将其分配给__hash__。你知道吗

如果浏览CPython源代码,我们可以看到operator.attrgetter没有__get__的事实。我对cpythonapi不是很熟悉,但我相信现在的情况如下。attrgetter_type的定义在Modules/_operator.c中,在我写这篇文章的第1439行。此类型将tp_descr_get设置为0。根据type object documentation,这意味着类型为attrgetter_type的对象将没有__get__。你知道吗

当然,如果我们给自己一个__get__方法,那么一切都可以。这是上面第一个例子中的情况,其中__hash__实际上是一个函数,而不仅仅是一个可调用函数。在其他一些情况下也是如此。例如,如果要查找类属性,可以编写以下代码:

class C:
    y = 'spam'
    get_y = classmethod(operator.attrgetter('y'))

如前所述,这是非常不符合python的(尽管如果有一个奇怪的习惯__getattr__我们想为其提供方便的函数,这可能是可以辩护的)。但至少它给出了预期的结果:

>>> C.get_y()
'spam'

我想不出任何理由说明attrgetter_type实现__get__是不好的。但另一方面,即使它这样做了,也会有其他情况下,我们遇到麻烦。例如,假设我们有一个实例可调用的类:

class D:
    def __call__(self, other):
        ...

我们不能将此类的实例用作类属性,而期望实例查找生成绑定方法。例如

d = D()

class E:
    apply_d = d

调用D.__call__时,它将接收self,而不是other,这将生成一个TypeError。这个例子可能有点牵强,但如果没有人在实践中遇到过这样的事情,我会有点惊讶。它可以通过给D一个__get__方法来修复;但是如果D来自第三方库,这可能会带来不便。你知道吗

似乎最简单的解决方案是使用instancemethod函数。然后我们可以写__hash__ = instancemethod(operator.attrgetter('_hash_cache'))apply_d = instancemethod(d),它们都可以按预期工作。然而,据我所知,没有这样的功能存在。因此我的问题是:为什么没有instancemethod函数?你知道吗


编辑:为了清楚起见,instancemethod的功能相当于:

def instancemethod(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

这可以像上面原来的问题一样适用。我们还可以设想编写一个类装饰器,它可以应用于D,从而为它提供一个__get__方法;但这段代码并不能做到这一点。你知道吗

所以我不是说给Python添加一个新特性。实际上,问题是语言设计:为什么不提供它,比如说,functools.instancemethod?如果答案很简单,“使用c案例太晦涩了,没人在意,“没关系。但我很乐意了解其他原因,如果有的话。你知道吗


Tags: 对象实例方法函数selfcacheget属性
2条回答

没有instancemethod修饰符,因为这是类内声明的函数的默认行为。你知道吗

class A:
    ...

    # This is an instance method
    def __hash__(self):
        return self._hash_cache

因此,任何没有__get__方法的可调用方法都可以像这样包装到实例方法中。你知道吗

class A:
    def instance_method(*args):
        return any_callable(*args)

因此,创建instancemethod修饰符只会为已经存在的特性添加另一种语法。这与there should be one and preferably only one obvious way to do it的说法背道而驰。你知道吗

旁注

如果对实例进行哈希运算的成本太高,您可能希望避免在实例化时调用哈希函数,并在对对象进行哈希运算时将其延迟。你知道吗

一种方法是在__hash__中设置属性_hash_cache,而不是__init__。尽管如此,我还是建议使用一种更加独立的方法,它依赖于缓存散列。你知道吗

from weakref import finalize

class CachedHash:
    def __init__(self, x):
        self.x = x

    def __hash__(self, _cache={}):
        if id(self) not in _cache:
            finalize(self, _cache.pop, id(self))
            _cache[id(self)] = hash(self.x) # or some complex hash function
        return _cache[id(self)]

使用^{}可以确保当缓存的实例被垃圾收集时,缓存中的id被清除。你知道吗

我对我的问题有一个满意的答案。Python确实具有instancemethod函数所需的内部接口,但它在默认情况下不公开。你知道吗

import ctypes
import operator

instancemethod = ctypes.pythonapi.PyInstanceMethod_New
instancemethod.argtypes = (ctypes.py_object,)
instancemethod.restype = ctypes.py_object

class A:
    def __init__(self, x):
        self.x = x
        self._hash_cache = hash(x)

    __hash__ = instancemethod(operator.attrgetter('_hash_cache'))

a = A(1)
print(hash(a))

它创建的instancemethod函数的工作方式与classmethodstaticmethod基本相同。这三个函数分别返回instancemethodclassmethodstaticmethod类型的新对象。我们可以通过查看Objects/funcobject.c来了解它们是如何工作的。这些对象都有存储可调用对象的__func__成员。它们也有一个__get__。对于staticmethod对象,__get__返回__func__不变。对于classmethod对象,__get__返回绑定方法对象,其中绑定到类对象。对于staticmethod对象,__get__返回绑定方法对象,其中绑定到对象实例。这与函数对象的__get__行为完全相同,正是我们想要的。你知道吗

关于这些对象的唯一文档似乎在pythoncapihere中。我猜它们不会暴露是因为它们很少被需要。我认为PyInstanceMethod_New作为functools.instancemethod提供会很好。你知道吗

相关问题 更多 >