Skip to main content

Chapter 12: Function Object Internals

函数在 Python 里既是可调用对象,也是运行时代码、默认参数、闭包状态、注解和调试元数据的聚合点。本章要建立的判断动作是:看到一个函数定义、函数调用、闭包、方法调用或装饰器包装函数时,能够追踪它背后的 function object、code object、cell object、bound method 和 frame 的关系。

贯穿本章的代码现象是一段会生成闭包的函数工厂。它覆盖默认参数、关键字专用默认值、注解、自定义属性、闭包捕获和后续调用路径。读函数内部结构时,先把 def 当作一次运行时动作:执行 def 会创建 function object;调用 function object 才会按它持有的 code object 创建执行状态。

from collections.abc import Callable

def make_counter(start: int, *, label: str = "count") -> Callable[[int], tuple[str, int]]:
total = start

def step(delta: int = 1) -> tuple[str, int]:
nonlocal total
total += delta
return label, total

step.kind = "counter"
return step

counter_a = make_counter(10, label="score")
counter_b = make_counter(10, label="score")

本文以 CPython 为主解释实现形状。语言层面的对象属性以 Python 3.14 data model 中 user-defined functions 为准;调用协议以 Python 3.14 C API 的 Call Protocol 为实现观察入口;CPython 字段布局只作为阅读源码时的结构线索,例如 CPython funcobject.h

12.1 Function object and code object

Function object 是 def 执行后产生的 Python 对象,它把一段编译后的函数体和运行时环境绑定在一起。Code object 是函数体的编译结果,保存参数形状、局部变量名、常量、字节码、源码位置信息和闭包变量名。二者分工清晰:code object 描述“要执行什么代码”,function object 描述“这段代码在什么全局命名空间、默认参数、闭包和元数据下执行”。

在贯穿示例里,模块顶层执行到 def make_counter... 时,会创建一个名为 make_counter 的 function object。它的 __code__ 指向外层函数体的 code object,__globals__ 指向模块全局字典,__defaults__ 保存普通位置参数默认值,__kwdefaults__ 保存关键字专用参数默认值,__annotations__ 保存或触发注解相关信息。执行 make_counter(10, label="score") 时,外层函数体内部再次执行 def step...,因此每次调用都会创建一个新的 step function object。

同一段源码中的嵌套 def step 对应一个编译出来的 code object,运行时可以产生多个 function object。counter_acounter_b 都来自同一个 step 函数体,二者各自持有独立闭包状态。这个差异解释了闭包函数的常见现象:两个闭包对象打印出的 __code__ 可以相同,调用结果仍然可以互相独立,因为它们持有的 cell object 组不同。

print(counter_a.__code__ is counter_b.__code__) # True
print(counter_a.__closure__ is counter_b.__closure__) # False
print(counter_a(5)) # ('score', 15)
print(counter_b(1)) # ('score', 11)

这段代码要证明的对象关系是:函数身份由 function object 决定,函数体结构由 code object 决定,闭包状态由 function object 持有的 cell tuple 决定。counter_a.__code__ is counter_b.__code__ 为真,只能说明二者执行同一份编译函数体;counter_a.__closure__ is counter_b.__closure__ 为假,说明二者的外部绑定保存位置分离。

下面的图只表达本章的对象关系边界:从源码到 def 执行,再到函数调用,每一步产生或使用的对象不同。

阅读 CPython 源码时,可以把 PyFunctionObject 看作这个关系的字段化版本。它通常包含 func_globalsfunc_builtinsfunc_namefunc_qualnamefunc_codefunc_defaultsfunc_kwdefaultsfunc_closurefunc_annotationsvectorcall 等字段。源码字段名帮助定位实现,但正文中的结论仍按 Python 对象模型表达:function object 是运行时容器,code object 是编译产物,frame 是一次执行产生的状态载体。

12.2 Defaults, kwdefaults, annotations, and metadata

函数静态信息影响两类行为:调用时如何补齐参数,以及工具如何检查函数。__defaults__ 保存普通参数默认值,__kwdefaults__ 保存关键字专用参数默认值,__annotations__ 描述参数和返回值标注,__dict__ 保存用户给函数附加的自定义元数据。它们都挂在 function object 上,因此同一个 function object 的后续调用会复用这些信息。

在贯穿示例中,外层 make_counterlabel 是关键字专用参数,所以它的默认值进入 make_counter.__kwdefaults__。内层 stepdelta 是普通参数,所以它的默认值进入 step.__defaults__step.kind = "counter" 写入的是 step.__dict__,这类元数据供框架、装饰器、路由注册和检查工具读取,解释器执行函数体时通常无需读取它。

print(make_counter.__kwdefaults__) # {'label': 'count'}
print(counter_a.__defaults__) # (1,)
print(counter_a.__annotations__) # {'delta': <class 'int'>, 'return': tuple[str, int]}
print(counter_a.__dict__) # {'kind': 'counter'}

这段代码要证明默认参数和元数据的挂载位置。label 在外层函数对象上,delta 在返回出来的内层函数对象上。调用 counter_a() 时,参数绑定逻辑会读取 counter_a.__defaults__ 为缺失的 delta 补值;它不会回到 make_counter.__kwdefaults__ 查找 label,因为 label 已经在创建 step 时通过 cell 进入闭包。

默认参数保存的是对象引用。默认值在 def 执行时计算一次,随后挂到 function object。这个规则能解释可变默认参数的工程风险:如果默认值是 list、dict 或 set,多次调用会共享同一个默认对象。更稳定的写法是使用 None 作为缺省哨兵,在函数体内部创建新的容器。

def collect(item, bucket=[]):
bucket.append(item)
return bucket

print(collect("a")) # ['a']
print(collect("b")) # ['a', 'b']

这个示例的结论是:bucket 的默认 list 存在 collect.__defaults__ 里,多次调用复用同一对象。判断默认参数问题时,先看参数是否缺省,再看缺省值对象是否可变,再看函数体是否原地修改该对象。

注解在 Python 3.14 有版本边界。官方 data model 已标注函数注解改为惰性求值,并新增 __annotate__ 相关属性。对工程代码的判断方式是:注解服务检查、文档、运行时框架和类型工具;调用协议本身通常按实参对象运行,注解不自动强制参数类型。框架可以主动读取注解并执行转换或校验,这属于框架行为。

12.3 Closure and cell object

Closure 是 function object 对外层绑定的持有关系。Python 编译器在符号分析阶段发现内层函数读取或写入外层局部变量时,会把相关名字放入 code object 的 free variable 信息;运行时创建内层 function object 时,会把对应外层绑定装入 cell object,再把 cell tuple 挂到 __closure__

在贯穿示例中,step 读取 label,也通过 nonlocal total 修改 total。因此 counter_a.__closure__ 中至少会有对应 labeltotal 的 cell。cell 保存的是绑定槽位,cell_contents 返回当前槽位里的对象。调用 counter_a(5) 后,total 这个 cell 的内容会从 10 更新为 15counter_b 持有另一组 cell,所以它的 total 仍按自己的调用历史变化。

for name, cell in zip(counter_a.__code__.co_freevars, counter_a.__closure__):
print(name, cell.cell_contents)

print(counter_a(5))

for name, cell in zip(counter_a.__code__.co_freevars, counter_a.__closure__):
print(name, cell.cell_contents)

这段代码要证明闭包保存的是外层绑定的运行时位置。co_freevars 给出名字顺序,__closure__ 给出 cell tuple,二者按顺序对应。阅读闭包时,先看内层函数的 __code__.co_freevars,再看 __closure__ 中每个 cell 的当前内容,最后根据函数体是否使用 nonlocal 判断后续调用会替换 cell 内容还是只读取它。

闭包常见误判来自把变量名和对象混在一起。label 这类字符串对象本身没有被复制出一套闭包规则;闭包保存的是名字绑定所需的 cell。total += delta 对整数会产生新整数对象,再把 cell 更新为新对象;如果 cell 内容是 list,函数体调用 append 会修改同一个 list 对象。闭包分析要同时区分 cell 位置、cell 中当前对象、对象自身可变性。

闭包也解释装饰器和函数工厂的状态来源。装饰器返回 wrapper function 时,wrapper 通常通过 closure 保存原函数对象、配置参数或缓存容器。调试 wrapper 时,检查顺序是:先看外层装饰器返回了哪个 function object,再看 wrapper 的 __closure__ 保存了哪些对象,再看 wrapper 的 __dict__functools.wraps 是否补充了元数据。

12.4 Callable protocol, vectorcall, and bound method

Callable protocol 回答的问题是:表达式 target(...) 如何找到并执行一个可调用对象。语言层面上,只要对象类型支持调用行为,callable(obj) 就可能为真;用户定义函数、内置函数、类、实现 __call__ 的实例、bound method 都属于可调用对象。CPython 实现层面存在 tp_call 和 vectorcall 两套调用入口,vectorcall 用数组形态传递参数,减少临时 tuple 和 dict 的构造开销。

vectorcall 是 CPython 调用优化协议,语义目标仍然是执行同一次 Python 调用。它影响内部传参布局和扩展类型的实现方式,通常不改变 Python 代码层面的调用结果。Python 3.14 C API 文档说明 CPython 支持 tp_call 与 vectorcall,并说明支持 vectorcall 的类仍需要保持和 tp_call 相同的语义。阅读性能路径时,可以把 vectorcall 当作“参数如何进入 callee”的实现细节,把参数绑定、默认值补齐和函数体执行当作语义路径。

函数作为类属性时,还会参与 descriptor 绑定。用户定义函数对象实现 descriptor 行为:通过实例读取类上的函数属性时,会生成 bound method。Bound method 保存两个关键引用:__func__ 指向原始 function object,__self__ 指向实例。调用 bound method 时,实例会作为第一个实参传给原始函数。

class Meter:
def measure(self, delta: int = 1) -> int:
return delta

meter = Meter()
method = meter.measure

print(method.__func__ is Meter.measure) # True
print(method.__self__ is meter) # True
print(method(3)) # 等价于 Meter.measure(meter, 3)

这段代码要证明方法调用的中间对象。meter.measure 读取类属性 Meter.measure 时触发函数 descriptor,得到的 method 保存实例和原函数。method(3) 的调用对象是 bound method,bound method 再调用 method.__func__,并把 method.__self__ 放到实参列表前面。这个路径解释了为什么实例方法定义时要写 self,调用时通常无需手动传入实例。

Wrapper function 是另一类可调用路径。装饰器返回 wrapper 后,名字会重新绑定到 wrapper function object。此时调用名字进入 wrapper 的 __call__ 路径,wrapper 再在函数体内调用原函数。判断 wrapper 是否改变语义,要看三个位置:wrapper 的参数签名是否完整接收调用方输入,wrapper 如何调用原函数,wrapper 是否保留原函数的元数据。functools.wraps 主要处理 __name____qualname____doc____annotations____wrapped__ 等可检查信息,函数体的真实执行路径仍由 wrapper 代码决定。

12.5 Function and frame relationship

Frame 是一次函数执行的状态对象。Function object 保存可复用的执行配置,frame 保存某一次调用正在推进到哪里。一次普通 Python 函数调用会根据 function object 持有的 code object、globals、builtins、closure 和实参创建执行状态,然后进入解释器执行函数体。Frame 里可观察的关键属性包括 f_codef_localsf_globalsf_builtinsf_lastif_back

把贯穿示例放回调用路径,counter_a(5) 的执行顺序是:先确认 counter_a 可调用;再把实参 5 绑定到 delta,缺失参数按 __defaults__ 补齐;再使用 counter_a.__code__ 创建执行状态;再把闭包 cell 暴露给函数体中的 free variable 访问;最后执行 nonlocal total 对应的 cell 更新并返回结果。Frame 承担的是这次调用的局部变量、指令位置和调用栈连接。

import inspect

def probe(value: int = 1) -> tuple[str, tuple[str, ...], int]:
frame = inspect.currentframe()
return frame.f_code.co_name, tuple(frame.f_locals), frame.f_lasti

print(probe())

这段代码要证明 frame 是执行中对象。probe.__code__ 是可复用的编译结果,inspect.currentframe() 返回的是当前这次调用的 frame。f_code.co_name 指回正在执行的 code object 名称,f_locals 展示本次调用的局部名字,f_lasti 展示当前执行位置。Python 3.13 起,优化作用域的 f_locals 行为有代理化边界;因此调试工具读取 frame locals 时应按版本文档判断可写入行为。

函数调用与 frame 的关系还要区分普通函数、生成器函数和 coroutine function。普通函数调用会执行函数体直到返回或抛异常。生成器函数调用先返回 generator object,函数体在迭代推进时执行。Coroutine function 调用先返回 coroutine object,函数体在 await 驱动下执行。它们都关联 code object 和执行状态,但调用表达式是否立即进入函数体,需要按函数类别判断。

可复用检查顺序可以固定为五步。第一步,看当前名字绑定到哪个 object。第二步,如果对象来自函数定义,检查 __code____globals____defaults____kwdefaults____annotations____dict__。第三步,如果涉及外层变量,检查 __code__.co_freevars__closure__。第四步,如果从实例读取函数,检查是否生成 bound method,并查看 __func____self__。第五步,如果已经发生调用,再把问题放入 frame:实参如何绑定、局部变量如何形成、指令执行到哪里、异常或返回如何离开当前 frame。

最小自检任务

阅读下面代码,判断 firstsecondmethod 分别持有哪些运行时对象关系,并说明调用 first() 两次后的状态变化。

def build(prefix: str, *, step: int = 1):
total = 0

def add(value: int = step) -> tuple[str, int]:
nonlocal total
total += value
return prefix, total

add.category = "adder"
return add

first = build("A", step=2)
second = build("A", step=2)

class Holder:
run = first

holder = Holder()
method = holder.run

答案要点

firstsecond 是两次执行 build 后返回的两个 function object。它们共享同一个 add 函数体对应的 code object,因此 first.__code__ is second.__code__ 可以成立;它们持有不同的 __closure__,所以 total 的运行时状态彼此独立。

first.__defaults__ 保存 value 的默认值 (2,),这个值来自执行 build("A", step=2) 时内层 def add 的默认参数求值。first.__dict__ 中保存 category 元数据。prefixtotal 通过 first.__closure__ 中的 cell object 进入 add 的后续调用路径。

连续调用 first() 两次会复用 first 持有的 total cell。第一次调用把 total0 更新为 2,第二次调用把同一个 cell 更新为 4second() 的结果仍从自己的 total cell 开始计算。

method = holder.run 会从实例读取类属性上的 function object,并生成 bound method。method.__func__ is first 成立,method.__self__ is holder 成立。调用 method() 时,bound method 会把 holder 作为第一个实参传给 first;由于 first 的函数签名没有接收这个实例参数,直接调用 method() 会触发参数绑定错误。这个结论来自 bound method 的自动插入实例规则。

本章知识点总结

  • 函数对象def 执行会创建 function object,function object 保存执行配置和元数据。
  • 代码对象:code object 是编译后的函数体,保存参数形状、局部名字、常量、字节码和位置信息。
  • 对象分工:function object 决定运行时环境,code object 决定可执行结构,frame 决定一次调用状态。
  • 默认参数__defaults__ 保存普通参数默认值,默认对象在 def 执行时确定并挂到 function object。
  • 关键字默认值__kwdefaults__ 保存 keyword-only 参数默认值,调用绑定缺失关键字参数时会读取它。
  • 函数注解__annotations__ 服务检查、文档和框架读取,Python 3.14 起注解具有惰性求值边界。
  • 函数元数据__dict__ 保存用户附加属性,框架可以读取它,函数体执行路径通常由 code object 决定。
  • 闭包关系:closure 通过 cell object 保存外层绑定,co_freevars__closure__ 按顺序对应。
  • 闭包状态:多个闭包 function object 可以共享 code object,同时持有不同 cell tuple。
  • 调用协议:CPython 的 tp_call 和 vectorcall 是调用入口实现,Python 调用语义仍以参数绑定和函数体执行为核心。
  • 绑定方法:bound method 保存 __func____self__,调用时把实例插入原函数实参列表前端。
  • 包装函数:装饰器返回 wrapper 后,名字绑定到新的 function object,真实执行路径由 wrapper 函数体决定。
  • 执行帧:frame 保存一次调用的 code、locals、globals、builtins、指令位置和调用栈连接。
  • 检查顺序:先看名字绑定,再看函数对象字段,再看闭包 cell,再看 bound method,最后回到 frame 执行状态。