Python源码剖析笔记6-函数机制

原创 2015年08月31日 00:27:08

Python的函数机制是很重要的部分,很多时候用python写脚本,就是几个函数简单解决问题,不需要像java那样必须弄个class什么的。

本文简书地址:http://www.jianshu.com/p/d00108741a18

1 函数对象PyFunctionObject

PyFunctionObject对象的定义如下:

typedef struct {
    PyObject_HEAD
    PyObject *func_code;    /* A code object */
    PyObject *func_globals; /* A dictionary (other mappings won't do) */
    PyObject *func_defaults;    /* NULL or a tuple */
    PyObject *func_closure; /* NULL or a tuple of cell objects */
    PyObject *func_doc;     /* The __doc__ attribute, can be anything */
    PyObject *func_name;    /* The __name__ attribute, a string object */
    PyObject *func_dict;    /* The __dict__ attribute, a dict or NULL */
    PyObject *func_weakreflist; /* List of weak references */
    PyObject *func_module;  /* The __module__ attribute, can be anything */
} PyFunctionObject;

先说一下PyFunctionObject中几个重要的变量,func_code, func_globals。其中func_code是函数对象对应的PyCodeObject,而func_globals则是函数的global名字空间,其实这个值是从上一层PyFrameObject传递而来。func_defaults是存储函数默认值的,后面分析函数参数的时候会提到,func_closure与闭包相关,后面也会提到。

###func.py
def f():
  print "Function"

f()

如上面例子func.py,该文件编译后对应2个PyCodeObject对象,一个是func.py本身,一个是函数f。而PyFunctionObject则是在执行字节码def f():时通过MAKE_FUNCTION指令生成。创建PyFunctionObject对象时,会将函数f对应的PyCodeObject对象和当前PyFrameObject对象传入作为参数,最终也就是赋值给PyFunctionObject中的func_code和func_globals字段了。在调用函数时,会将PyFunctionObject对象传入到fast_function函数中,最终根据PyFunctionObject对象的func_code和func_globals字段构建新的栈帧对象PyFrameObject,然后调用PyEval_EvalFrameEx在新的栈帧中执行函数字节码。其中PyEval_EvalFrameEx函数在之前的Python执行原理中有提到过,当时提到的PyEval_EvalCodeEx函数其实也是创建了新的栈帧对象PyFrameObject然后执行PyEval_EvalFrameEx函数。

2 函数调用栈帧

函数调用通过栈帧来建立关联,每个被调用函数的栈帧PyFrameObject会通过f_back指针指向调用函数。而local,global以及builtin名字空间,local名字空间针对新的栈帧是全新的,而global名字空间则是由创建PyFrameObject时从PyFunctionObject传递过来。builtin名字空间则是共享调用者栈帧的(如果该栈帧是初始栈帧,则会先获取builtin字典用于设置PyFrameObject的f_builtins字段)。

这里可以回顾一下C语言中的函数调用的栈帧关系。如下面的代码,对应的栈帧结构如图所示。在调用函数时,会先把函数参数会压入当前函数的栈帧中,每个函数都有自己的栈帧,由于esp会变化,所以其他函数会通过ebp来索引函数参数。

图1 c语言函数栈帧

//函数调用栈帧测试代码func.c
int bar(int c, int d)
{
    int e = c + d;
    return e;
}

int foo(int a, int b)
{
    return bar(a, b);
}

int main(void)
{
    foo(2, 3);
    return 0;
}

那么python中是如何来模拟函数参数传递的呢?从C语言函数调用过程可以知道,函数调用前,函数参数会先压入到调用函数的栈帧中,而被调用函数则根据ebp来取参数。这里先回顾下PyFrameObject对象的结构,函数调用与PyFrameObject有着千丝万缕的联系。

typedef struct _frame {
    PyObject_VAR_HEAD
    struct _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 */
    PyObject **f_stacktop;
    PyObject *f_trace;      /* Trace function */

    PyObject *f_exc_type, *f_exc_value, *f_exc_traceback;

    PyThreadState *f_tstate;
    int f_lasti;        /* Last instruction if called */

    int f_lineno;       /* Current line number */
    int f_iblock;       /* index in f_blockstack */
    PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */
    PyObject *f_localsplus[1];  /* locals+stack, dynamically sized */
} PyFrameObject;

PyFrameObject对象中,f_valuestack指向运行时栈的栈底,而f_stacktop则是指向栈顶,在往运行时栈中压入函数参数时,f_stacktop会变化,这两个变量有点类似C里面的ebp和esp。f_localsplus则是指向局部变量+Cell对象+Free对象+运行时栈,其内存布局如图2所示。其中cell对象和free对象在闭包中用到,后面再看,这里主要说说局部变量和运行时栈。python在调用函数之前,会先将函数对象,函数参数压入到当前栈帧的运行时栈中,而在执行函数时,会新建一个PyFrameObject栈帧对象,然后将函数参数拷贝到新栈帧的存储局部变量的那块空间中(也就是f_localsplus执行的那块内存),接着才会调用PyEval_EvalFrameEx执行被调用函数的代码。

图2 f_localsplus内存布局

看下面的func2.py,通过这个例子可以来看一下函数调用流程。例子代码和对应字节码如下。

#func2.py
def f(name, age):
    age += 5
    print '%s is %s old' % (name, age)
f('ssj', 18)

##字节码
In [1]: import dis

In [2]: source = open('func2.py').read()

In [3]: co = compile(source, 'func2.py', 'exec')

In [4]: dis.dis(co)
  1           0 LOAD_CONST               0 (<code object f at 0x10776faf8, file "func2.py", line 1>)
              3 MAKE_FUNCTION            0
              6 STORE_NAME               0 (f)

  4           9 LOAD_NAME                0 (f)
             12 LOAD_CONST               1 ('ssj')
             15 LOAD_CONST               2 (18)
             18 CALL_FUNCTION            2
             21 POP_TOP             
             22 LOAD_CONST               3 (None)
             25 RETURN_VALUE 

In [5]: dis.dis(co.co_consts[0])
  2           0 LOAD_FAST                1 (age)
              3 LOAD_CONST               1 (5)
              6 INPLACE_ADD         
              7 STORE_FAST               1 (age)
              ......

可以看到def f(name, age):跟之前说过的一样,字节码就是通过MAKE_FUNCTION指令创建PyFunctionObject对象并存储到local名字空间,对应的符号为函数名f,如果函数有默认参数,在MAKE_FUNCTION指令中还会设置默认参数到func_defaults字段。而准备调用函数时,则是先讲函数对象和函数参数执行函数时,会将函数参数压栈,然后才通过CALL_FUNCTION指令调用函数。在调用PyEval_EvalFrameEx执行函数代码前,创建新的栈帧后,会先将函数参数拷贝到f_localsplus指向的那片局部变量空间中,然后才真正执行函数f调用代码。执行函数f时,会将age参数压入栈然后加上5,然后存储到f_localsplus的第二个字段(第一个字段为name字符串”ssj”)。函数参数位置变化如下图所示。

图3 函数参数位置变化

3 函数执行时名字空间

还是看第一节中给的例子func.py,其对应的字节码如下,其实定义函数def f():就是用函数对应的PyCodeObject和栈帧对应的f_globals构建PyFunctionObject对象,然后通过STORE_NAME指令将PyFunctionObject对象与函数名f关联并存储到local名字空间。函数f对应的PyCodeObject可以通过co.co_consts[0]获取并查看。

In [1]: source = open('func.py').read()

In [2]: import dis

In [3]: co = dis.dis(source, 'func.py', 'exec')
 1           0 LOAD_CONST               0 (<code object f at 0x1107688a0, file "func.py", line 1>)
              3 MAKE_FUNCTION            0
              6 STORE_NAME               0 (f)

  4           9 LOAD_NAME                0 (f)
             12 CALL_FUNCTION            0
             15 POP_TOP             
             16 LOAD_CONST               1 (None)
             19 RETURN_VALUE  

In [10]: dis.dis(co.co_consts[0])
  2           0 LOAD_CONST               1 ('Function')
              3 PRINT_ITEM          
              4 PRINT_NEWLINE       
              5 LOAD_CONST               0 (None)
              8 RETURN_VALUE        

之前有说过python中分为local,global,builtin名字空间,函数执行时的名字空间略有不同。其global名字空间我们可以看到是通过PyFunctionObject从上一层栈帧传递来的,而local名字空间则是赋值为NULL,也就是函数中并没有用到local名字空间,那么问题来了,函数中的那些局部变量是怎么访问到的呢?那其实在函数中局部变量是通过LOAD_FAST指令(这个指令下一节会分析)来访问的,也就是说它访问的是f_localsplus的内存空间,不需要动态查找f_locals这个PyDictObject,静态的方法可以提供效率。

4 函数参数

Python中函数参数分为位置参数,键参数以及扩展位置参数和扩展键参数。位置参数就是之前我们例子中的参数,而键参数则是在调用函数指定参数的值。而扩展位置参数和扩展键参数格式则是类似*lst**kwargs。位置参数还能设置默认值,如果有默认值,默认值是在MAKE_FUNCTION指令赋值给func_defaults的。

下面的例子可以看到这几种参数的用法。扩展位置参数在python内部是通过一个元组对象存储的,不管最终传递了几个参数。而扩展键参数在python内部则是通过一个字典对象存储的。对于像 def f(a, b, *lst):这样的函数,如果调用函数时参数为f(1,2,3,4),其实在PyCodeObject对象中的co_argcount=2, co_nlocals=3。co_argcount是位置参数的个数,而co_nlocals是局部变量数目,包括位置参数在内。

##params1.py 位置参数和键参数
def f(a, b):
    print a, b
f(1, 2)
f(b=2, a=1)

##params2.py 位置参数,扩展位置参数,扩展键参数
def f(value, *lst, **kwargs):
    print value # -1
    print lst  # (1,2)
    print kwargs # {'a':3, 'b':4}
f(-1, 1, 2, a=3, b=4)

##params3.py 位置参数默认值
def f(lst = []):
    lst.append(3) 
    print lst 
f() #打印[3]
f() #打印[3,3]

最后还要提到的一点的是,函数参数默认值是在定义函数时设置的。如例子中的params3.py所示,如果指定了参数默认值,而调用函数时又没有覆盖默认值,则容易出现问题。要解决这个问题,可以在函数f中加个判断if lst: lst = []

5 闭包和装饰器

之前提到过,PyCodeObject中有两个字段与闭包相关,分别是co_cellvars和co_freevars。其中co_cellvars通常是一个元组,里面保存的是嵌套作用域中使用的变量名集合,而co_freevars也通常是一个元组,里面保存的是外层作用域中的变量名集合。如下面这个闭包的例子,有三个PyCodeObject对象,closure.py本身,函数get_func以及inner_func分布对应一个PyCodeObject。其中get_func的PyCodeObject中的co_cellvars值是元组(‘value’,),同时,inner_func的PyCodeObject的co_freevars存储的内容也是变量名value。

#closure.py 闭包
def get_func():
    value = "inner"
    def inner_func():
         print value
    return inner_func
show_value = get_func()
show_value()

可以看下get_func和inner_func的PyFrameObject的内存布局,就可以大致了解闭包的机制了。其实就是在外层函数的局部变量中存储内层嵌套函数inner_func的PyFunctionObject对象,而PyFunctionObject中的func_closure字段是一个存储PyCellObject的元组对象。在执行inner_func时,会先将func_closure中存储的PyCellObject对象拷贝到inner_func的PyFrameObject的free对象中,也就是cell对象后面那块存储空间,这样在inner_func中通过freevars引用到value了(注意,这个freevars不是inner_func的PyCodeObject中的co_freevars,而是PyFrameObject中的对应的内存区域,虽然他们的内容是一致的)。

图5 闭包机制图示

装饰器是基于闭包实现的,可以对一个函数,方法,类进行加工,实现一些额外功能,在实际编码中会经常用到,比如检查用户是否登录,检查输入参数等,就可以用到装饰器来减少冗余代码。下面是一个装饰器的例子:

#decorator.py 装饰器
def wrapper(fn):
    def _wrapper():
        print 'wrapper '
        fn()
    return _wrapper

@wrapper
def func():
    print 'real func'

if __name__ == "__main__":
    func()  #输出'wrapper' 'real func'

更多装饰器介绍,参见vamei的这篇文章 Python深入05 装饰器

6 参考资料

  • 《python源码剖析》 主要例子和原理都是参照本书
  • Python快速教程
  • 宋劲松 《Linux C语言一站式编程》
版权声明:本文为博主原创文章,未经博主允许不得转载。

相关文章推荐

谈谈 Python 程序的运行原理

这篇文章准确说是『Python 源码剖析』的读书笔记,整理完之后才发现很长,那就将就看吧。以下使用的是 Python 2,在 Python 3 下一些细节可能会略有不同。...

Python源码剖析笔记5-模块机制

本文简书地址: http://www.jianshu.com/p/14586ec50ab6 python中经常用到模块,比如import xxx,from xxx import yyy这样子,里面...

Python源码剖析笔记7-类机制

拖了好一段时间了,终于有空来看看python中的类机制了。内容太多,感觉有些地方还是模糊的,先写一些吧,有错误烦请指出。 1 Python对象模型1.1 概述python2.2之前的这里就不考虑了,从...

【库函数源码剖析系列】(6) strchr

strchr: // strchr #include char *Strchr(const char *s, int c) { for (; *s != (char)c; ++s) ...
  • Justme0
  • Justme0
  • 2012年09月29日 00:53
  • 1928

Python的垃圾回收机制(四)之回收流程源码剖析

0. 背景之前介绍了Python垃圾回收的简介,内存模型和回收机制,这里会详细讲述上一文回收机制中回收流程的具体实现。...

Android菜鸟的成长笔记(6)——剖析源码学自定义主题Theme

还记得在Android菜鸟的成长笔记(3)中我们曾经遇到了一个问题吗?"这个界面和真真的QQ界面还有点不同的就是上面的标题myFirstApp,怎么去掉这个标题呢?",当时我直接在AndroidMai...

STL源码剖析笔记-6算法(6.7 其他算法2)

6.7.2 lower_bound 在不破坏排序状态的原则下,可插入value的第一个位置 区间为有序的 如果存在与value相等的元素,返回其迭代器 如果不存在,返回第一个不小于value的元素,即...

STL源码剖析笔记-6算法(6.7 其他算法1)

6.7 其他算法6.7.1 单纯的数据处理 adjacent_find: 找出第一组满足条件的相邻元素。 template ForwardIterator adjacent_find(Forwar...

STL源码剖析笔记-6算法(6.4 基本算法)

6.4 基本算法 基本算法位于头文件中 equal:判断两个序列是否相等 要求第二个序列要比第一个序列多。 template inline bool equal(InputIterator1 fi...

Android菜鸟的成长笔记(6)——剖析源码学自定义主题Theme

还记得在Android菜鸟的成长笔记(3)中我们曾经遇到了一个问题吗?"这个界面和真真的QQ界面还有点不同的就是上面的标题myFirstApp,怎么去掉这个标题呢?",当时我直接在AndroidMai...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:Python源码剖析笔记6-函数机制
举报原因:
原因补充:

(最多只允许输入30个字)