大家好,我是正在实战各种 AI 项目的程序员晚枫。
函数调用时 Python 内部发生了什么?栈帧是如何创建、执行、销毁的?这一讲,揭开函数调用的底层全过程。
📖 开篇:调用栈不只是调用栈 当 Python 执行一个程序时,会维护一个「调用栈」:
1 2 3 4 5 6 7 8 9 10 def a (): return b() + 1 def b (): return c() * 2 def c (): return 42 print (a())
调用时:
1 2 3 4 5 6 a() -> 创建 frame_a -> b() -> 创建 frame_b -> c() -> 创建 frame_c -> return 42 frame_c 销毁 frame_b return 86 frame_a return 85
每一层函数调用,都有一个对应的栈帧(Frame)。
📚 栈帧结构详解 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 typedef struct _frame { PyObject_VAR_HEAD struct _frame *f_back ; PyCodeObject *f_code; PyObject *f_builtins; PyObject *f_globals; PyObject *f_locals; PyObject **f_valuestack; PyObject **f_stacktop; PyObject *f_localsplus[1 ]; int f_lasti; int f_lineno; PyObject *exc_type, *exc_value, *exc_traceback; } PyFrameObject;
栈帧的内存布局 1 2 3 4 5 6 7 8 9 10 11 12 13 栈帧内存: +------------------+ | PyObject_HEAD | <- 标准对象头 +------------------+ | f_back | <- 上一个栈帧指针 | f_code | <- 代码对象引用 | f_globals | <- 全局命名空间 | f_locals | <- 局部命名空间 +------------------+ | f_localsplus | <- 局部变量槽 + 闭包单元格 +------------------+ | 值栈区域 | <- 操作数栈 +------------------+
🔄 调用过程详解 第1步:创建栈帧 1 2 3 4 5 6 7 8 9 def foo (): x = 1 return x
第2步:执行字节码 1 2 3 4 5 6 7 import disdef bar (): result = 1 + 2 return result dis.dis(bar)
执行过程就是:从 f_lasti 读取指令 -> 执行 -> 更新 f_lasti -> 重复,直到遇到 RETURN_VALUE。
第3步:值栈操作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 执行 result = 1 + 2: 初始状态: f_stacktop = f_valuestack [] LOAD_CONST 1: [1] LOAD_CONST 2: [1, 2] BINARY_ADD:弹出 2,弹出 1,push 3 [3] STORE_FAST result:弹出 3,存入局部变量槽 0 [] (栈空了,但局部变量里有 result=3) RETURN_VALUE 3: 把 3 返回给调用者,销毁栈帧
🔗 栈帧链 栈帧之间通过 f_back 指针形成链表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import inspectdef outer (): middle() def middle (): inner() def inner (): frame = inspect.currentframe() f = frame depth = 0 while f is not None : print (f" depth={depth} : {f.f_code.co_name} () at line {f.f_lineno} " ) f = f.f_back depth += 1 outer()
输出:
1 2 3 4 depth=0: inner() at line ... depth=1: middle() at line ... depth=2: outer() at line ... depth=3: <module> at line ...
这就是调用栈的完整链!Python 用它来做异常回溯和调试。
🧠 闭包与单元格(Cell) 1 2 3 4 5 6 7 8 9 10 def outer (): x = 10 def inner (): return x * 2 return inner f = outer() print (f()) print (f.__closure__) print (f.__closure__[0 ].cell_contents)
闭包变量存储在单元格(Cell)中:
外层函数的局部变量 x 存在 cell 中 内层函数通过 freevars 访问 cell ⚠️ 栈帧与垃圾回收 1 2 3 4 5 6 7 8 9 def create_cycle (): class Node : def __init__ (self ): self.next = None n = Node() return n
理解 :Python 的垃圾回收器处理循环引用,但栈帧持有对局部对象的引用,所以在函数内部不要创建大量对象而不引用。
💡 本节作业 用 inspect 打印一个嵌套函数调用的完整调用栈 验证闭包的 closure 属性:什么情况下 closure 为 None? 思考:为什么递归深度太大会导致栈溢出? 🎯 本讲总结 栈帧结构 :链式结构(f_back),包含命名空间、执行状态、值栈。
调用过程 :创建栈帧 -> 分配局部变量槽 -> 执行字节码 -> 值栈操作 -> RETURN_VALUE。
栈帧链 :通过 f_back 指针形成链表,用于异常回溯和调试。
闭包实现 :freevars 和 cell,通过 f_localsplus 数组实现。
📚 推荐教材 《Python 编程从入门到实践(第 3 版)》 | 《流畅的 Python(第 2 版)》 | 《CPython 设计与实现》
🔗 课程导航 ← 上一讲:函数与类实现 | 下一讲:GIL 全局解释器锁 →
💬 联系我 主营业务 :AI 编程培训、企业内训、技术咨询
🎓 AI 编程实战课程 想系统学习 AI 编程?程序员晚枫的 AI 编程实战课 帮你从零上手!