位置参数的默认值

在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设置为原来的默认值

Python虚拟机函数机制之位置参数的默认值(五)的更多相关文章

  1. Python虚拟机函数机制之位置参数(四)

    位置参数的传递 前面我们已经分析了无参函数的调用过程,我们来看看Python是如何来实现带参函数的调用的.其实,基本的调用流程与无参函数一样,而不同的是,在调用带参函数时,Python虚拟机必须传递参 ...

  2. Python虚拟机函数机制之扩展位置参数和扩展键参数(六)

    扩展位置参数和扩展键参数 在Python虚拟机函数机制之参数类别(三)的例3和例4中,我们看到了使用扩展位置参数和扩展键参数时指示参数个数的变量的值.在那里,我们发现在函数内部没有使用局部变量时,co ...

  3. Python虚拟机函数机制之参数类别(三)

    参数类别 我们在Python虚拟机函数机制之无参调用(一)和Python虚拟机函数机制之名字空间(二)这两个章节中,分别PyFunctionObject对象和函数执行时的名字空间.本章,我们来剖析一下 ...

  4. Python虚拟机函数机制之名字空间(二)

    函数执行时的名字空间 在Python虚拟机函数机制之无参调用(一)这一章中,我们对Python中的函数调用机制有个大概的了解,在此基础上,我们再来看一些细节上的问题.在执行MAKE_FUNCTION指 ...

  5. Python虚拟机函数机制之闭包和装饰器(七)

    函数中局部变量的访问 在完成了对函数参数的剖析后,我们再来看看,在Python中,函数的局部变量时如何实现的.前面提到过,函数参数也是一种局部变量.所以,其实局部变量的实现机制与函数参数的实现机制是完 ...

  6. Python虚拟机函数机制之无参调用(一)

    PyFunctionObject对象 在Python中,任何一个东西都是对象,函数也不例外.函数这种抽象机制,是通过一个Python对象——PyFunctionObject来实现的 typedef s ...

  7. python函数中的位置参数、默认参数、关键字参数、可变参数区别

    一.位置参数 调用函数时根据函数定义的参数位置来传递参数. #!/usr/bin/env python # coding=utf-8 def print_hello(name, sex): sex_d ...

  8. Python虚拟机类机制之绑定方法和非绑定方法(七)

    Bound Method和Unbound Method 在Python中,当对作为属性的函数进行引用时,会有两种形式,一种称为Bound Method,这种形式是通过类的实例对象进行属性引用,而另一种 ...

  9. Python虚拟机类机制之instance对象(六)

    instance对象中的__dict__ 在Python虚拟机类机制之从class对象到instance对象(五)这一章中最后的属性访问算法中,我们看到“a.__dict__”这样的形式. # 首先寻 ...

随机推荐

  1. P3930 SAC E#1 - 一道大水题 Knight

    TLE,额 ,有空再写吧. #include<queue> #include<cstdio> #include<vector> #include<algori ...

  2. Redis sorted set(有序集合)

    Redis 有序集合是string类型元素的集合,元素不允许重复. 有序集合中的每个元素都会关联一个数值型的分数.redis正是通过分数来为集合中的成员进行从小到大的排序. 有序集合的成员是唯一的(不 ...

  3. vuejs 组件 移动端push 没有渲染页面

    this.idcards.push(arr) 这个无效 就知道了 vuejs有个跟push相同的方法 console.log(this.list.push.toString()) 这个push是个同名 ...

  4. linux命令行—《命令行快速入门》

    pwd print working directory 打印工作目录 hostname my computer's network name 电脑在网络中的名称 mkdir make director ...

  5. struts2 ognl表达式访问值栈

    1:简单的说,值栈是对应每一个请求对象的轻量级的数据存储中心,在这里统一管理着数据,供Action.Result.Interceptor等Struts2的其他部分使用,这样数据被集中管理起来而不凌乱. ...

  6. Android仿360悬浮小球自定义view实现

    转载请标明出处:http://www.jianshu.com/u/a5ad093cffe8 效果图如下: 图片.png   图片.png 实现当前这种类似的效果 (360小球 悬浮桌面差不错类似).第 ...

  7. Google Play发布App中遇到"多个APK:版本1未提供给任何设备配置使用。"问题的解决方法

    在google play上发布apk,当上传了apk文件,填写了相关的内容信息和介绍图片.图标后,出现“发布应用”始终灰色无法点击,查看原因显示如下问题: 其中支持的设备数量始终显示为0,怀疑是编译出 ...

  8. 51nod 1489 蜥蜴和地下室

    题目来源: CodeForces 基准时间限制:1 秒 空间限制:131072 KB 分值: 10 难度:2级算法题 哈利喜欢玩角色扮演的电脑游戏<蜥蜴和地下室>.此时,他正在扮演一个魔术 ...

  9. python基础一 day14 生成器函数进阶(1)

  10. 第三单元OO总结