目录
概要
上一篇文章讲了python中的可执行/调用对象都有一个code object,但是没有讲执行的一些细节。这篇文章讲一讲这个内容。一个可执行/调用对象可以被多次执行/调用,比如一个函数/方法可以被多次调用,每次调用都是不同的,两两之间互不相干,函数/方法和它的调用之间是1对n的关系,就像类和类的实例是1对n的关系一样,正如类是类的实例的模版,函数/方法对于它的调用来说也是如此,函数/方法有一些属性,值得关注,因为它们将作为它的调用生成的frame(栈帧)对象的依据。本文还将详细介绍python函数的调用过程是如何,帮助你理解一点python解释器的设计思路。
函数属性
我们先来看一看函数/方法的一些属性,看它们是何意义。
__code__、__defaults__和__clousure__
__code__:函数/方法的code object,它有几个属性需要我们了解。见下*)
*)co_code:是函数/方法的字节码序列。
*)co_names:是一个元组,保存的是函数内除开局部变量、自由变量、cell变量(也是局部变量)之外的名字,主要是全局变量及其属性的名字。
*)co_varnames是一个元组,保存的是函数内局部变量的名字。
*)co_cellvars是一个元组,保存的是被函数的内部定义的函数所使用的它的局部变量的名字。
*)co_freevars是一个元组,保存的是被函数所使用的它的外嵌套函数(定义它的外部函数)的局部变量的名字。
*)co_consts用来保存函数内用到的常量的名字。
__defaults__:函数的参数的默认值元组,python中的函数定义的时候可以给它参数一个默认值,但不能让非默认值参数跟在默认值参数的后面,比如def f(a=1, b)这样是行不通的,这意味着函数中的参数要么都没有默认值,要么从某一个参数开始直到最后一个参数都有默认值,这表明__defaults__存的是函数的最后几个参数的默认值,也就是__defaults__ 和函数参数是尾部对齐的,__defaults__最后一个值是函数最后一个参数的默认值,__defaults__倒数第二个值是函数倒数第二个参数的默认值,以此类推。
__clousure__:函数所使用的它的外嵌套函数(定义它的函数)的局部变量包装成的cell对象的元祖,cell对象有一个属性cell_contents,通过它访问实际的变量内容。co_freevars和__closure__是一一对应的。
示例代码:
import sys
k = 0
def f():
a1 = 0
a2 = 1
def g():
b = a1 + a2
c = k
def h():
d = c
return h
return g
g = f()
print("func g attributes\nco_names: %s\nco_varnames: %s\n
co_cellvars: %s\nco_freevars: %s" %
(g.__code__.co_names, g.__code__.co_varnames,
g.__code__.co_cellvars, g.__code__.co_freevars))
print("freevars value: ", [i.cell_contents for i in g.__closure__])
函数的执行原理,frame对象
再来了解一下函数/方法的调用,函数的调用就是一次函数的执行过程,每一次执行,python解释器都先生成一个执行对象:frame对象。
让我们先搞清楚一件事情,一个对象要满足什么条件才能被执行?其实可执行对象没什么复杂的,就是代码加数据,大到线程,小到函数、字节码、汇编指令都一样,它们都是可执行对象,要执行它们,得有地方取得要执行的代码/指令,有地方取得输入的数据和保存输出的数据(结果),还需要一个栈以实现函数调用。
frame对象把函数的code object作为它的f_code属性,代码有了,存放数据的地方在哪呢?frame对象中有三个字典/hash和两个stack,分别是f_locals、f_globals、f_builtins、f_valuestack、f_blockstack,f_locals、f_globals、f_builtins分别是局部作用域、全局作用域和builtin作用域下的一个namespace, f_locals用于存储局部变量(包括cellvar和freevar),f_globals是全局变量的字典,它是从当前被执行的函数的调用者(可以是另一个函数,或者是模块、类)继承而来,f_builtins则是内置对象的字典。python解释器解释执行的是字节码,字节码指令使用的输入/产生的输出来源于/保存到一个栈,这个栈就是frame对象的f_valuestack,当然最终数据来源于或者保存到的位置是f_locals、f_globals、f_builtins这几个namespace,一个典型的字节码的执行过程是:它从f_valuestack的栈顶取几个参数进行运算然后它把结果再存到栈顶。如果跟汇编指令来做一个类比,f_valuestack相当于寄存器,f_locals、f_globals、f_builtins相当于内存,frame对象上还有一个f_blockstack,它用于异常处理流程的实现,这里不展开,有兴趣的读者可以去阅读python源码了解。python解释器借助一个栈去执行字节码,所以python解释器被称为是个栈机器(stack machine)。frame的f_valuestack是用于执行字节码的,它相当于汇编里的寄存器,它不是函数的调用栈。汇编里的函数的调用返回是借助于一个栈来实现的,函数被调用时,在栈的顶部向下(栈是由高地址向低地址扩展的)扩展出一部分作为函数的栈帧,函数的局部变量都存于这块栈帧里,当函数返回时释放栈帧恢复旧栈顶。python里面函数的调用返回是怎么实现的?同样的也是基于栈实现的,python解释器为每一个线程维护一个栈,用于实现该线程内的函数调用,不同于汇编里调用栈是一块连续的内存,python的调用栈相当于一个list,当调用一个函数时把它的一个frame压入栈中,当函数执行完了,再把它的frame出栈,这与汇编里的调用栈是非常不同的,如果我们在函数的字节码序列还没执行完时暂停执行把它的frame出栈,后面再把它入栈,从它上次执行的最后一个字节码之后恢复继续执行,就可以实现函数的多次调用了,这在汇编里面是不可能的,聪明的同学会立即领悟,这不就是生成器的原理嘛,确实如此,刚从c语言转过来的同学会大呼惊奇,其实是被思维定势给框住了,谁说函数的调用只能实现成汇编那样的?这取决于栈和栈帧的实现方式。
说了这么多,函数的frame对象在哪呢?没见过啊。其实是可以得到的,下面给出示例:
import sys
def f(m, n):
return sys._getframe(0)
fr = f(0, 1)
print(fr.f_locals, fr.f_globals, fr.f_builtins)
f_locals和f_localsplus
下面来说说frame对象的f_locals是怎么来的。f_locals里面存放的是函数的局部变量和cellvars和freevars,cellvar同时也是局部变量,也就是f_locals存放的是局部变量和自由变量,局部变量又分为两部分,函数参数和函数体内定义的变量,函数参数在函数的代码被执行之前就被存放到f_locals里面了,函数参数的值要么是调用时候传的,要么是默认值,默认值由函数的__defaults__和函数的code object的co_varnames和co_argcount计算而来,通过上面函数属性部分的内容讲解,相信大家能够猜想出来函数的参数的取值如何得来的,不再啰嗦。函数的自由变量由函数的__closure__和函数code object的co_freevars计算而来,自由变量也是在函数的代码被执行之前被存入f_locals里的,函数的函数体内定义的局部变量则是在函数执行时被存入f_locals的。
有心的同学去查看字节码,会发现所有字节码都是两个字节,一个字节是opcode,一个字节是参数,参数部分都是整数,没有参数的opcode,还是带一个参数部分,它的值是0。可是比如说从某个namespace中取一个对象存到f_valuestack上,使用opcode比如LOAD_NAME/LOAD_GLOBAL/LOAD_DEREF,参数直接使用变量名不好吗?如果是一个整数,得先得从函数的code object的co_varnames/co_names/co_freevars中以这个整数为index去取得变量名,然后再到对应的namespace中取得对象,如果参数直接是变量名,不是省了一步吗,问题在哪呢,问题在于变量名如果是个字符串,它的长度是任意的,如果opcode的参数可以是字符串,那么字节码的长度是不固定的,不同opcode的字节码长度可能不同,相同opcode的字节码,因为参数的长度不同它们的长度也可能不同,解释器拿到一个字节码序列的时候如何去划分字节码的边界?因此python的字节码参数部分被设计成是个整数,这也就是为什么函数的code object上有co_varnames、co_names、co_freevars几个名字元祖的原因。
不过python解释器对函数frame的f_locals中的变量访问也是做了优化的,frame对象上有一个f_localsplus属性,它是一个数组,它先后按照co_varnames、co_cellvars、co_freevars里的名字顺序,把它们对应的变量对象存到f_localsplus里面,当分别用LODA_FAST、LOAD_DEREF去查找局部变量和自由变量时,以它的整数参数为index到f_localsplus的局部变量对象和自由变量对象存放的起始位置进行一个相对寻址就可以了,比从f_locals中去查找快了很多,数组是最快的hash表。
稍微有点经验的python程序员都知道,一个函数里面如果对一个对象的属性访问要重复很多次,而这个对象的该属性值是不会变的(要注意,比如对象的属性是一个list,list的元素可以变化,但对象的属性始终是那个list,对象的属性值仍然没变),那么把那个对象的属性值赋给函数的一个局部变量,会带来效率的提升,道理就是上面所说的道理。