在之前我们聊过Python内存的管理,今天我们就来看看调用栈,连接Python对象与代码执行的关键环境。我们已经了解了Python如何在内存中分配和管理对象(包括PyObject、Arena以及分配池等机制)。今天,我们先搞懂一个前提:当这些对象通过代码执行真正“运行起来”时,底层的内存区域是如何配合的?对于从C/C++转Python的人来说,最容易踩的坑就是「混淆两个栈的概念」。Python内部实际上存在两个用途完全不同的“栈”,如果搞混,后续所有理解都会跑偏。简单说:操作系统有自己的“系统栈”,用于管理进程的函数调用、局部变量;但Python并没有直接使用这个系统栈,而是自己实现了一套独立的栈机制,用于管理自身的代码执行。从操作系统的角度看,一个进程的内存会被划分为4个独立的区域,每个区域有明确的用途、固定的特性,我们逐一拆解,重点看和“栈”相关的部分。
首先是代码段,存储可执行指令,也就是我们写代码编译后的机器码,它在运行时不可修改,固定大小,编译后就确定了大小,不会随运行时动态变化;可共享,多个进程如果运行同一个程序,代码段可共享,节省内存。其次是数据段,用于存放程序的全局变量和静态变量。从程序启动到程序技术,这些变量一直存在与内存中,不会被轻易释放。第三就是堆段,用于动态内存分配,存放程序运行时动态创建的对象。最后是系统站,存储函数调用相关的信息,包括函数调用的上下文;函数的局部变量、参数;函数的返回地址。理解Python调用栈的另一个关键,是吃透「代码即对象」——这是Python作为解释型语言的灵魂,也是它和C/C++最大的区别之一。我们前面说过:当你运行一个.py文件时,Python解释器的机器码被加载到「代码段」,而你写的Python代码,只是解释器处理的“数据”。这句话的本质,就是「代码即对象」。每个函数、每个模块都是一个对象,每段可执行代码,都会被解析为抽象语法树,并在运行时被解释器执行。
我们可以用Python内置的ast模块,直接查看一个函数被解析后的AST结构——代码很简单,复制就能运行
import astimport inspect# 定义一个简单的函数source_code = """def func(a: int, b: int) -> int: "This function adds two numbers" return a + b"""print("Function source code:")print(source_code)print("="*80)print("Equivalent AST Syntax:")# 解析代码并打印格式化的AST结构print(ast.dump(ast.parse(source_code), indent=4))
运行后,我们会看到一个复杂的嵌套结构——这就是函数func的AST表示。解释器执行函数时,就是按照这个AST结构,一步步解析、执行每一条指令。我们还可以直接手动构造AST结构,然后通过解释器编译、执行,在运行时“动态创建”一个函数——这进一步证明了“代码即对象”,代码可以被当作数据来操作。
import astfrom ast import *# 手动构造AST结构,对应上面的func函数module_ast = Module( body=[ FunctionDef( name='func', args=arguments( posonlyargs=[], args=[ arg( arg='a', annotation=Name(id='int', ctx=Load()) ), arg( arg='b', annotation=Name(id='int', ctx=Load()) ) ], kwonlyargs=[], kw_defaults=[], defaults=[] ), body=[ Expr( value=Constant(value='This function adds two numbers') ), Return( value=BinOp( left=Name(id='a', ctx=Load()), op=Add(), right=Name(id='b', ctx=Load()) ) ) ], decorator_list=[], returns=Name(id='int', ctx=Load()))], type_ignores=[])# 修复AST的位置信息(必须步骤)ast.fix_missing_locations(module_ast)# 编译AST为可执行的代码对象code_obj = compile(module_ast, '<runtime>', 'exec')# 创建一个空的命名空间,用于存储动态创建的函数namespace = {}# 执行代码对象,将函数存入命名空间exec(code_obj, namespace)# 从命名空间中获取动态创建的函数dynamic_func = namespace['func']# 测试函数是否正常工作print(f"{dynamic_func(1, 3)=}") # 输出:dynamic_func(1, 3)=4print(f"{id(dynamic_func)=:x}") # 输出函数的内存地址(证明是一个真实的对象)
运行后会发现:我们手动构造的AST,成功生成了一个可以正常调用的函数dynamic_func——它和我们直接用def定义的函数,没有任何区别。
这就是Python的灵活性:代码可以被解析、被修改、被动态生成,而这一切的基础,就是「代码即对象」。