关于Python函数机制,我们引入一个重要的对象——PyFunctionObject,它在函数机制里充当的是一个邮递员的角色。在函数创建时(def语句),Python将函数需要的静态信息(如PyCodeObject指针)和运行时的信息(如global名字空间)打包成一个结构(就是PyFunctionObject),压入运行时栈,再通过STORE_NAME将其与函数名对应。到了函数调用时,通过LOAD_NAME将函数名对应的PyFunctionObject压入栈(有参数的还得先压参数)接着由CALL_FUNCTION接手,将PyFunctionObject里面有用的东西取出来,然后最终还是通过递归调用之前介绍过的虚拟机的实现——PyEval_EvalFrameEx来执行函数中的字节码。
短短几句,似乎就将Python函数的大概讲完,其实当然没那么简单,Python的各种函数参数形式、嵌套函数、闭包,还是蛮有研究价值的,让我们先从一个简单的例子开始:
1 def f(): 2 pass 3 4 def g(a, b): 5 pass 6 7 def h(*lst, **dct): 8 pass 1 0 LOAD_CONST 0 (<code object f at 0xb77205c0, file "function.py", line 1>) 3 MAKE_FUNCTION 0 6 STORE_NAME 0 (f) 4 9 LOAD_CONST 1 (<code object g at 0xb7720530, file "function.py", line 4>) 12 MAKE_FUNCTION 0 15 STORE_NAME 1 (g) 7 18 LOAD_CONST 2 (<code object h at 0xb7720698, file "function.py", line 7>) 21 MAKE_FUNCTION 0 24 STORE_NAME 2 (h)
可以看出,无论是无参数、有参数,还是扩展位置、扩展键参数,它们创建函数的字节码竟然是惊人的相似,尤其是MAKE_FUNCTION,连字节码参数都是一样的“0”,难道是调用的时候会有不同?嗯,这是必须的,不过我们先按下不表,因为还漏了一种函数创建的形式,那就是带默认参数的:
1 def f(a, b): 2 pass 3 4 def g(a=1, b=2): 5 pass 6 1 0 LOAD_CONST 0 (<code object f at 0xb77855c0, file "function2.py", line 1>) 3 MAKE_FUNCTION 0 6 STORE_NAME 0 (f) 4 9 LOAD_CONST 1 (1) 12 LOAD_CONST 2 (2) 15 LOAD_CONST 3 (<code object g at 0xb7785530, file "function2.py", line 4>) 18 MAKE_FUNCTION 2 21 STORE_NAME 1 (g)
从“f”和“g”的对比发现,把符号“g”压入栈前,还要把两个默认参数“1”跟“2”压入栈,这是为了在MAKE_FUNCTION的时候,把这两个默认值添加到“g” 这个PyFunctionObject的func_defaults(是一个tuple指针,顾名思义,保存函数默认值)里面。
接下来要看一下函数调用的时候,Python是怎样处理参数的传递的,看下面这个简单的例子:
6 def g(a, b): 7 pass ... 12 g(1, 2) 13 g(1, b=2) 12 39 LOAD_NAME 2 (g) 42 LOAD_CONST 5 (1) 45 LOAD_CONST 6 (2) 48 CALL_FUNCTION 2 51 POP_TOP 13 52 LOAD_NAME 2 (g) 55 LOAD_CONST 5 (1) 58 LOAD_CONST 7 ('b') 61 LOAD_CONST 6 (2) 64 CALL_FUNCTION 257
可以看到,函数调用确实展现各种不同。第一次调用只有位置参数,那么就把“1”和“2”按顺序压入栈,而第二次调用,由于有键参数,所以要把键的对应关系“b”和“2”同时压进去。接下来CALL_FUNCTION的参数就显得尤为重要了,它是帮助Python正确处理函数参数信息和获得PyFunctionObject对象的关键。我们说过,Python字节码的参数是2个字节,而这个字节码的参数中,低字节是记录位置参数的个数,高字节是记录键参数的个数(由于这个规定,像g(a=1, 2)这样的,键参数位于位置参数之前的调用,是会报语法错误的)。对于第一次调用,有两个位置参数,所以是0x0002=2;对于第二次调用,有一个位置参数和一个键参数,所以是0x0101=257。获得参数的信息后,又如何获得PyFunctionObject对象呢?有一个公式(其实也差不多是其代码实现了):
n = na + 2 * nk; // na为位置参数个数,nk为键参数个数 p_func = stack_pointer - n - 1; // stack_pointer是当前运行时栈栈顶指针
得到的这个p_func就是指向该PyFunctionObject对象的,要说明一下的是nk还要乘以2,那是因为键参数要以“key/value”的形式压栈的,所以每个键参数传入占的空间为2。至于函数创建时有扩展位置参数或扩展键参数声明的,在函数调用时是什么情况?其实一样的,例如“def f(*lst, **dict)”这种形式的,我们调用时是“def f(1, 2, a=3, b=4)”,它对应字节码就是(这里省略了字节码参数):
LOAD_NAME (f) LOAD_CONST (1) LOAD_CONST (2) LOAD_CONST (a) LOAD_CONST (3) LOAD_CONST (b) LOAD_CONST (4) CALL_FUNCTION
接着,对于一般情况,都是进入fast_function,在里面创建一个新的PyFrameObject,向它传递一些信息(如PyCodeObject、globals、localsplus),并递归调用PyEval_EvalFrameEx(没默认参数的情况)。假如有默认参数,那么MAKE_FUNCTION的时候就会把默认参数绑定到PyFunctionObject.func_defaults这个值,在函数调用时会进入PyEval_EvalCodeEx,然而最终还是会调用PyEval_EvalFrameEx来执行函数实现的相关代码。
接下来,要讨论下函数参数到了函数内部是怎样访问。回想刚才我们的函数定义和函数调用,似乎没有涉及到形参名字与其实参的对应关系。这里,关键在于PyFrameObject中f_localsplus这个变量,这个变量指向的内存最前面部分放的就是实参的值(对于函数中的局部变量,它也是存放在这里,紧接着函数参数,所以我们有时候说函数参数实际上也是一种局部变量,就是这样来的)。调用函数时,我们把这些实参按顺序地放在f_localsplus的前面,到了函数内部,Python是通过一个偏移量i(字节码的一个参数,编译时决定,例如字节码“LOAD_FAST 1”中的“1”),然后调用GETLOCAL(i)和SETLOCAL(i)(字节码LOAD_FAST和STORE_FAST中对应的关键C宏)来操作这一片内存区域的函数实参,而不是通过一些名字的映射机制。下面一段代码和图示可以展现这段内存到底是怎样分布的:
def f(value1, value2, *lst, **dct): pass f(1, 2, 3, 4, a=5, b=6)
它的PyFrameObject中f_localsplus的情形是:
.---------. | 'a' | 5 | |-----|---| | 'b' | 6 | '---------' ↑ _____________________________________________ | | | | | | | | |...|value1|value2| lst | dct | ... | (stack) | | | (1) | (2) | | | | | |___|______|______|_____|_____|_____|_________| ↓ .-------. | 3 | 4 | '-------'
对于有默认参数值的函数,上面的图就要根据实参的情况来定了,这时Python要做的事情就是将函数声明时的na、nk(na、nk分别指位置参数和键参数的个数)和函数调用时的na、nk作各种比较,决定f_localsplus的值到底用默认值还是用实参替换,具体算法这里就不作展开了。
Python函数机制中还有一个重要的东西,就是嵌套函数,它是实现闭包的基础,而闭包又能衍生出decorator这么美好的东西,所以必须介绍一下。看下面这个例子:
val = "global" def outer_func(v): val = v def inner_func(): print "value :", val return inner_func f = outer_func("closure") f()
相信很多人都知道显示的是“value : closure”,而不是“value : global”,那到底为什么呢?我们知道,编译时,对于每一个PyCodeObject,都有两个属性:co_cellvars和co_freevars。co_cellvars是一个tuple,保存被嵌套作用域使用的变量名集合;co_freevars也是一个tuple,保存使用了外层作用域中的变量名集合。它们对应的运行时的对象所处的位置如下图所示:
_______________________________________ | | | | | | |...|局部变量|cell对象|free对象|运行时栈| | | | | | | |___|________|________|________|________|
这个地方怎么这么熟悉,没错了,就是刚刚提到的f_localsplus(“局部变量”到“运行时栈”这段,如果算上前面的省略号“...”,那叫PyFrameObject),cell和free对象正好就位于刚才我们介绍的函数参数所在的“局部变量”区域后面。在执行“f = outer_func(1)”的时候,Python首先会在如下图所示的位置创建一个PyCellObject对象cell(它的结构很简单,就一个头部和内容指针ob_ref):
outer_func的PyFrameObject对象 _______________________________________ | | | | | | |...|局部变量|cell对象|free对象|运行时栈| | | | | | | |___|________|________|________|________| ↓ .------. ob_ref | cell |------→ NULL '------'
接下来,进入outer_func的函数体,到“val = v”这里,把cell对象的ob_ref绑定为v(就是函数调用传进来的字符串“closure”):
outer_func的PyFrameObject对象 _______________________________________ | | | | | | |...|局部变量|cell对象|free对象|运行时栈| | | | | | | |___|________|________|________|________| ↓ .------. ob_ref .-----------. | cell |------→| "closure" | '------' '-----------'
接着,执行“def inner_func():”。这里,先新建一个PyTupleObject对象tuple_obj,打包刚才新建的cell对象。然后新建一个PyFunction对象func_obj(要完成对相应PyCodeObject和global名字空间的绑定),随后通过MAKE_CLOSURE将func_obj的func_closure指针指向刚才新建的tuple_obj,再通过STORE_FAST又将func_obj放置到f_localsplus的局部变量区域中:
outer_func的PyFrameObject对象 _______________________________________ | | | | | | |...|局部变量|cell对象|free对象|运行时栈| | | | | | | |___|________|________|________|________| ↓ | .--------. | |func_obj| ↓ '--------' .------. ob_ref .-----------. func_closure↓ | cell |------→| "closure" | .---------. '------' '-----------' |tuple_obj|---------↑ '---------'
至此,闭包的创建已经完成。最后,“return inner_func”将新建的func_obj压入栈,并将它返回给上一个栈帧(这里也就是最外层py文件的栈帧)。接下来调用闭包函数“f()”,进入call_function的时候,因为inner_func是嵌套函数,它会先把引用到的外层作用域中的变量放入自己的free对象空间里(也就是刚才被压进py文件栈帧的func_obj,实质上拿的是func_obj->tuple_obj->cell,有点复杂):
inner_func的PyFrameObject对象 _______________________________________ | | | | | | |...|局部变量|cell对象|free对象|运行时栈| | | | | | | |___|________|________|________|________| ↓ .------. ob_ref .-----------. | cell |------→| "closure" | '------' '-----------'
到了inner_func里面的“print "value :", val”时,Python就会去执行“LOAD_DEREF”(编译时Python就知道该往什么地方找了),也就是去上面这个f_localsplus中的free对象空间去找val,打印它的值。
至此,闭包的大致全过程已经展现出来了。而在闭包基础上实现的decorator实际上也不难理解,只要理解了闭包的原理,并且理解:
@decorator_func def real_func(): pass
实际上和
real_func = decorator_func(real_func)
几乎是同一回事,也就差不多了,所以这里将不再展开介绍decorator。