大家好,我是正在实战各种 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()) # (42 * 2) + 1 = 85

调用时:

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
// Include/frameobject.h
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

# 调用 foo() 时,Python:
# 1. 分配 PyFrameObject 内存
# 2. 初始化 f_code, f_globals, f_locals 等
# 3. 分配 f_localsplus 数组(大小由 co_nlocals 决定)
# 4. f_stacktop 指向 f_valuestack

第2步:执行字节码

1
2
3
4
5
6
7
import dis

def bar():
result = 1 + 2 # LOAD_FAST 1 -> LOAD_CONST 2 -> BINARY_ADD -> STORE_FAST
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 inspect

def 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()) # 20
print(f.__closure__) # (<cell at ...>,)
print(f.__closure__[0].cell_contents) # 10

闭包变量存储在单元格(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

# 这个 Node 对象会被栈帧引用,直到函数返回
n = Node()
return n # 如果不返回,函数结束后栈帧销毁,对象才可能被回收

理解:Python 的垃圾回收器处理循环引用,但栈帧持有对局部对象的引用,所以在函数内部不要创建大量对象而不引用。


💡 本节作业

  1. 用 inspect 打印一个嵌套函数调用的完整调用栈
  2. 验证闭包的 closure 属性:什么情况下 closure 为 None?
  3. 思考:为什么递归深度太大会导致栈溢出?

🎯 本讲总结

栈帧结构:链式结构(f_back),包含命名空间、执行状态、值栈。

调用过程:创建栈帧 -> 分配局部变量槽 -> 执行字节码 -> 值栈操作 -> RETURN_VALUE。

栈帧链:通过 f_back 指针形成链表,用于异常回溯和调试。

闭包实现:freevars 和 cell,通过 f_localsplus 数组实现。


📚 推荐教材

《Python 编程从入门到实践(第 3 版)》 | 《流畅的 Python(第 2 版)》 | 《CPython 设计与实现》


🔗 课程导航

上一讲:函数与类实现 | 下一讲:GIL 全局解释器锁


💬 联系我

平台账号/链接
微信扫码加好友
B 站Python 自动化办公社区

主营业务:AI 编程培训、企业内训、技术咨询

🎓 AI 编程实战课程

想系统学习 AI 编程?程序员晚枫的 AI 编程实战课 帮你从零上手!