本文目录
在 Python 里,后缀 () 不代表“只能是函数”。任何实现了调用协议的对象都可以写在表达式后面加括号——callable(x) 为真时,解释器会走 x(...) 这套机制。函数是最常见的可调用体,但 bound method、类本身、以及带 __call__ 的实例,日常代码里同样频繁出现。把几类可调用对象列清楚,读 sorted(..., key=...)、装饰器、以及 Cla() 创建实例时就不会各讲各的。
调用协议在 CPython 里统一落到 type(obj).__call__(obj, *args, **kwargs)。内置 函数 有自己的 __call__ 实现;类 的 __call__ 触发实例创建;你手写类的 __call__ 则让实例行为像函数。装饰器、回调注册表、框架钩子,本质都在传递“能加括号的对象”。
callable:能不能加括号
内置函数 callable(obj) 检查对象是否可调用(通常是有 __call__ 方法,或内置 callable 类型):
def f() -> None:
pass
print(callable(f)) # True
print(callable(len)) # True
print(callable(42)) # False
class C:
pass
print(callable(C)) # True —— 类可调用
print(callable(C())) # False —— 普通实例默认不可调用callable 是静态预判;真正调用时若不可调用,仍会 TypeError: 'int' object is not callable。
函数:最直接的 callable
def 和 lambda 产生 function 对象;调用时新建栈帧、绑定参数、执行体:
def add(a: int, b: int) -> int:
return a + b
print(add(2, 3)) # 5
print(callable(add)) # True
print(type(add).__name__) # function内置函数(如 len)也是 callable,C 实现,语义一样:obj(*args, **kwargs)。
方法:函数绑在实例或类上
类体里的 def method(self, ...) 产生函数;经实例访问时变成 bound method,self 已绑好:
class Greeter:
def hi(self, name: str) -> str:
return f"hi, {name}"
g = Greeter()
print(g.hi("Ada")) # hi, Ada
print(callable(g.hi)) # True
print(Greeter.hi(g, "Bob")) # hi, Bob —— 未绑定的函数式调用经类访问时(Greeter.hi)得到的是函数,需要手动传实例作第一个参数。方法仍是 函数 的一种包装,不是新类型魔法。
class Counter:
def __init__(self) -> None:
self.n = 0
def inc(self) -> None:
self.n += 1
c = Counter()
bound = c.inc
print(bound()) # n 变成 1
print(callable(bound)) # Truebound 是 bound method 对象;它记住了 c 和原始函数,所以 bound() 等价于 Counter.inc(c)。
类:调用即构造(上一篇的 Cla())
类对象本身 callable:Point(1, 2) 触发类的 __call__,内部走 __new__ / __init__ 造 实例:
class Box:
def __init__(self, item: str) -> None:
self.item = item
b = Box("note")
print(callable(Box)) # True
print(type(b).__name__) # Box装饰器写法 @some_class 有时也利用“类可调用”:调用类返回实例,等价于把实例注册进某个流程——本质仍是 some_class(...)。
数据类、命名元组等语法糖底层仍走 __new__ / __init__,只是帮你生成了模板代码。看到 @dataclass 时不要以为实例是“魔术变出来的”——只是 __init__ 等被自动写好了,创建顺序与普通 class 一致。
call:把实例变成“像函数一样”
在类里定义 __call__,实例 也可加括号:
class Multiplier:
def __init__(self, factor: int) -> None:
self.factor = factor
def __call__(self, x: int) -> int:
return x * self.factor
double = Multiplier(2)
print(callable(double)) # True
print(double(21)) # 42这种模式常见于:策略对象、闭包的可读替代、 functools.partial 的手写版、以及可复用的“函数对象”封装状态。__call__ 可以是任意签名,和普通函数一样支持 *args, **kwargs。
class Logger:
def __init__(self, prefix: str) -> None:
self.prefix = prefix
def __call__(self, message: str, *, level: str = "INFO") -> None:
print(f"[{self.prefix}][{level}] {message}")
log = Logger("app")
log("started")
log("warn", level="WARN")带状态的 callable 实例,和闭包 函数 能表达类似模式:闭包把自由变量藏在 cell 里,__call__ 把状态挂在 self 上。选哪种主要看可读性与是否需要多个入口方法;只有一个“像函数一样调用”的入口时,__call__ 往往更直观。
四类 callable 对照
| 对象 | 例子 | 括号里发生什么 |
|---|---|---|
| 函数 | def f / lambda | 执行函数体 |
| 方法 | obj.method | 绑定的 self + 执行 |
| 类 | MyClass(...) | 创建并初始化实例 |
带 __call__ 的实例 | obj(...) | 执行实例的 __call__ |
底层统一:解释器查找 type(obj).__call__,再分发到具体实现(函数.call、type.call 等)。
可以粗记一张 mental checklist:看到 () 先问“左边表达式的值是谁”,再查 callable;若是 函数 或 方法,直接执行;若是 类,走创建链;若是 实例,找 __call__。调试 TypeError: 'X' object is not callable 时,十有八九是忘了实例默认没有 __call__,或把类名和实例名搞混(该写 Factory() 却写了 Factory 没括号,反之亦然)。
作为参数传递:key=、回调、注册表
API 接受 callable 时,上述四类都能传,只要签名匹配:
def apply_twice(fn: callable[[int], int], n: int) -> int:
return fn(fn(n))
print(apply_twice(lambda x: x + 1, 3)) # 5
m = Multiplier(3)
print(apply_twice(m, 2)) # 18 —— m 作 callablesorted(items, key=fn) 的 key 也是 callable;传 lambda、普通函数、或带 __call__ 的对象均可。
functools.partial:预填参数的 callable
标准库常把 partial 当作“绑了一半参数的函数”传给 API:
from functools import partial
def power(base: int, exp: int) -> int:
return base ** exp
square = partial(power, exp=2)
print(callable(square)) # True
print(square(5)) # 25
print([square(x) for x in range(4)]) # [0, 1, 4, 9]partial 对象同样实现调用协议;类型检查里常标注为 Callable[..., T]。
类上的 @classmethod 也能当 callable 传给 API:第一个参数是类,适合“从多种输入构造实例”的入口,例如 Dict.fromkeys 那种风格。静态方法 @staticmethod 则不绑实例也不绑类,本质仍是挂在类命名空间里的 函数。
class Parser:
@classmethod
def from_json(cls, raw: str) -> "Parser":
return cls(raw.strip())
def __init__(self, text: str) -> None:
self.text = text
factory = Parser.from_json
print(callable(factory)) # True
p = factory('{"a":1}')
print(p.text) # {"a":1}调用失败 vs callable 为假
两类错误不要混为一谈:
def needs_two(a: int, b: int) -> int:
return a + b
print(callable(needs_two)) # True
# needs_two(1) # TypeError: 少参数
# 42() # TypeError: int 不可调用callable(x) 为真只说明有 __call__ 入口;参数不匹配、或内部逻辑抛异常,都在真正调用时才暴露。
标准库 operator 模块里很多名字(如 itemgetter、attrgetter)也是 callable 对象,不是 def 写出来的 函数;读第三方库时看到“可调用但 type 不是 function”不必惊讶——回到 __call__ 或 C 实现的内置 callable 即可解释。
常见误解
- “只有函数能加括号”:类、实例(有
__call__)、方法都可以。 - 把类实例当函数用却忘了写
__call__:C()可以,C()()不行,除非定义了__call__。 callable(x)为真但调用仍失败:例如参数个数不对,或__call__内部再抛错——callable只表示“有调用入口”。- 把“可调用”当成“是 function 类型”:
type(add)是 function,但 类、partial、带__call__的实例类型名各不相同,都可以用callable检测。 - 实例默认可调用吗:只有定义了
__call__的实例才可再套一层括号;obj()与Obj()是两层不同 callable。
从 callable 视角串起来:lambda 篇的 函数、类篇的 实例 与构造、创建篇的 Cla(),都是同一条“查找 __call__ 并执行”的路由表。下一批会进入元类、装饰器——它们都站在“对象可被调用或包装 callable”这层模型之上。
小结:谁在后面响应括号
| 表达式类型 | callable | 括号触发 |
|---|---|---|
| 函数 / lambda | 是 | 执行函数体 |
| bound method | 是 | 绑好的 self + 函数体 |
| class | 是 | __new__ / __init__ 创建 实例 |
| 普通 实例 | 否(默认) | — |
有 __call__ 的 实例 | 是 | 实例的 __call__ |
遇到设计问题“该传 函数 还是带 __call__ 的小对象”,优先看是否需要额外状态与多个方法:只有单一调用入口且状态明显属于对象本身时,__call__ 类往往更清晰;否则普通 def 或 lambda 更简单。
callable 也是内置 函数,可以对任意对象做预判,但别在热路径里滥用——直接调用试一次有时更直观。类型检查器用 typing.Callable[[ArgTypes], ReturnType] 描述“期望可调用”,与运行时 callable 不完全等价:某些 C 扩展对象可能可调用却难以静态标注。工程上二者结合:callable 做防御性入口检查,标注做文档与 mypy/pyright 校验。
装饰器(下一篇)会在 函数 或 class 定义时把名字替换成另一个 callable;现在你已经知道“谁可以加括号”,后面只需追问“替换前后各是谁在响应调用”。把 __call__、bound method、class 构造与裸 函数 放在同一张表里,读 @ 语法糖时就不会觉得跳出了对象模型。
再补一个边界:classmethod 与 staticmethod 也是 callable,经类或 实例 访问时行为不同——经 实例 访问 @classmethod 仍会收到 class 作为第一个参数。它们不是本篇重点,但在注册表、工厂 API 里常作为 callable 值传递,与 __call__ 实例并列出现在“回调候选”列表里。
一句话收束:函数 是最常见的 callable,但协议属于整个对象模型——会加括号的不只有 def。下一篇元类会再往上追一层:谁让 class 本身也能响应 Cla() 这次调用。
回顾与 sorted(..., key=...) 的衔接:key 接受任意 callable,返回值用于比较而非最终输出;因此 __call__ 实例、partial、lambda 都能胜任。选哪种写法,取决于你是否需要对象上额外的状态与方法,而不是 callable 协议本身有何不同。先把 callable(x) 与 x() 分开想,再读装饰器与元类会轻松很多。这是对象模型里承上启下的一节,值得对照代码多敲几次验证输出结果吧。