扩展位置参数和扩展键参数
在Python虚拟机函数机制之参数类别(三)的例3和例4中,我们看到了使用扩展位置参数和扩展键参数时指示参数个数的变量的值。在那里,我们发现在函数内部没有使用局部变量时,co_nlocals和co_argcount的值已经不再相同了。从它们的差异我们猜测,当使用扩展位置参数*args或者扩展键参数**kwargs时,实际是作为一个局部变量来实现的。同时,我们还猜测,在Python内部,*args是由PyTuppleObject实现的,而**kwargs是由PyDictObject对象实现的。本章中,将深入地剖析Python是如何实现扩展位置参数和扩展键参数
def Py_Func(value, *args, **kwargs):
pass
Py_Func(-1, 1, 2, a=3, b=4)
//字节码指令
9 LOAD_NAME 0 (Py_Func)
12 LOAD_CONST 1 (-1)
15 LOAD_CONST 2 (1)
18 LOAD_CONST 3 (2)
21 LOAD_CONST 4 ('a')
24 LOAD_CONST 5 (3)
27 LOAD_CONST 6 ('b')
30 LOAD_CONST 7 (4)
33 CALL_FUNCTION 515
>>> Py_Func(-1, 1, 2, a=3, b=4)
[call_function]:na=3, nk=2, n=7
[call_function]:co->co_argcount=1, co->co_nlocals=3
Python虚拟机的执行路径最终将进入PyEval_EvalCodeEx,这个函数我们之前已经捋过几遍,下面,我们来看对扩展位置参数的处理
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;
//创建PyFrameObject对象
f = PyFrame_New(tstate, co, globals, locals);
if (f == NULL)
return NULL;
fastlocals = f->f_localsplus;
freevars = f->f_localsplus + co->co_nlocals;
//[1]:判断是否需要处理扩展位置参数或扩展键参数
if (co->co_argcount > 0 ||
co->co_flags & (CO_VARARGS | CO_VARKEYWORDS))
{
int i;
int n = argcount;
PyObject *kwdict = NULL;
if (argcount > co->co_argcount)
{
if (!(co->co_flags & CO_VARARGS))
{
PyErr_Format(PyExc_TypeError,
"%.200s() takes %s %d "
"%sargument%s (%d given)",
PyString_AsString(co->co_name),
defcount ? "at most" : "exactly",
co->co_argcount,
kwcount ? "non-keyword " : "",
co->co_argcount == 1 ? "" : "s",
argcount);
goto fail;
}
n = co->co_argcount;
}
//[2]:设置位置参数的参数值
for (i = 0; i < n; i++)
{
x = args[i];
Py_INCREF(x);
SETLOCAL(i, x);
}
//[3]:处理扩展位置参数
if (co->co_flags & CO_VARARGS)
{
//[4]:将PyTuppleObject对象放入到f_localsplus中
u = PyTuple_New(argcount - n);
if (u == NULL)
goto fail;
SETLOCAL(co->co_argcount, u);
//[5]:将扩展位置参数放入到PyTuppleObject中
for (i = n; i < argcount; i++)
{
x = args[i];
Py_INCREF(x);
PyTuple_SET_ITEM(u, i - n, x);
}
}
……
}
……
}
在Python编译一个函数时,如果在其形式参数中发现*args这样的扩展位置参数的参数形式,那么Python会在所编译得到的PyCodeObject对象的co_flags中添加一个标识符号:CO_VARARGS,表示该函数在被调用时需要处理扩展位置参数。同样,对于函数中包含**kwargs这样的参数,Python将在co_flags中添加CO_VARKEYWORDS标识。所以,对于含有扩展位置参数和扩展键参数的函数,上述代码[1]处都将成立
前面我们已经知道,在PyEval_EvalCodeEx中,argcount其实就是na的值,一旦argcount > co->co_argcount成立,就意味着函数调用时传递了扩展位置参数。在代码[2]处设置了正规的位置参数后,就会进入代码[3]处对扩展位置参数的处理过程。Python虚拟机会在代码[4]处创建一个长度为(argcount - n)的PyTuppleObject对象,将其放入f_localsplus,这里放置的索引位置是co->co_argcount,然后在代码[5]处将所有的扩展位置参数一股脑地塞入这个PyTuppleObject对象。注意,这里是先创建一个空的PyTuppleObject对象,将其放到f_localsplus,然后才根据传入的参数填充PyTuppleObject对象
了解扩展位置参数的传递机制之后,我们再来看看扩展键参数的传递机制
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))
{
int i;
int n = argcount;
PyObject *kwdict = NULL;
//[1]:创建一个PyDictObject对象,并将其放到f_localsplus中
if (co->co_flags & CO_VARKEYWORDS)
{
kwdict = PyDict_New();
if (kwdict == NULL)
goto fail;
i = co->co_argcount;
//[2]:PyDictObject对象必须在PyTuppleObject对象之后
if (co->co_flags & CO_VARARGS)
i++;
SETLOCAL(i, kwdict);
}
//遍历键参数,确定函数的def语句中是否出现了键参数的名字
for (i = 0; i < kwcount; i++)
{
PyObject *keyword = kws[2 * i];
PyObject *value = kws[2 * i + 1];
int j;
//在函数的变量名对象表中寻找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
{ //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);
}
}
}
……
}
其实,扩展键参数的传递机制与键参数的传递机制有很大的关系,之前分析函数参数的默认值机制时我们已经看过键参数的传递机制了,在那里我们知道Python会在函数的PyCodeObject对象的变量名对象表(co_varnames)中查找键参数的名字,只有在查找失败时,才能确定该键参数应该属于一个扩展键参数
和扩展位置参数的实现一样,在代码的[1]处,Python虚拟机创建了一个PyDictObject对象,并且将该对象放入到PyFrameObject对象的f_localsplus中。值的注意的是,在代码[2]处,这个判断及其后的操作保证:当函数拥有扩展位置参数时,扩展键参数的PyDictObject对象在f_localsplus中的位置一定在扩展位置参数PyTuppleObject对象之后的下一个位置
然后,对调用函数传递进来的每一个键参数,Python虚拟机都会判断它是一般的键参数,还是扩展键参数,如果是扩展键参数,就在代码[3]处将其插入到PyDictObject对象中