大家好,我是正在实战各种 AI 项目的程序员晚枫。
函数调用时 Python 内部发生了什么?栈帧是如何创建、执行、销毁的?这一讲,揭开函数调用的底层全过程。
📖 开篇:调用栈不只是调用栈
当 Python 执行一个程序时,会维护一个「调用栈」:
1 | def a(): |
调用时:
1 | a() -> 创建 frame_a -> |
每一层函数调用,都有一个对应的栈帧(Frame)。
📚 栈帧结构详解
1 | // Include/frameobject.h |
栈帧的内存布局
1 | 栈帧内存: |
🔄 调用过程详解
第1步:创建栈帧
1 | def foo(): |
第2步:执行字节码
1 | import dis |
执行过程就是:从 f_lasti 读取指令 -> 执行 -> 更新 f_lasti -> 重复,直到遇到 RETURN_VALUE。
第3步:值栈操作
1 | 执行 result = 1 + 2: |
🔗 栈帧链
栈帧之间通过 f_back 指针形成链表:
1 | import inspect |
输出:
1 | depth=0: inner() at line ... |
这就是调用栈的完整链!Python 用它来做异常回溯和调试。
🧠 闭包与单元格(Cell)
1 | def outer(): |
闭包变量存储在单元格(Cell)中:
- 外层函数的局部变量 x 存在 cell 中
- 内层函数通过 freevars 访问 cell
⚠️ 栈帧与垃圾回收
1 | # 循环引用会导致栈帧持有引用,延迟回收 |
理解:Python 的垃圾回收器处理循环引用,但栈帧持有对局部对象的引用,所以在函数内部不要创建大量对象而不引用。
💡 本节作业
- 用 inspect 打印一个嵌套函数调用的完整调用栈
- 验证闭包的 closure 属性:什么情况下 closure 为 None?
- 思考:为什么递归深度太大会导致栈溢出?
🎯 本讲总结
栈帧结构:链式结构(f_back),包含命名空间、执行状态、值栈。
调用过程:创建栈帧 -> 分配局部变量槽 -> 执行字节码 -> 值栈操作 -> RETURN_VALUE。
栈帧链:通过 f_back 指针形成链表,用于异常回溯和调试。
闭包实现:freevars 和 cell,通过 f_localsplus 数组实现。
📚 推荐教材
《Python 编程从入门到实践(第 3 版)》 | 《流畅的 Python(第 2 版)》 | 《CPython 设计与实现》
🔗 课程导航
← 上一讲:函数与类实现 | 下一讲:GIL 全局解释器锁 →
💬 联系我
| 平台 | 账号/链接 |
|---|---|
| 微信 | 扫码加好友 |
| B 站 | Python 自动化办公社区 |
主营业务:AI 编程培训、企业内训、技术咨询