Python虚拟机函数机制之位置参数的默认值(五)
位置参数的默认值
在Python中,允许函数的参数有默认值。假如函数f的参数value的默认值是1,在我们调用函数时,如果传递了value参数,那么f调用时value的值即为我们传递的值,如果调用时没有传递value的值,那么f将使用value的默认值,即为1。那么,带有默认值的位置参数,其实现机制与一般的位置参数有何不同呢?
我们先来看一下demo3.py
# cat demo3.py
def f(a=1, b=2):
print(a + b)
f()
f(b=5)
然后我们用dis模块编译下demo3.py文件和函数f所对应的字节码指令
# python2.5
……
>>> source = open("demo3.py").read()
>>> co = compile(source, "demo3.py", "exec")
>>> import dis
>>> dis.dis(co)
1 0 LOAD_CONST 0 (1)
3 LOAD_CONST 1 (2)
6 LOAD_CONST 2 (<code object f at 0x7efc8c8ab648, file "demo3.py", line 1>)
9 MAKE_FUNCTION 2
12 STORE_NAME 0 (f)
5 15 LOAD_NAME 0 (f)
18 CALL_FUNCTION 0
21 POP_TOP
6 22 LOAD_NAME 0 (f)
25 LOAD_CONST 3 ('b')
28 LOAD_CONST 4 (5)
31 CALL_FUNCTION 256
34 POP_TOP
35 LOAD_CONST 5 (None)
38 RETURN_VALUE
>>> from demo3 import f
>>> dis.dis(f)
2 0 LOAD_FAST 0 (a)
3 LOAD_FAST 1 (b)
6 BINARY_ADD
7 PRINT_ITEM
8 PRINT_NEWLINE
9 LOAD_CONST 0 (None)
12 RETURN_VALUE
在demo3.py的编译结果中,我们在执行def语句时,有三条LOAD_CONST指令,去除第三条LOAD_CONST是将函数f的PyFunctionObject对象压入运行时栈,还多出两条LOAD_CONST指令。同时,MAKE_FUNCTION指令的参数为2,多出来的LOAD_CONST指令与函数的参数默认值会不会有关系呢?让我们再次回到MAKE_FUNCTION指令,看看在MAKE_FUNCTION的指令中会如何处理2这个指令参数
ceval.c
case MAKE_FUNCTION:
//[1]:获得PyCodeObject对象,并创建PyFunctionObject
v = POP();
x = PyFunction_New(v, f->f_globals);
Py_DECREF(v);
//[2]:处理待默认值的函数参数
if (x != NULL && oparg > 0)
{
v = PyTuple_New(oparg);
if (v == NULL)
{
Py_DECREF(x);
x = NULL;
break;
}
while (--oparg >= 0)
{
w = POP();
PyTuple_SET_ITEM(v, oparg, w);
}
err = PyFunction_SetDefaults(x, v);
Py_DECREF(v);
}
PUSH(x);
break;
代码在[1]处创建PyFunctionObject对象的过程我们已经熟悉,在创建PyFunctionObject对象之后,MAKE_FUNCTION的指令代码会处理函数参数的默认值。MAKE_FUNCTION的指令参数表示当前在运行时栈中一共有多少个函数参数的默认值,在demo3.py中,这个值是2。MAKE_FUNCTION的指令代码会将指令参数指定的所有函数参数的默认值从运行时栈中弹出,然后塞到一个PyTuppleObject对象中。最后,通过调用PyFunction_SetDefaults将该PyTuppleObject对象赋值到PyFunctionObject对象的func_defaults域。这样,函数的参数默认值便成了PyFunctionObject对象的一部分,参数的默认值同PyCodeObject和global名字空间一样,被塞进PyFunctionObject这个大包袱
funcobject.c
int PyFunction_SetDefaults(PyObject *op, PyObject *defaults)
{
if (!PyFunction_Check(op)) {
PyErr_BadInternalCall();
return -1;
}
if (defaults == Py_None)
defaults = NULL;
else if (defaults && PyTuple_Check(defaults)) {
Py_INCREF(defaults);
}
else {
PyErr_SetString(PyExc_SystemError, "non-tuple default args");
return -1;
}
Py_XDECREF(((PyFunctionObject *) op) -> func_defaults);
//将参数默认值赋值给PyFunctionObject对象的func_defaults域
((PyFunctionObject *) op) -> func_defaults = defaults;
return 0;
}
在执行CALL_FUNCTION,并进入fast_function之后,Python虚拟机函数机制之名字空间(二)中的demo1.py的执行路径和本章的demo3.py的执行已经不再相同了。由于在执行MAKE_FUNCTION指令时,Python虚拟机会将函数的参数默认值塞入到与函数对应的PyFunctionObject对象中。所以,在下面代码的[2]处,判断会失败。于是,Python虚拟机会进入PyEval_EvalCodeEx,在进入PyEval_EvalCodeEx之前,将PyFunctionObject对象中的参数默认值提取出来,作为参数,传递给了PyEval_EvalCodeEx
ceval.c
static PyObject *
fast_function(PyObject *func, PyObject ***pp_stack, int n, int na, int nk)
{
PyCodeObject *co = (PyCodeObject *)PyFunction_GET_CODE(func);
PyObject *globals = PyFunction_GET_GLOBALS(func);
//[1]:获取函数对应的PyFunctionObject中的func_defaults
PyObject *argdefs = PyFunction_GET_DEFAULTS(func);
PyObject **d = NULL;
int nd = 0;
PCALL(PCALL_FUNCTION);
PCALL(PCALL_FAST_FUNCTION);
//[2]:判断是否进入快速通道,argdefs != NULL导致判断失败
if (argdefs == NULL && co->co_argcount == n && nk == 0 &&
co->co_flags == (CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE))
{
……
}
//这里获取函数参数默认值信息(1.第一个默认值是地址,2.默认值的个数)
if (argdefs != NULL)
{
d = &PyTuple_GET_ITEM(argdefs, 0);
nd = ((PyTupleObject *)argdefs)->ob_size;
}
return PyEval_EvalCodeEx(co, globals,
(PyObject *)NULL, (*pp_stack) - n, na,
(*pp_stack) - 2 * nk, nk, d, nd,
PyFunction_GET_CLOSURE(func));
}
PyEval_EvalCodeEx是一个非常重要的函数,扩展位置参数和扩展键参数也是通过它来完成的。从fast_function中对PyEval_EvalCodeEx的调用形式可以看到,Python虚拟机在调用PyEval_EvalCodeEx时,同时也将位置参数的信息和键参数的信息传递进去
接下来我们看看,当调用的函数
在下面的PyEval_EvalCodeEx代码中,argcount其实就是na的值,而kwcount就是nk的值。代码同样在[1]处会创建PyFrameObject对象,参数的默认值被直接进入到新的PyFrameObject对象的f_localsplus中
ceval.c
PyObject *
PyEval_EvalCodeEx(PyCodeObject *co, PyObject *globals, PyObject *locals,
PyObject **args, int argcount, //位置参数的信息
PyObject **kws, int kwcount,//键参数的信息
PyObject **defs, int defcount, //函数默认参数的信息
PyObject *closure)
{
register PyFrameObject *f;
register PyObject *retval = NULL;
register PyObject **fastlocals, **freevars;
PyThreadState *tstate = PyThreadState_GET();
PyObject *x, *u;
……
//[1]:创建PyFrameObject对象
f = PyFrame_New(tstate, co, globals, locals);
if (f == NULL)
return NULL;
fastlocals = f->f_localsplus;
freevars = f->f_localsplus + co->co_nlocals;
if (co->co_argcount > 0 ||
co->co_flags & (CO_VARARGS | CO_VARKEYWORDS))
{
int i;
//n为传入位置参数的个数
int n = argcount;
……
//[2]:判断是否使用参数的默认值
if (argcount < co->co_argcount)
{
//m = 位置参数总数-被设置了默认值的位置参数个数
int m = co->co_argcount - defcount;
//[3]:函数调用者必须传递一般位置参数的参数值
for (i = argcount; i < m; i++)
{
if (GETLOCAL(i) == NULL)
{
PyErr_Format(PyExc_TypeError,
"%.200s() takes %s %d "
"%sargument%s (%d given)",
PyString_AsString(co->co_name),
((co->co_flags & CO_VARARGS) ||
defcount)
? "at least"
: "exactly",
m, kwcount ? "non-keyword " : "",
m == 1 ? "" : "s", i);
goto fail;
}
}
//[4]:n>m意味着调用者希望替换一些默认位置参数的默认值
if (n > m)
i = n - m;
else
i = 0;
//[5]:设置默认位置参数的默认值
for (; i < defcount; i++)
{
if (GETLOCAL(m + i) == NULL)
{
PyObject *def = defs[i];
Py_INCREF(def);
SETLOCAL(m + i, def);
}
}
}
}
……
retval = PyEval_EvalFrameEx(f, 0);
……
return retval;
}
对默认参数的讨论中,我们将位置参数继续细分为两类:一般位置参数和默认位置参数。默认位置参数是指定了默认值的位置参数,而没有指定默认值的称为一般位置参数
在PyEval_EvalCodeEx的[2]处,Python虚拟机完成了是否需要设置默认参数值的判断,当调用函数传递的位置参数个数小于函数编译后PyCodeObject对象中的co_argcount指定的参数个数时,说明Python虚拟机需要为函数设定默认参数
在PyEval_EvalCodeEx的[3]处的判断是为了保证一般位置在函数被调用时,由调用者传递了参数值,这里的m就是我们前面说的一般位置参数的个数
在PyEval_EvalCodeEx的[4]处确定要从哪个默认位置参数开始设定参数的默认值。考虑函数def g(a, b, c=1, d=2),如果调用函数时的形式是这样:g(3, 3, 3),那么就不能为参数c设置默认参数了,只能对d设置默认参数,由于n代表了函数调用时传递的位置参数的个数,而m表示一般位置参数的个数,那么n-m就指示了函数在调用时传递的参数中,有多少个参数不是用于一般位置参数的,那这些参数自然是用于默认位置参数。于是,这些默认位置参数不需要再设置默认值了
当最终需要设置默认值的参数个数确定之后,Python虚拟机会在PyEval_EvalCodeEx的[5]处从PyFrameObject对象的func_defaults中将这些参数取出,并通过SETLOCAL将其放入PyFrameObject对象的f_localsplus所管理的内存块中。在[5]处,i指示了需要在f_localsplus中设置默认值的位置。这个i的值有一点值的注意,它从第一个需要设置默认值的默认位置参数的位置开始,依次向后,而这个位置之前的参数都不许用设置默认值,这和Python中设置函数参数默认值的规则是一致的,即:函数参数的默认值从函数参数列表的最右端开始,必须连续设置
现在,我们再来看看Python虚拟机会如何为我们替换默认值
def Py_Func(a=1, b=2):
pass
1.使用默认值
Py_Func() //字节码指令 15 LOAD_NAME 0 (Py_Func) 18 CALL_FUNCTION 0 >>> Py_Func() [call_function]:na=0, nk=0, n=0 [call_function]:co->co_argcount=2, co->co_nlocals=2
2.替换默认值
Py_Func(b=3)
//字节码指令
15 LOAD_NAME 0 (Py_Func)
18 LOAD_CONST 3 ('b')
21 LOAD_CONST 4 (3)
24 CALL_FUNCTION 256
>>> Py_Func(b=3)
[call_function]:na=0, nk=1, n=2
[call_function]:co->co_argcount=2, co->co_nlocals=2
与例1相比,在例2中我们设置了b的值,以此来观察在调用函数时,为默认位置参数传递了一个参数值后,Python虚拟机是如何为我们在例2中替换参数的默认值的,在例2中,字节码指令比例1多两条LOAD_CONST指令,分别将PyStringObject对象"b"和PyIntObject对象3依次压入运行时栈。同时,CALL_FUNCTION的指令参数变为256,这意味着,na=0,而nk=1
在fast_function中,例2不会选择快速通道,而是会进入PyEval_EvalCodeEx。之前我们已经说过,PyEval_EvalCodeEx的参数中,argcount是na的值,而kwcount是nk的值,在图1-1中我们将更细致地展现Python虚拟机进入PyEval_EvalCodeEx时各个参数的意义:

图1-1 PyEval_EvalCodeEx调用时各个参数的意义
现在只剩一个关键问题了,在PyEval_EvalCodeEx中,3是如何取代b的原始默认值2呢?当然,这一切都和kws密切相关
ceval.c
PyObject *
PyEval_EvalCodeEx(PyCodeObject *co, PyObject *globals, PyObject *locals,
PyObject **args, int argcount, //位置参数的信息
PyObject **kws, int kwcount,//键参数的信息
PyObject **defs, int defcount, //函数默认参数的信息
PyObject *closure)
{
……
if (co->co_argcount > 0 ||
co->co_flags & (CO_VARARGS | CO_VARKEYWORDS))
{
……
//[1]:遍历键参数,确定函数的def语句中是否出现了键参数的名字
for (i = 0; i < kwcount; i++)
{
PyObject *keyword = kws[2 * i];
PyObject *value = kws[2 * i + 1];
int j;
……
//[2]:在函数的变量名表中寻找keyword
for (j = 0; j < co->co_argcount; j++)
{
PyObject *nm = PyTuple_GET_ITEM(
co->co_varnames, j);
int cmp = PyObject_RichCompareBool(
keyword, nm, Py_EQ);
if (cmp > 0)
break;
else if (cmp < 0)
goto fail;
}
//[3]:keyword没有在变量名表中出现
if (j >= co->co_argcount)
{
if (kwdict == NULL)
{
PyErr_Format(PyExc_TypeError,
"%.200s() got an unexpected "
"keyword argument '%.400s'",
PyString_AsString(co->co_name),
PyString_AsString(keyword));
goto fail;
}
PyDict_SetItem(kwdict, keyword, value);
}
else
{//[4]:keyword在变量名表中出现
if (GETLOCAL(j) != NULL)
{
PyErr_Format(PyExc_TypeError,
"%.200s() got multiple "
"values for keyword "
"argument '%.400s'",
PyString_AsString(co->co_name),
PyString_AsString(keyword));
goto fail;
}
Py_INCREF(value);
SETLOCAL(j, value);
}
}
if (argcount < co->co_argcount)
{
int m = co->co_argcount - defcount;
for (i = argcount; i < m; i++)
{
if (GETLOCAL(i) == NULL)
{
PyErr_Format(PyExc_TypeError,
"%.200s() takes %s %d "
"%sargument%s (%d given)",
PyString_AsString(co->co_name),
((co->co_flags & CO_VARARGS) ||
defcount)
? "at least"
: "exactly",
m, kwcount ? "non-keyword " : "",
m == 1 ? "" : "s", i);
goto fail;
}
}
if (n > m)
i = n - m;
else
i = 0;
//[5]:设置默认位置参数的默认值
for (; i < defcount; i++)
{
if (GETLOCAL(m + i) == NULL)
{
PyObject *def = defs[i];
Py_INCREF(def);
SETLOCAL(m + i, def);
}
}
}
}
……
}
这里算法的基本思想是:在编译时,Python会将函数的def语句中出现的参数名称都记录在变量名表co_varnames中。由于我们已经看到,在Py_Func(b=3)的指令序列中,Python在执行CALL_FUNCTION指令前会将键参数的名字压入运行时栈,那么我们在PyEval_EvalCodeEx中就能利用运行时栈中保存的键参数的名字在Python编译时得到的co_varnames中进行查找。在co_varnames中记录的变量名的顺序与在函数的def语句中出现的参数的顺序是一致的,在PyFrameObject对象的f_localsplus所维护的内存中,用于存储函数参数的内存也是按照def语句中出现的参数的顺序排列。所以在co_varnames中搜索到键参数名字后,我们可以根据得到的序号信息直接设置f_localsplus中的内存,这样为默认位置参数设置了调用者希望的值
在上述代码的[1]处的for循环中,i为0时,就有keyword为PyStringObject对象"b",而value为PyIntObject对象3。在代码[2]处的for循环,会在函数Py_Func对应的PyCodeObject对象中的co_varnames中查找"b",在Py_Func中,b的索引为1,所以查找成功后,j的值为1。Py_Func对应的PyCodeObject对象中的co_argcount为2,所以判断j >= co->co_argcount将不成立,于是会进入[4]处的else分支,通过SETLOCAL,将新建的PyFrameObject对象的f_localsplus中参数b对应的位置设置为3
代码[5]处的for循环是为需要设置默认值的默认位置参数设置默认值,值的注意的是,这个for循环中设置函数参数的默认值动作只有在条件GETLOCAL(m + i) == NULL满足的时候才能发生,对于f_localsplus[1]这个位置所代表的参数b已经被设置为3,所以不会把b设置为原来的默认值
浙公网安备 33010602011771号