在上篇内容我们聊了Python与系统栈的区别、程序的内存布局,以及“代码即对象”的核心逻辑,铺垫好了理解Python调用栈的基础。今天我们来看看Python调用栈的基本单元「PyFrame对象」、分析Python栈的结构(链表而非数组)
Python没有直接使用操作系统的系统栈,而是自己实现了一套独立的栈机制。这套机制的核心,就是“PyFrame对象”——简称“帧对象”。每个PyFrame对象,都会完整记录一次函数调用的所有信息,包括:当前执行位置、函数参数、局部变量、全局变量、以及调用它的上一个帧(父帧)——这也是Python能实现“回溯调用栈”“调试代码”的底层基础。
我们用Python内置的inspect模块,直接查看函数调用时的栈帧信息——代码很直观,复制就能运行,看看PyFrame的结构。
import inspectdef recursive_function(n, depth=0): """用递归展示栈帧(递归调用会创建多个帧对象)""" # 获取当前函数调用的帧对象 current_frame = inspect.currentframe() # 打印当前帧的ID、父帧的ID、递归深度 print(f"Depth: {depth} Frame ID: {id(current_frame)}, Previous Frame ID: {id(current_frame.f_back)}") if n > 0: # 递归调用,创建新的帧对象 recursive_function(n-1, depth=depth+1) else: # 递归终止,展示帧对象的详细信息 demonstrate_frames(depth) # 函数返回前,打印当前帧的父帧和局部变量 print(f"Depth {depth}: Returning from Frame ID: {id(current_frame.f_back)} , Local n: {n}")def demonstrate_frames(depth): """展示帧对象的详细属性""" print("="*80) frame = inspect.currentframe() print(f"Frame details:") print(f"Current Line number: {frame.f_lineno}") # 当前执行的行号 print(f"Filename: {frame.f_code.co_filename}") # 所在文件名 print(f"Function name: {frame.f_code.co_name}") # 所在函数名 print(f"Code object:\t{frame.f_code}") # 对应的代码对象 print(f"Local variables: {list(frame.f_locals)}") # 局部变量列表 print(f"Depth variable: {frame.f_locals['depth']}") # 局部变量depth的值 print(f"Global variables: {list(frame.f_globals)}") # 全局变量列表 print("="*80)# 调用递归函数,触发多个帧对象的创建recursive_function(2)
运行后我们会看到输出的帧ID不同,但核心结构一致,从输出中,我们能看到3个关键信息:每次递归调用都会创建一个新的PyFrame对象;每个PyFrame对象通过f_back属性指向父帧,形成链表结构;帧对象记录了完整的执行上下文。
接下来我们观察一下帧对象的分配和复用,我们用gc模块和sys模块跟踪帧对象的内存地址,看看它是否会被复用
import gcimport sys# 用于存储所有帧对象的ID(内存地址)all_frame_ids = set()def track_frame_allocation(): """跟踪帧对象的分配与复用""" def create_frames(): """创建多层函数调用,生成多个帧对象""" def level_3(): # 获取当前帧对象 frame = sys._getframe() print(f" level 3: frame at 0x{id(frame):x}, function: {frame.f_code.co_name}") all_frame_ids.add(id(frame)) # 记录帧ID return 3 def level_2(): frame = sys._getframe() print(f" level 2: frame at 0x{id(frame):x}, function: {frame.f_code.co_name}") all_frame_ids.add(id(frame)) return 2 + level_3() # 调用level_3,创建新帧 def level_1(): frame = sys._getframe() print(f" level 1: frame at 0x{id(frame):x}, function: {frame.f_code.co_name}") all_frame_ids.add(id(frame)) return 1 + level_2() # 调用level_2,创建新帧 level_1() # 启动多层调用 print("=== FRAME ALLOCATION PATTERNS ===") # 运行3次,观察帧地址是否复用 for i in range(3): print(f"\nRun {i + 1}:") create_frames() # 手动触发垃圾回收,释放无用的帧对象 gc.collect() # 打印所有唯一的帧地址数量 print(f"\nUnique frame addresses across all runs: {len(all_frame_ids)}")# 执行跟踪函数track_frame_allocation()
输出信息:
=== FRAME ALLOCATION PATTERNS ===Run 1: level 1: frame at 0x7fb2b1ff8110, function: level_1 level 2: frame at 0x7fb2b1ff9be0, function: level_2 level 3: frame at 0x7fb2b1f34b80, function: level_3Run 2: level 1: frame at 0x7fb2b1ff9a40, function: level_1 level 2: frame at 0x7fb2b1ff8110, function: level_2 level 3: frame at 0x7fb2b1f34a00, function: level_3Run 3: level 1: frame at 0x7fb2b1ff9a40, function: level_1 level 2: frame at 0x7fb2b1ff8110, function: level_2 level 3: frame at 0x7fb2b1f34a00, function: level_3Unique frame addresses across all runs: 5
从输出中看到一个 规律,上一轮函数调用过的帧地址,在Run2中被level_2调用复用了,Run2的level_1帧地址,在Run3中被复用了。
今天就先到这里了,下次我们来看看生成器与协程