当前位置:首页>python>看看Python的内存管理(六):PyFrame栈帧

看看Python的内存管理(六):PyFrame栈帧

  • 2026-09-02 20:20:19
看看Python的内存管理(六):PyFrame栈帧

在上篇内容我们聊了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中被复用了。

今天就先到这里了,下次我们来看看生成器与协程

最新文章

随机文章