参数类别

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

在Python中,函数的参数根据形势的不同可以分为四种类别:

  • 位置参数:如f(a, b),a和b称为位置参数
  • 键参数:f(a, b, name="Python"),其中的name="Python"被称为键参数
  • 扩展位置参数:f(a, b, *args),其中*args被称为扩展位置参数
  • 扩展键参数:f(a, b, **kwargs),其中**kwargs被称为扩展键参数

函数的调用时通过CALL_FUNCTION指令实现的,而CALL_FUNCTION又是通过call_function这个函数来完成函数的调用。之前,在剖析无参函数调用时,我们曾进入到call_function这个函数中,这次,我们要剖析函数的有参调用,依旧要回到call_function函数

ceval.c

static PyObject * call_function(PyObject ***pp_stack, int oparg)
{
//[1]:处理函数参数信息
int na = oparg & 0xff;
int nk = (oparg >> 8) & 0xff;
int n = na + 2 * nk;
//[2]:获得PyFunctionObject对象
PyObject **pfunc = (*pp_stack) - n - 1;
PyObject *func = *pfunc;
……
}

  

当Python函数开始执行CALL_FUNCTION指令时,会首先获得一个指令参数oparg。在这个指令参数oparg中,实际记录的是函数参数的个数信息,包括位置参数的个数和键参数的个数。虽然扩展位置参数和扩展键参数是位置参数和键参数更高级的形式,但是本质上扩展位置参数是由多个位置参数构成的。这意味着,虽然Python中存在四种参数形式,但实际上我们只需要记录位置参数的个数和键参数的个数,就能知道一共有多少个参数,一共需要多大的内存空间来维护参数

CALL_FUNCTION指令参数的长度是两个字节,在低字节,记录着位置参数的个数,在高字节,记录键参数的个数。因为,在理论上,Python中的函数只能有256个位置参数和256个键参数

从call_function中我们可以看到na实际上是位置参数的个数,nk则是键参数的个数。下面,我们修改一下call_function的源码,来观察一下拥有不同种类参数的函数中na和nk究竟是多少。在输出na和nk的同时,我们还输出函数对应的PyCodeObject对象中维护的两个与参数有关的信息:co_argcount(Code Block的位置参数个数,比如说一个函数的位置参数个数)和co_nlocals(Code Block中局部变量的个数,包括其位置参数的个数)

这里有一个问题,既然co_nlocals包含局部变量的个数,和函数位置参数的个数,那co_argcount不是多此一举了吗?实际上,在Python中,函数参数和函数的局部变量关系非常密切,在某种意义上,函数参数就是一种函数局部变量,它们在内存中是连续放置的。当Python需要为函数申请存放局部变量的内存空间时,就需要通过co_nlocals知道局部变量的总数。所以,只有在co_nlocals中包含参数的数量,才能为参数申请内存空间。虽然co_nlocals包含参数的数量,但没有办法从中得知参数的个数,所以必须有一个co_argcount告诉Python函数一共有多少个参数。是不是有点晕了?没关系,下面,我们修改call_function方法,一睹函数参数与局部变量的区别和联系

ceval.c

static PyObject * call_function(PyObject ***pp_stack, int oparg)
{
int na = oparg & 0xff;
int nk = (oparg>>8) & 0xff;
int n = na + 2 * nk;
PyObject **pfunc = (*pp_stack) - n - 1;
PyObject *func = *pfunc;
PyObject *x, *w; char *func_name = PyEval_GetFuncName(func); if (strcmp(func_name, "Py_Func") == 0)
{
printf("[call_function]:na=%d, nk=%d, n=%d\n", na, nk, n);
PyCodeObject *co = (PyCodeObject *)PyFunction_GET_CODE(func);
printf("[call_function]:co->co_argcount=%d, co->co_nlocals=%d\n", co->co_argcount, co->co_nlocals);
} ……
}

  

如上面代码所示,只有当函数名为Py_Func时,才会打印参数信息。编译运行完之后,我们就来花式折腾Py_Func这个函数

def Py_Func(a, b):
pass

  

1.位置参数

Py_Func(1, 2)
//字节码指令
9 LOAD_NAME 0 (Py_Func)
12 LOAD_CONST 1 (1)
15 LOAD_CONST 2 (2)
18 CALL_FUNCTION 2 >>> Py_Func(1, 2)
[call_function]:na=2, nk=0, n=2
[call_function]:co->co_argcount=2, co->co_nlocals=2

  

2.位置参数+键参数

Py_Func(1, b=2)
//字节码指令
9 LOAD_NAME 0 (Py_Func)
12 LOAD_CONST 1 (1)
15 LOAD_CONST 2 ('b')
18 LOAD_CONST 3 (2)
21 CALL_FUNCTION 257 >>> Py_Func(1, b=2)
[call_function]:na=1, nk=1, n=3
[call_function]:co->co_argcount=2, co->co_nlocals=2

  

从例1和例2的对比可以看出,函数参数中一个参数是位置参数还是键参数实际上仅仅是由函数实参的形式决定的,而与函数定义时的形参没有任何关系。从例1到例2,同样是为第二个参数b传递参数值2,由于采用了不同形式的实参,就从位置参数变为键参数。而(na, nk)对也从(2, 0)变为了(1, 1)。可见,na和nk确实忠实地反应着位置参数和键参数的个数

虽然在例1和例2中,na + nk的值是一样的,都是2,但是我们看到,n的值是不同的。在例1时n为2,在例2时n为3。这个起源于计算n的公式,n = na + 2 * nk。为什么会有这样一个公式呢?这一切都要从n的意义说起

ceval.c

PyObject **pfunc = (*pp_stack) - n - 1;
PyObject *func = *pfunc;

  

call_function中,func指向运行时栈中存放的PyFunctionObject对象。而在这条语句之前有句PyObject **pfunc = (*pp_stack) - n - 1,其中pp_stack是当前运行时栈的栈顶指针。所以,pfunc就是栈顶指针回退(n+1)的结果。从例1和例2的指令序列可以看到,在执行MAKE_FUNCTION指令时,会把PyFunctionObject对象压入运行时栈,接着会将所有与参数相关的信息也压入运行时栈,这些信息的个数因函数的不同而不同。所以,在call_function中,我们想成功回退到PyFunctionObject对象的位置,必须获得参数有关的信息个数,这个个数正是n。之前我们说过,na是位置参数的个数,而nk是键参数个数,由于键参数会比位置参数多执行一条LOAD_CONST,将符号压入运行时栈。因此,n = na + 2 * nk

为什么键参数会导致两条LOAD_CONST指令呢?换句话说,在例2中传递b是否必要呢?考虑一个带有默认值的函数def f(a=1, b=2, c=3),假如我们是这样调用f:f(b=1),表示我们希望替换b的默认值,而保留a和c的默认值,如何不传递b,Python如何知道要替换哪个变量的默认值呢?这正是键参数的作用

3.位置参数+扩展位置参数

def Py_Func(a, b, *args):
pass

  

Py_Func(1, 2, 3, 4)
//字节码指令
9 LOAD_NAME 0 (Py_Func)
12 LOAD_CONST 1 (1)
15 LOAD_CONST 2 (2)
18 LOAD_CONST 3 (3)
21 LOAD_CONST 4 (4)
24 CALL_FUNCTION 4 >>> Py_Func(1, 2, 3, 4)
[call_function]:na=4, nk=0, n=4
[call_function]:co->co_argcount=2, co->co_nlocals=3

  

在Python函数的参数表中,非键参数的位置必须在键参数之前,所以Py_Func(1, b=2, 3, 4)这样的函数调用是非法的。从na的值可以看到,扩展位置参数的信息确实被归在位置参数这一类

在[call_function]第二行的输出信息中,我们发现一些特别的地方,在例1和例2中,co_argcount的值和co_nlocals的值是相同的,因为函数内没有局部变量。但是在例3中,函数内同样没有局部变量,co_argcount和co_nlocals的值却是不同的,更奇怪的是,co_argcount作为函数参数的个数,居然是2,明明Py_Func函数中声明了a、b和*args这3个参数。唯一合理的解释是Python内部将扩展位置参数*args作为一个局部变量了,这样,才会有co_argcount=2而co_nlocals=3的结果

我们还能看到,尽管我们调用函数时传递了四个参数,但是这丝毫不能影响co_argcount和co_nlocals的值。实际上,不管我们传多少个参数,都不能影响o_argcount和co_nlocals的值。因为co_argcount和co_nlocals是函数Py_Func编译后产生的PyCodeObject对象的域,也就是说它们的值是在编译期就确定的。从co_argcount=2,co_nlocals=3的结果我们已经可以做一个大胆的猜测,那就是在Python实现Py_Func函数时,所有的扩展位置参数实际上是被存储在一个PyTuppleObject对象中

4.位置参数+扩展键参数

def Py_Func(a, b, **kwargs):
pass

  

Py_Func(1, 2, name="Python", author="Guido")
//字节码指令
9 LOAD_NAME 0 (Py_Func)
12 LOAD_CONST 1 (1)
15 LOAD_CONST 2 (2)
18 LOAD_CONST 3 ('name')
21 LOAD_CONST 4 ('Python')
24 LOAD_CONST 5 ('author')
27 LOAD_CONST 6 ('Guido')
30 CALL_FUNCTION 514 >>> Py_Func(1, 2, name="Python", author="Guido")
[call_function]:na=2, nk=2, n=6
[call_function]:co->co_argcount=2, co->co_nlocals=3

  

从co_argcount和co_nlocals的值上来看,扩展键参数在Python内部也是被当做一个局部变量来看

5.位置参数+局部变量

def Py_Func(a, b):
c = 1

  

Py_Func(1, 2)
//字节码指令
9 LOAD_NAME 0 (Py_Func)
12 LOAD_CONST 1 (1)
15 LOAD_CONST 2 (2)
18 CALL_FUNCTION 2 >>> Py_Func(1, 2)
[call_function]:na=2, nk=0, n=2
[call_function]:co->co_argcount=2, co->co_nlocals=3

  

这里co_nlocals=3是理所当然的,因为Py_Func函数内部终于有一个局部变量了。从执行函数调用的指令上来看,没有涉及到局部变量的指令,这无疑是正确的,因为局部变量属于另一个PyCodeObject中

Python虚拟机函数机制之参数类别(三)的更多相关文章

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

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

  2. Python虚拟机函数机制之位置参数的默认值(五)

    位置参数的默认值 在Python中,允许函数的参数有默认值.假如函数f的参数value的默认值是1,在我们调用函数时,如果传递了value参数,那么f调用时value的值即为我们传递的值,如果调用时没 ...

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

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

  4. Python虚拟机类机制之descriptor(三)

    从slot到descriptor 在Python虚拟机类机制之填充tp_dict(二)这一章的末尾,我们介绍了slot,slot包含了很多关于一个操作的信息,但是很可惜,在tp_dict中,与__ge ...

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

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

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

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

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

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

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

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

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

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

随机推荐

  1. java 与 c#的 中 字符串比较“==”与“equals”的差异

    .net中,其字符串特有的驻留机制,保证了在同一进程中,相同字符序列的字符串,只有一个实例,这样能避免相同内容的字符串重复实例化,以减少性能开销. 先来回顾一下c#中的代码: public stati ...

  2. 解决mysql本地数据库不能用ip访问的问题

    [转]http://gone18611.blog.163.com/blog/static/1851943120104705244116/ MYSQL数据库缺省安装后,其默认用户名ROOT如果只能以&l ...

  3. JavaScript是什么

    JavaScript是一种解释型语言而不是编译型语言,它往往被认为是一种脚本语言,而不被看作是一种真正的编程语言.也就是说,脚本语言比较简单,它们是非程序员所使用的编程语言. 如果一个程序员对Java ...

  4. Windows 8 / win8 拼音输入法/搜狗输入法 visual Studio 2010 / VS2010 不兼容

    是visual assist X 的问题,更新到VA_X_Setup 2001 解决问题 老版本处理:Tools-->Extension Manager-->Uninstall

  5. ArcGIS for Server内置JS Viewer的离线部署和配置

    很多情况下,在地图服务发布完毕后,我们往往利用 ArcGIS for Server内置的 JS Viewer来查看和检测所发布的地图服务是否满足我们的要求.具体操作如下: 点击开始 -> 所有程 ...

  6. Jquery 日期差函数 修改 对火狐进行兼容

    function DateDiff(sDate1, sDate2) { //sDate1和sDate2是yyyy-MM-dd格式 var aDate, oDate1, oDate2, iDays; a ...

  7. ceisum_加载倾斜摄影模型

    osgb转换为3Dtiles格式(使用工具转换) 然后加载到cesium中(加载代码见下,可以控制模型高度) var offset = function(height,tileset) { conso ...

  8. [dp][uestc oj][最长上升子序列] LIS N - 导弹拦截

    N - 导弹拦截 Time Limit: 3000/1000MS (Java/Others)     Memory Limit: 65535/65535KB (Java/Others) Submit  ...

  9. [神经网络]一步一步使用Mobile-Net完成视觉识别(四)

    1.环境配置 2.数据集获取 3.训练集获取 4.训练 5.调用测试训练结果 6.代码讲解 本文是第四篇,下载预训练模型并训练自己的数据集. 前面我们配置好了labelmap,下面我们开始下载训练好的 ...

  10. guruguru

    6576: guruguru 时间限制: 1 Sec  内存限制: 128 MB提交: 28  解决: 12[提交] [状态] [讨论版] [命题人:admin] 题目描述 Snuke is buyi ...