Python在这方面设计了PyFrameObject这个结构(对应于龙书中的“活动记录”)来维护运行时环境,并采用了“访问链”的思想(龙书中介绍了“访问链”和“显示表”)来解决不同作用域间变量的访问问题。
不过在PyFrameObject中维护了3个成员,用来指向最经常使用的3个符号表,内置符号表、全局符号表、局部符号表:
PyObject *f_builtins; /* builtin symbol table (PyDictObject) */ PyObject *f_globals; /* global symbol table (PyDictObject) */ PyObject *f_locals; /* local symbol table (any mapping) */
这样可以避免在访问全局变量、内建变量时还要通过“访问链”上的回溯来搜索。
PyFrameObject通过如下成员来维护“访问链”(或者称“符号表链”、“名字空间链”):
struct _frame *f_back; /* previous frame, or NULL */
关于Python的作用域,有一些规则。
最内嵌套作用域规则:由一个赋值语句引进的名字在这个赋值语句所在的作用域里是可见(起作用)的,而且在其内部嵌套的每个作用域里也可见,除非它被嵌套于内部的,引进同样名字的另一条赋值语句所遮蔽/覆盖。
LEGB:符号表的搜索顺序是Local -> Enclosing Function -> Global -> Built-in
一个比较常见而且经典的案例是UnboundLocalError,见如下代码:
x = 10 def foo(): print(x) x += 1 foo()
这一段代码会出现如下错误:
UnboundLocalError: local variable 'x' referenced before assignment
这个问题可以用下面两段话来解答:
This is because when you make an assignment to a variable in a scope, that variable becomes local to that scope and shadows any similarly named variable in the outer scope. Since the last statement in foo assigns a new value
to x, the compiler recognizes it as a local variable. Consequently when the earlier print x attempts
to print the uninitialized local variable and an error results.
Otherwise, all variables found outside of the innermost scope are read-only (an attempt to write to such a variable will simply create a new local
variable in the innermost scope, leaving the identically named outer variable unchanged).
第二个URL,即官方文档也说明了LEGB规则:
- the innermost scope, which is searched first, contains the local names
- the scopes of any enclosing functions, which are searched starting with the nearest enclosing scope, contains non-local, but also non-global names
- the next-to-last scope contains the current module’s global names
- the outermost scope (searched last) is the namespace containing built-in names
上面讨论了帧对象PyFrameObject和作用域、符号表等,下面是比较大的概念:关于Python虚拟机的运行时环境。
虚拟机的具体实现位于ceval.c中的PyEval_EvalFrameEx函数中。
函数开头首先定义了如下变量:
register PyObject **stack_pointer; /* Next free slot in value stack */ register unsigned char *next_instr; register int opcode; /* Current opcode */ register int oparg; /* Current opcode argument, if any */ register enum why_code why; /* Reason for block stack unwind */
含义可以从注释中看出,比如next_instr表示下一条指令,why表示栈展开的原因。
PyEval_EvalFrameEx是一个非常庞大的函数,拥有庞大的switch/case语句数目来执行各种指令。
函数中提供了几个访问指令的宏:
/* Code access macros */ #define INSTR_OFFSET() ((int)(next_instr - first_instr)) #define NEXTOP() (*next_instr++) #define NEXTARG() (next_instr += 2, (next_instr[-1]<<8) + next_instr[-2]) #define PEEKARG() ((next_instr[2]<<8) + next_instr[1]) #define JUMPTO(x) (next_instr = first_instr + (x)) #define JUMPBY(x) (next_instr += (x))
此外,在运行时需要涉及的还有线程和进程,Python使用的是系统原生的线程/进程,并使用PyThreadState和PyInterpreterState对象来进行抽象和维护。
在PyEval_EvalFrameEx函数开头,也定义了tstate变量,并把当前线程状态赋值给该变量:
PyThreadState *tstate = PyThreadState_GET();
接着设置线程状态对象中的帧:
tstate->frame = f;
然后再设置帧的一些信息:
co = f->f_code; names = co->co_names; consts = co->co_consts; fastlocals = f->f_localsplus; freevars = f->f_localsplus + co->co_nlocals; first_instr = (unsigned char*) PyString_AS_STRING(co->co_code); next_instr = first_instr + f->f_lasti + 1; stack_pointer = f->f_stacktop; assert(stack_pointer != NULL); f->f_stacktop = NULL; /* remains NULL unless yield suspends frame */
最后,进入switch/case:
switch (opcode) {
P.S. “访问链”的形成是在PyFrame_New函数中,帧的f_back成员指向当前线程状态对象的frame成员。
JasonLee 2011.08.20 20:18