Python内核阅读(十四): 函数闭包与修饰器

Python 2017-08-16

起步

一段代码的执行结果不光取决于代码中的符号, 更多地取决于代码中符号的语义, 运行时的语义由名字空间决定. 名字空间在运行时由Python虚拟机动态维护的, 但是有时候需要将名字空间静态化. 例如:

base = 1
def get_compare(base):
    def real_compare(value):
        return value > base
    return real_compare

compare_10 = get_compare(10)
print(compare_10(5))    # False
print(compare_10(20))   # True

这段代码中先设置一个基准值10, 此后尽管每次比较的 real_compare 函数中没有base, 而globals名字空间有 base = 1 , 然后real_compare还是能去得到10.

也就是说, 在 real_compare 这个函数作为返回值赋值给变量compare_10时, 有一个名字空间与它绑定在一起了, 这个 名字空间与函数捆绑后的结果被称为一个闭包(closure) .

闭包的基石

闭包的创建通常是利用嵌套函数来完成的. 与嵌套函数相关的属性 co_freevarsco_cellvars :

  • co_freevars: 通常是tuple, 保存使用了外层作用域中的变量名集合.
  • co_cellvars: 通常是tuple, 保存嵌套作用域中变量名集合.

考虑如下代码:

def get_func():
    value = "inner"
    def inner_func():
        print(value)
    return inner_func
show_value = get_func()
show_value()

这段代码会编译出3个PyCodeObject对象, 一个是这个文件, 一个是get_func, 一个是inner_func. 主要看下inner_func对应的code对象中:

// get_func 中
co_flags : 3
co_cellvars : ('value',)
co_freevars : ()

// inner_func 中
co_flags : 19
co_cellvars : ()
co_freevars : ('value',)

PyFrame_New 创建栈帧的时候:

extras = code->co_stacksize + code->co_nlocals + ncells + nfrees;

运行时栈, 局部变量, cell对象, free对象.

闭包的实现

要知道闭包的实现, 首先需要查看对应编译后的字节码:

def get_func():
    0 LOAD_CONST               0 (<code object get_func at 0x00000000029E5390>)
    2 LOAD_CONST               1 ('get_func')
    4 MAKE_FUNCTION            0
    6 STORE_NAME               0 (get_func)

    value = "inner"
        0 LOAD_CONST               1 ('inner')
        2 STORE_DEREF              0 (value)

    def inner_func():
         4 LOAD_CLOSURE             0 (value)
         6 BUILD_TUPLE              1
         8 LOAD_CONST               2 (<code object inner_func at 0x00000000029DC810>)
        10 LOAD_CONST               3 ('get_func.<locals>.inner_func')
        12 MAKE_FUNCTION            8
        14 STORE_FAST               0 (inner_func)

        print(value)
            0 LOAD_GLOBAL              0 (print)
            2 LOAD_DEREF               0 (value)
            4 CALL_FUNCTION            1

    return inner_func
        16 LOAD_FAST                0 (inner_func)
        18 RETURN_VALUE

show_value = get_func()
     8 LOAD_NAME                0 (get_func)
    10 CALL_FUNCTION            0
    12 STORE_NAME               1 (show_value)

show_value()
    14 LOAD_NAME                1 (show_value)
    16 CALL_FUNCTION            0

有几个新的指令, 不怕, 会一个一个分析的.

在创建闭包函数 inner_func 的指令 MAKE_FUNCTION 8 参数是8, 这意味着:

TARGET(MAKE_FUNCTION) {
    ...
    // 处理参数
    if (oparg & 0x08) {
        assert(PyTuple_CheckExact(TOP()));
        func ->func_closure = POP();
    }
    ...
}

关于闭包用到的变量列表被保存在 func_closure 中, 这是 MAKE_FUNCTION 时对动态信息的处理, 相对的, 静态信息保存在code对象中co_cellvarsco_freevars 中, 在call_funciton中会创建新的PyFrameObject对象, 因此,需要将这些东西拷贝到新的栈帧对象的 f_localsplus 中:

PyObject * _PyEval_EvalCodeWithName(...)
{
    ...
    for (i = 0; i < PyTuple_GET_SIZE(co->co_cellvars); ++i) {
        PyObject *c;
        Py_ssize_t arg;

        if (co->co_cell2arg != NULL &&
            (arg = co->co_cell2arg[i]) != CO_CELL_NOT_AN_ARG) {
            c = PyCell_New(GETLOCAL(arg));
            SETLOCAL(arg, NULL);    // 设置到f_localsplus的宏
        }
        else {
            c = PyCell_New(NULL);
        }
        if (c == NULL)
            goto fail;
        SETLOCAL(co->co_nlocals + i, c);
    }

    for (i = 0; i < PyTuple_GET_SIZE(co->co_freevars); ++i) {
        PyObject *o = PyTuple_GET_ITEM(closure, i);
        Py_INCREF(o);
        freevars[PyTuple_GET_SIZE(co->co_cellvars) + i] = o;
    }
    ...
}

python虚拟机获得了嵌套函数引用的符号名, 用 PyCell_New 新建了一个PyCellObject对象:

[cellobject.h]
typedef struct {
    PyObject_HEAD
    PyObject *ob_ref;   /* Content of the cell or NULL when empty */
} PyCellObject;

这个对象很简单, 只有一个ob_ref, 指向一个PyObject对象. 它的创建也比较简单:

PyObject * PyCell_New(PyObject *obj)
{
    PyCellObject *op;

    op = (PyCellObject *)PyObject_GC_New(PyCellObject, &PyCell_Type);
    op->ob_ref = obj;
    Py_XINCREF(obj);

    _PyObject_GC_TRACK(op);
    return (PyObject *)op;
}

在我们例子中, 创建的PyCellObject的ob_ref是指向NULL. 因为这个是要在value = "inner" 这个赋值语句执行的时候才能被正确识别. 随后这个cell对象被拷贝到新建的PyFrameObject的f_localsplus中. 创建新的栈帧对象后就调用 retval = PyEval_EvalFrameEx(f,0); 从而正式开始对函数 get_func 函数的调用过程.

get_func函数中第一句:

value = "inner"
    0 LOAD_CONST               1 ('inner')
    2 STORE_DEREF              0 (value)

先将"inner"压入栈, 然后是STORE_DEREF:

TARGET(STORE_DEREF) {
    PyObject *v = POP();
    PyObject *cell = freevars[oparg];
    PyObject *oldobj = PyCell_GET(cell);
    PyCell_SET(cell, v);
    Py_XDECREF(oldobj);
    DISPATCH();
}

从运行时栈中弹出"inner" , 然后设置PyCellObject中的ob_ref. 就是这么简单. 这样一来, f_localsplus就有了新的变量名与变量值的对应关系, 闭包的作用是将这个对应关系进行冻结, 使得在嵌套函数inner_func也能使用这层对应关系:

def inner_func():
     4 LOAD_CLOSURE             0 (value)
     6 BUILD_TUPLE              1
     8 LOAD_CONST               2 (<code object inner_func at 0x00000000029DC810>)
    10 LOAD_CONST               3 ('get_func.<locals>.inner_func')
    12 MAKE_FUNCTION            8
    14 STORE_FAST               0 (inner_func)

value属于闭包时用到的变量:

TARGET(LOAD_CLOSURE) {
    PyObject *cell = freevars[oparg];
    Py_INCREF(cell);
    PUSH(cell);
    DISPATCH();
}

从freevars中获取他们的对应关系, MAKE_FUNCTION指令中参数是8:

TARGET(MAKE_FUNCTION) {
    ...
    if (oparg & 0x08) {
        assert(PyTuple_CheckExact(TOP()));
        func ->func_closure = POP();
    }
    ...
}

这样闭包需要的对应关系就顺利的注入到func_closure中.

修饰器的实现

在闭包的基础上, python实现了修饰器, 例如下面代码:

def should_say(fn):
    def say(*args):
        print("will say:")
        fn(*args)
    return say

@should_say
def func():
    print("in func")
func()

修饰器一般也能修改成闭包形式:

def should_say():
    print("will say:")
    def func():
        print("in func")
    return func
func = should_say()
func()

两个输出完全一致, 理解了闭包的实现后, 对修饰器的行为也非常容易理解了. 修饰器的指令和一样是用了闭包用到的co_cellvarsco_freevars, 在make函数func时:

  8           8 LOAD_NAME                0 (should_say)
             10 LOAD_CONST               2 (<code object func at 0x00000000029B5270, line 8>)
             12 LOAD_CONST               3 ('func')
             14 MAKE_FUNCTION            0
             16 CALL_FUNCTION            1
             18 STORE_NAME               1 (func)

MAKE_FUNCTION后紧接着是CALL_FUNCTION, 这里调用的就是LOAD_NAME的should_say函数, 当使用修饰器, 即使不调用函数, 修饰器也会执行的, call_function后才store给func, 也就是func已经不是原来的定义了, 而是赋值为should_say函数的返回值. 执行时候可能已经不是你定义的那样了, 全看修饰器的脸色.

当函数有多个修饰器时:

@a
@b
def c():
    pass

调用顺序是先c函数作为参数调用b, b函数的返回值做参数再调用a. 最后将a的返回值赋值给c.


本文由 hongweipeng 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

赏个馒头吧