Python虚拟机函数机制之参数类别(三)
参数类别
我们在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虚拟机函数机制之参数类别(三)的更多相关文章
- Python虚拟机函数机制之扩展位置参数和扩展键参数(六)
扩展位置参数和扩展键参数 在Python虚拟机函数机制之参数类别(三)的例3和例4中,我们看到了使用扩展位置参数和扩展键参数时指示参数个数的变量的值.在那里,我们发现在函数内部没有使用局部变量时,co ...
- Python虚拟机函数机制之位置参数的默认值(五)
位置参数的默认值 在Python中,允许函数的参数有默认值.假如函数f的参数value的默认值是1,在我们调用函数时,如果传递了value参数,那么f调用时value的值即为我们传递的值,如果调用时没 ...
- Python虚拟机函数机制之名字空间(二)
函数执行时的名字空间 在Python虚拟机函数机制之无参调用(一)这一章中,我们对Python中的函数调用机制有个大概的了解,在此基础上,我们再来看一些细节上的问题.在执行MAKE_FUNCTION指 ...
- Python虚拟机类机制之descriptor(三)
从slot到descriptor 在Python虚拟机类机制之填充tp_dict(二)这一章的末尾,我们介绍了slot,slot包含了很多关于一个操作的信息,但是很可惜,在tp_dict中,与__ge ...
- Python虚拟机函数机制之闭包和装饰器(七)
函数中局部变量的访问 在完成了对函数参数的剖析后,我们再来看看,在Python中,函数的局部变量时如何实现的.前面提到过,函数参数也是一种局部变量.所以,其实局部变量的实现机制与函数参数的实现机制是完 ...
- Python虚拟机函数机制之位置参数(四)
位置参数的传递 前面我们已经分析了无参函数的调用过程,我们来看看Python是如何来实现带参函数的调用的.其实,基本的调用流程与无参函数一样,而不同的是,在调用带参函数时,Python虚拟机必须传递参 ...
- Python虚拟机函数机制之无参调用(一)
PyFunctionObject对象 在Python中,任何一个东西都是对象,函数也不例外.函数这种抽象机制,是通过一个Python对象——PyFunctionObject来实现的 typedef s ...
- Python虚拟机类机制之instance对象(六)
instance对象中的__dict__ 在Python虚拟机类机制之从class对象到instance对象(五)这一章中最后的属性访问算法中,我们看到“a.__dict__”这样的形式. # 首先寻 ...
- Python虚拟机类机制之绑定方法和非绑定方法(七)
Bound Method和Unbound Method 在Python中,当对作为属性的函数进行引用时,会有两种形式,一种称为Bound Method,这种形式是通过类的实例对象进行属性引用,而另一种 ...
随机推荐
- Error: Trying to open unclosed connection.
错误:试图打开未关闭的连接.在我自己写model文件的时候,重复打开了连接数据库的操作,所以报这种错误. 错误实例: 两个model文件: userModel.js var mongoose = re ...
- 30个提高Web程序执行效率的好经验
尽量避免使用DOM.当需要反复使用DOM时,先把对DOM的引用存到JavaScript本地变量里再使用.使用设置innerHTML的方法来替换document.createElement/append ...
- VS2010/OpenGL配置
1.下载glut:http://www.opengl.org/resources/libraries/glut/glutdlls37beta.zip 2.把解压得到的glut.h放到"C:\ ...
- UIScrollView使用stoboard自动布局
使用stoboard布局 scrollView 是有点麻烦的,首先我们往往约束好一个 scrollView 然后在添加子控件,此时都会报错,原因是, scrollView必须确定滚动范围 然后在使用V ...
- Java语法基础(2)
一.变量与常量 1.标识符与关键字 (1). 标识符 标识符可以简单的理解为一个名字,用来标识类名.变量名.方法名.数组名.文件名的有效字符序列.如图所示. Java语言规定标识符由任意顺序的字母.下 ...
- Get和Post的初步探究
Get请求和Post请求这两种基本请求类型,大部分开发者心里大概都有所谓的"标准答案",但博主最近用Postman测试接口的时候,遇到传参的问题:用post请求,参数放在reque ...
- 邮箱/邮件地址的正则表达式及分析(JavaScript,email,regex)
简言 在做用户注册时,常会用到邮箱/邮件地址的正则表达式.本文列举了几种方案,大家可以根据自己的项目情况,选择最适合的方案. 方案1 (常用) 规则定义如下: 以大写字母[A-Z].小写字母[a-z] ...
- 本号讯 | 微软和百度携手推进全球自动驾驶技术; 微软发布新一代可垂直可水平滚动的Arc鼠标
7 月 13 日,微软宣布了与宝马的最新合作进展,继语音助手 Cortana .云服务 Azure.Office 365 和微软 Exchange 安装在部分宝马车型后——Skype for Busi ...
- HDU 3681 Prison Break 越狱(状压DP,变形)
题意: 给一个n*m的矩阵,每个格子中有一个大写字母,一个机器人从‘F’出发,拾取所有的开关‘Y’时便能够越狱,但是每走一格需要花费1点能量,部分格子为充电站‘G’,每个电站只能充1次电.而且部分格子 ...
- SAP Cloud for Customer的Account Team里的role如何配置
Account Team标签页里点击Add按钮: 这些下拉菜单里的role在哪里配置? 在business configuration工作中心:Implementation projects-> ...