Python为什么没有实例方法功能?

时间:2019-04-28 06:15:26

标签: python

为什么Python没有instancemethodstaticmethod类似的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 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不会返回 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的形式提供它?如果答案很简单,“用例太晦涩,没有人打扰”,那就可以了。但是,如果有其他原因,我很乐意了解其他原因。

2 个答案:

答案 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函数的工作方式基本上与classmethodstaticmethod相同。这三个函数分别返回类型为instancemethodclassmethodstaticmethod的新对象。我们可以通过查看Objects/funcobject.c来了解它们的工作方式。这些对象都有__func__个成员,这些成员存储可调用对象。他们也有一个__get__。对于staticmethod对象,__get__不变地返回__func__。对于classmethod对象,__get__返回一个绑定的方法对象,其中绑定是指向类对象的。对于staticmethod对象,__get__返回一个绑定方法对象,其中绑定是对象实例。对于函数对象,这与__get__的行为完全相同,正是我们想要的。

关于这些对象的唯一文档似乎在Python C API link中。我的猜测是它们没有被暴露,因为它们很少需要。我认为将PyInstanceMethod_New用作functools.instancemethod会很好。