为什么Python没有instancemethod
和staticmethod
类似的classmethod
函数?
这就是我的想法。假设我有一个我知道会经常被散列并且其散列计算起来很昂贵的对象。在这种假设下,合理地计算一次哈希值并将其缓存,如以下玩具示例所示:
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 documentation,operator.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
不会返回 function ;它返回一个可调用对象。实际上,它是一个没有__get__
方法的可调用对象:
>>> hasattr(operator.attrgetter('_hash_cache'), '__get__')
False
缺少__get__
方法,这当然不会自动变成绑定方法。我们可以使用types.MethodType
从中创建一个绑定方法,但是在类B
中使用它需要为每个对象实例创建一个绑定方法并将其分配给__hash__
。
如果浏览CPython源代码,我们可以直接看到operator.attrgetter
没有__get__
的事实。我对CPython API不太熟悉,但是我相信正在发生的事情如下。在撰写本文时,attrgetter_type
的定义在Modules/_operator.c的第1439行中。此类型将tp_descr_get
设置为0
。根据{{3}},这意味着类型为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
的形式提供它?如果答案很简单,“用例太晦涩,没有人打扰”,那就可以了。但是,如果有其他原因,我很乐意了解其他原因。
答案 0 :(得分:3)
没有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_cache
中而不是__hash__
中设置属性__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)]
使用finalize
可确保在实例被垃圾回收时清除id
的缓存。
答案 1 :(得分:0)
我对我的问题有一个令人满意的答案。 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
函数的工作方式基本上与classmethod
和staticmethod
相同。这三个函数分别返回类型为instancemethod
,classmethod
和staticmethod
的新对象。我们可以通过查看Objects/funcobject.c
来了解它们的工作方式。这些对象都有__func__
个成员,这些成员存储可调用对象。他们也有一个__get__
。对于staticmethod
对象,__get__
不变地返回__func__
。对于classmethod
对象,__get__
返回一个绑定的方法对象,其中绑定是指向类对象的。对于staticmethod
对象,__get__
返回一个绑定方法对象,其中绑定是对象实例。对于函数对象,这与__get__
的行为完全相同,正是我们想要的。
关于这些对象的唯一文档似乎在Python C API link中。我的猜测是它们没有被暴露,因为它们很少需要。我认为将PyInstanceMethod_New
用作functools.instancemethod
会很好。