CoderXL's Blog

Back

随笔 →

你以为你在调用谁?#

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
6
text

明明把 __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
9
text

实例上的假 __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")
python
I can call myself.
False
TypeError: obj is not callable
text

有一个能调用的 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, foo
text

注意到此处 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__,就知道它在忙什么了:一个负责“怎么调用”,一个负责“给谁调用”。

Comment seems to stuck. Try to refresh?✨