为什么不动态地将`__call__`方法添加到实例工作中?

问题描述 投票:16回答:2

在Python 2和Python 3中代码:

class Foo(object):
    pass

f = Foo()
f.__call__ = lambda *args : args

f(1, 2, 3)

返回错误Foo object is not callable。为什么会这样?

PS:对于旧式的类,它按预期工作。

PPS:此行为是有意的(请参阅接受的答案)。作为一种解决方法,可以在类级别定义一个__call__,它只是转发给另一个成员,并将这个“普通”成员设置为每个实例的__call__实现。

python instance
2个回答
19
投票

总是在类上查找双下划线方法,而不是实例。见Special method lookup for new-style classes

对于新式类,只有在对象的类型上定义,而不是在对象的实例字典中,才能保证对特殊方法的隐式调用才能正常工作。

那是因为类型可能需要支持相同的操作(在这种情况下,特殊方法会在元类型上查找)。

例如,类是可调用的(这就是你生成实例的方式),但是如果Python在实际对象上查找__call__方法,那么你就永远不能在为它们的实例实现__call__的类上这样做。 ClassObject()将成为ClassObject.__call__(),因为self参数未传入未绑定方法而失败。因此,使用type(ClassObject).__call__(ClassObject),并调用instance()转换为type(instance).__call__(instance)

要解决这个问题,你可以在类中添加一个__call__方法,该类检查类的__call__属性,如果有,则调用它。


3
投票

在新的样式类(默认3.x)和继承自2.x中的对象的属性拦截方法__getattr____getattribute__ are no longer called for built-in operations上的dunder重载实例的方法,而不是搜索从类开始。

这背后的基本原理涉及由MetaClasses的存在引入的技术性。

因为类是元类的实例,并且因为元类可能定义了对类起作用的某些操作,所以跳过类(在本例中可以被认为是instance)是有意义的;您需要调用元类中定义的方法,该方法被定义为处理类(cls作为第一个参数)。如果使用了实例查找,则查找将使用为该类的实例(self)定义的类方法。

另一个(有争议的原因)涉及优化:由于实例上的内置操作通常非常频繁地调用,因此完全跳过实例查找并直接进入类可以节省一些时间。 [来源Lutz,学习Python,第5版]


这可能带来不便的主要区域是创建重载__getattr____getattribute__的代理对象,意图将调用转发给嵌入的内部对象。由于内置调用将完全跳过实例,因此它们不会被捕获,因为效果不会转发到嵌入对象。

一个简单而又乏味的解决方法是实际重载您希望在代理对象中拦截的所有dunders。然后在这些重载的dunders中,您可以根据需要将调用委托给内部对象。


我能想到的最简单的解决方法是使用Foo在类setattr上设置属性:

setattr(Foo, '__call__', lambda *args: print(args))

f(1, 2, 3)
(<__main__.Foo object at 0x7f40640faa90>, 1, 2, 3)
© www.soinside.com 2019 - 2024. All rights reserved.