Python 虚拟机
x86 函数调用栈
py文件被编译为字节码指令序列之后,就通过 Python 虚拟机执行字节码。
python 代码到成为运行的进程,这之间经过了什么操作呢?
在 x86 的机器上可执行文件会维护函数调用栈来保证函数的有序执行,下图为出现函数调用时的栈帧情况。
caller 作为函数调用者,需要做的工作:
- 函数参数入栈
- return address 入栈,当前 PC + 4
callee 是被调用函数,需要做的工作:
- 保存上一栈帧的 ebp,用于在函数调用结束后恢复 caller 的栈帧
- 将 esp 寄存器值存到 ebp (将 ebp 理解为 esp 寄存器的快照)
- esp 值减少用于划定栈帧的大小
- 保存寄存器、局部变量的值
运行时环境
PyCodeObject
Python 源代码中的任何一个 Code Block 都会创建一个 CodeObject
/* Bytecode object */typedef struct {PyObject_HEADint co_argcount; /* #arguments, except *args */int co_posonlyargcount; /* #positional only arguments */int co_kwonlyargcount; /* #keyword only arguments */int co_nlocals; /* #local variables */int co_stacksize; /* #entries needed for evaluation stack */int co_flags; /* CO_..., see below */int co_firstlineno; /* first source line number */PyObject *co_code; /* instruction opcodes */PyObject *co_consts; /* list (constants used) */PyObject *co_names; /* list of strings (names used) */PyObject *co_varnames; /* tuple of strings (local variable names) */PyObject *co_freevars; /* tuple of strings (free variable names) */PyObject *co_cellvars; /* tuple of strings (cell variable names) *//* The rest aren't used in either hash or comparisons, except for co_name,used in both. This is done to preserve the name and line numberfor tracebacks and debuggers; otherwise, constant de-duplicationwould collapse identical functions/lambdas defined on different lines.*/Py_ssize_t *co_cell2arg; /* Maps cell vars which are arguments. */PyObject *co_filename; /* unicode (where it was loaded from) */PyObject *co_name; /* unicode (name, for reference) */PyObject *co_lnotab; /* string (encoding addr<->lineno mapping) SeeObjects/lnotab_notes.txt for details. */void *co_zombieframe; /* for optimization only (see frameobject.c) */PyObject *co_weakreflist; /* to support weakrefs to code objects *//* Scratch space for extra data relating to the code object.Type is a void* to keep the format private in codeobject.c to forcepeople to go through the proper APIs. */void *co_extra;/* Per opcodes just-in-time cache** To reduce cache size, we use indirect mapping from opcode index to* cache object:* cache = co_opcache[co_opcache_map[next_instr - first_instr] - 1]*/// co_opcache_map is indexed by (next_instr - first_instr).// * 0 means there is no cache for this opcode.// * n > 0 means there is cache in co_opcache[n-1].unsigned char *co_opcache_map;_PyOpcache *co_opcache;int co_opcache_flag; // used to determine when create a cache.unsigned char co_opcache_size; // length of co_opcache.} PyCodeObject;
PyFrameObject
没发生一次函数调用都会创建一个执行环境,每发生一次函数调用都会创建一个新的执行环境,之间环境之间通过 f_back 连接起来形成链式结构的运行时环境(类比一个函数调用栈)。
每一个执行环境就是一个 PYFrameObject 的实例,改对象是一个变长的对象,里面包含 PyCodeObject、内置变量、全局变量、局部变量、栈顶、栈底这些信息。
typedef struct _frame {PyObject_VAR_HEADstruct _frame *f_back; /* previous frame, or NULL */PyCodeObject *f_code; /* code segment */PyObject *f_builtins; /* builtin symbol table (PyDictObject) */PyObject *f_globals; /* global symbol table (PyDictObject) */PyObject *f_locals; /* local symbol table (any mapping) */PyObject **f_valuestack; /* points after the last local *//* Next free slot in f_valuestack. Frame creation sets to f_valuestack.Frame evaluation usually NULLs it, but a frame that yields sets itto the current stack top. */PyObject **f_stacktop;PyObject *f_trace; /* Trace function */char f_trace_lines; /* Emit per-line trace events? */char f_trace_opcodes; /* Emit per-opcode trace events? *//* Borrowed reference to a generator, or NULL */PyObject *f_gen;int f_lasti; /* Last instruction if called *//* Call PyFrame_GetLineNumber() instead of reading this fielddirectly. As of 2.3 f_lineno is only valid when tracing isactive (i.e. when f_trace is set). At other times we usePyCode_Addr2Line to calculate the line from the currentbytecode index. */int f_lineno; /* Current line number */int f_iblock; /* index in f_blockstack */char f_executing; /* whether the frame is still executing */PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */PyObject *f_localsplus[1]; /* locals+stack, dynamically sized */} PyFrameObject;
代码
字节码
# 编译之后成为 PyCodeObject 存储在堆中>>> data = open('demo.py').read()>>> co = compile(data, "demo.py", "exec")>>> co<code object <module> at 0x00000203E2EEA270, file "demo.py", line 1># 访问co的局部变量、栈大小>>> type(co)<class 'code'>>>> co.co_consts(<code object foo at 0x00000203E2EEA1E0, file "demo.py", line 1>, 'foo', '__main__', 100, None)>>> co.co_varnames()>>> co.co_stacksize2# 查看字节码>>> import dis>>> dis.dis(co)1 0 LOAD_CONST 0 (<code object foo at 0x00000203E2EEA1E0, file "demo.py", line 1>)2 LOAD_CONST 1 ('foo')4 MAKE_FUNCTION 06 STORE_NAME 0 (foo)13 8 LOAD_NAME 1 (__name__)10 LOAD_CONST 2 ('__main__')12 COMPARE_OP 2 (==)14 POP_JUMP_IF_FALSE 4214 16 LOAD_CONST 3 (100)18 STORE_GLOBAL 2 (a)15 20 LOAD_NAME 3 (print)22 LOAD_GLOBAL 2 (a)24 CALL_FUNCTION 126 POP_TOP16 28 LOAD_NAME 0 (foo)30 CALL_FUNCTION 032 POP_TOP17 34 LOAD_NAME 3 (print)36 LOAD_GLOBAL 2 (a)38 CALL_FUNCTION 140 POP_TOP>> 42 LOAD_CONST 4 (None)44 RETURN_VALUEDisassembly of <code object foo at 0x00000203E2EEA1E0, file "demo.py", line 1>:3 0 LOAD_CONST 1 (200)2 STORE_GLOBAL 0 (a)4 4 LOAD_CONST 2 (1000)6 STORE_DEREF 0 (b)5 8 LOAD_GLOBAL 1 (print)10 LOAD_DEREF 0 (b)12 CALL_FUNCTION 114 POP_TOP7 16 LOAD_CLOSURE 0 (b)18 BUILD_TUPLE 120 LOAD_CONST 3 (<code object bar at 0x00000203E2EEA4B0, file "demo.py", line 7>)22 LOAD_CONST 4 ('foo.<locals>.bar')24 MAKE_FUNCTION 826 STORE_FAST 0 (bar)10 28 LOAD_FAST 0 (bar)30 CALL_FUNCTION 032 POP_TOP11 34 LOAD_GLOBAL 1 (print)38 CALL_FUNCTION 140 POP_TOP42 LOAD_CONST 0 (None)44 RETURN_VALUEDisassembly of <code object bar at 0x00000203E2EEA4B0, file "demo.py", line 7>:9 0 LOAD_CONST 1 (2000)2 STORE_DEREF 0 (b)4 LOAD_CONST 0 (None)6 RETURN_VALUE>>>
变量作用域
import inspectdef bar():# nonlocal b# b = 2000# print(b)frame = inspect.currentframe()print(f"Callee fun: {frame.f_code.co_name}")print(f"Callee's local: {frame.f_locals}")print(f"Callee's global: {frame.f_globals}")caller = frame.f_backprint(f"Caller fun: {caller.f_code.co_name}")print(f"Caller's local: {caller.f_locals}")print(f"Caller's global: {caller.f_globals}")def foo():global aa = 200b = 1000print(b)bar()print(b)if __name__ == "__main__":a = 100print(a)foo()print(a)
输出:
1001000Callee fun: barCallee's local: {'frame': <frame at 0x000002A0782775D8, file '.\\demo.py', line 10, code bar>}Callee's global: {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000002A07890C208>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': '.\\demo.py', '__cached__': None, 'inspect': <module 'inspect' from 'C:\\Users\\Administrator\\AppData\\Local\\Programs\\Python\\Python37\\lib\\inspect.py'>, 'bar': <function bar at 0x000002A078949CA8>, 'foo': <function foo at 0x000002A0789CB288>, 'a': 200}Caller fun: fooCaller's local: {'b': 1000}Caller's global: {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000002A07890C208>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': '.\\demo.py', '__cached__': None, 'inspect': <module 'inspect' from 'C:\\Users\\Administrator\\AppData\\Local\\Programs\\Python\\Python37\\lib\\inspect.py'>, 'bar': <function bar at 0x000002A078949CA8>, 'foo': <function foo at 0x000002A0789CB288>, 'a': 200}1000200
这里可以看到
- 被调用函数能访问到调用者的栈帧信息
- 全局变量被存储在每一个栈帧中
内嵌函数
把 bar 内嵌在 foo 函数内,并且两个函数有相同变量名称的局部变量
我们能看到同名局部变量之间互不影响import inspectdef foo():global aa = 200b = 1000print(b)def bar():b = 2000print(b)frame = inspect.currentframe()print(f"Callee fun: {frame.f_code.co_name}")print(f"Callee's local: {frame.f_locals}")caller = frame.f_backprint(f"Caller fun: {caller.f_code.co_name}")print(f"Caller's local: {caller.f_locals}")bar()print(b)if __name__ == "__main__":a = 100print(a)foo()print(a)
不定义这个同名局部变量,直接打印变量 b10010002000Callee fun: barCallee's local: {'b': 2000, 'frame': <frame at 0x0000025051CEAAF8, file '.\\demo.py', line 13, code bar>}Caller fun: fooCaller's local: {'b': 1000, 'bar': <function foo.<locals>.bar at 0x000002505249A318>}1000200
此时程序不会报错,并且直接将上层函数的变量作为自己的局部变量,import inspectdef foo():global aa = 200b = 1000print(b)def bar():# nonlocal bprint(b)frame = inspect.currentframe()print(f"Callee fun: {frame.f_code.co_name}")print(f"Callee's local: {frame.f_locals}")caller = frame.f_backprint(f"Caller fun: {caller.f_code.co_name}")print(f"Caller's local: {caller.f_locals}")bar()print(b)if __name__ == "__main__":a = 100print(a)foo()print(a)
nonlocal b写不写输出都一样。10010001000Callee fun: barCallee's local: {'frame': <frame at 0x000002199FBD0E08, file '.\\demo.py', line 13, code bar>, 'b': 1000}Caller fun: fooCaller's local: {'bar': <function foo.<locals>.bar at 0x00000219A022A288>, 'b': 1000}1000200
参考
https://he11olx.com/2018/07/21/1.CPython3.6源码分析/1.5.Python Code Frame/
《Python 源码剖析》

