你以为你在调用谁?#
Python 中的对象,只要在类上定义了 __call__,就可以像函数一样调用。
例如:
class doubleit:
def __call__(self, number):
return number * 2
db = doubleit()
print(db(3))
print(db.__call__(3))python两行都输出 6,非常合理。
于是可能会以为,db(3) 就等同于 db.__call__(3),前者只是后者的语法糖。
对…对对吗?
稍微改动一下:
class doubleit:
def __call__(self, number):
return number * 2
db = doubleit()
db.__call__ = lambda: "Fake __call__ Hahaha"
print(db.__call__())
print(db(3))python结果是:
Fake __call__ Hahaha
6text明明把 __call__ 换掉了,怎么 db(3) 还在用原来的那个?!
特 · 殊 · 方 · 法#
关键在于,这两种写法走的查找路线不同。
db.__call__ 是一次显式的属性访问。对于这个例子,Python 会先在实例自己的 __dict__ 中找到刚刚放进去的 lambda,于是调用的就是它。
但 db(3) 触发的是特殊方法的隐式调用,会从 db 的类型上查找 __call__,绕过实例自己的同名属性。对于这里的普通方法,可以把它理解成:
type(db).__call__(db, 3)text也就是 doubleit.__call__(db, 3),所以结果还是 6。类上查找时也会沿着 MRO 找父类,并不要求方法必须写在当前类里面。
当然,这里的 type(db).__call__(db, ...) 只是帮助理解的近似写法,不是说解释器真的把代码改写成了这句。隐式查找通常还会绕过自定义的 __getattribute__。这套规则也不只服务于 callable,len(obj) 等操作同样会用到它。
既然实例上的改动没用,那直接改类呢?
class doubleit:
def __call__(self, number):
return number * 2
db = doubleit()
db.__call__ = lambda: "Fake __call__ Hahaha"
doubleit.__call__ = lambda self, number: number * 3
print(db.__call__())
print(db(3))python这次输出:
Fake __call__ Hahaha
9text实例上的假 __call__ 还在,但是 db(3) 已经换了一套逻辑。
再反过来试试,如果类上根本没有 __call__,只给实例塞一个呢?
class Empty:
pass
obj = Empty()
obj.__call__ = lambda: "I can call myself."
print(obj.__call__())
print(callable(obj))
try:
obj()
except TypeError:
print("TypeError: obj is not callable")pythonI can call myself.
False
TypeError: obj is not callabletext有一个能调用的 obj.__call__,竟然不代表 obj 能被调用。
哟,会调用啦?#
接下来,把 callable 和装饰器放到一起。
函数可以当装饰器,按理说任何 callable 也可以。先写一个让结果重复两遍的装饰器:
class doubleit:
def __init__(self, fn):
self.fn = fn
def __call__(self, *args, **kwargs):
return self.fn(*args, **kwargs) + ', ' + self.fn(*args, **kwargs)
@doubleit
def foo():
return 'foo'
print(foo())python输出 foo, foo,没有问题。
这里的 @doubleit 相当于在函数定义后执行 foo = doubleit(foo)。所以现在的 foo 已经不是原来的函数,而是一个 doubleit 实例;foo() 调用的就是这个实例对应的 __call__。
那把它搬到类里面,装饰一下方法呢?
class doubleit:
def __init__(self, fn):
self.fn = fn
def __call__(self, *args, **kwargs):
return self.fn(*args, **kwargs) + ', ' + self.fn(*args, **kwargs)
class MyClass:
@doubleit
def foo(self):
return 'foo'
obj = MyClass()
try:
print(obj.foo())
except TypeError:
print("TypeError: foo() is missing self")python咦?咋不行了咧?
弄丢的 self#
还记得 Python 颠覆认知的冷知识 #3 里面的实验吗?普通函数放在类上,通过实例访问时,会得到一个绑定了实例的 bound method,调用时就会自动传入 self。
但这不是所有可调用对象都自带的待遇。
上面的 MyClass.foo 已经被替换成了一个 doubleit 实例。它虽然有 __call__,却没有负责方法绑定的 __get__。
所以 obj.foo 取得的就是那个装饰器实例。调用 obj.foo() 时,进入 doubleit.__call__ 的 args 是空元组,接下来 self.fn() 自然就少了原函数需要的那个 self。
注意这里有两个不同的 self:doubleit.__call__ 的 self 是装饰器实例,原函数 foo 想要的 self 则是 MyClass 的实例 obj。
至于 self.fn,原函数被保存在装饰器实例的属性里,不会因为这次访问就自动绑定到 obj 上。
普通函数能做到自动绑定,是因为函数本身实现了 Descriptor 协议,另见 Python 颠覆认知的冷知识 #3 - Descriptor ↗
把 self 塞回去#
既然少了 __get__,那就补上:
class doubleit:
def __init__(self, fn):
self.fn = fn
def __call__(self, *args, **kwargs):
return self.fn(*args, **kwargs) + ', ' + self.fn(*args, **kwargs)
def __get__(self, obj, cls=None):
if obj is None:
return self
return lambda *args, **kwargs: self(obj, *args, **kwargs)
class MyClass:
@doubleit
def foo(self):
return 'foo'
def bar(self):
return 'bar'
bar = doubleit(bar)
obj = MyClass()
print(obj.foo())
print(obj.bar())
print(MyClass.foo(obj))python这次终于正常了:
foo, foo
bar, bar
foo, footext注意到此处 foo 和 bar 的装饰方式是等价的。
当访问 obj.foo 时,Python 会找到类上的那个 doubleit 实例,并调用它的 __get__(obj, MyClass)。这里返回的 lambda 记住了 obj,之后调用它,就会把 obj 补在参数最前面,再交给装饰器处理。
而通过类访问 MyClass.foo 时,__get__ 收到的 obj 是 None,于是返回装饰器实例本身。此时不会自动补参数,所以要写 MyClass.foo(obj)。
Key Takeaway#
__call__ 决定一个实例能不能像函数一样调用;__get__ 决定它放在类上、被访问时返回的东西。
下次再看到一个对象既实现了 __call__,又实现了 __get__,就知道它在忙什么了:一个负责“怎么调用”,一个负责“给谁调用”。