参数类别

我们在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. Redis set(集合)

    Redis 的 Set 是 String 类型的无序集合,元素不允许重复. Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1). 集合中最大的元素数为 232 - 1 ( ...

  2. 切记切记:Spring配置文件中,Component-scan无法扫描到的类中的自动装配对象无法被调用,报空指针错误。

    Spring单例注入,单例对象可设置成Spring元件. 只有Spring的元件中@Autowired才有用,在普通类中@Autowired虽然不会编译报错,但运行时会报空指针错误.

  3. css 02

    Css 02 Url  ./  http://www. Src  引入 拿取过来内容 Href 引用 连接前往 a  link 现在所有的命名 请按照下面我说的去命名 可以使用字母 数字 下划线组成  ...

  4. 第二章 你第首个Electron应用 | Electron in Action(中译)

    本章主要内容 构建并启动Electron应用 生成package.json,配置成Electron应用 在你的项目中包含预先构建Electron版本 配置package.json以启动主进程 从主进程 ...

  5. malloc/free函数

    一.malloc 函数原型:void *malloc(unsigned int size); 功       能:在内存的动态存储区中分配一个长度为size的连续空间. 返  回 值:指向所分配的连续 ...

  6. vuex的state,mutation,getter,action

    开始!正常的简单的拆分下是这样的文件当然module可以在store下面新建一个文件夹用来处理单独模块的vuex管理比较合适. 1.index.js下面 import Vue from 'vue' i ...

  7. javaSe-线程2

    package com.java.chap09.sec02; public class Thread3 implements Runnable{ private int baoZi=1; privat ...

  8. POJ 3252 Round Numbers (区间DP,基础)

    题意: 统计区间[L,R]有多少个数,其二进制表示法中的0的个数不少于1的个数?(不允许前缀0) 思路: 状态表示为 [当前第几位][总位数][1的个数],最后判断一下1的个数是否满足条件,要注意前导 ...

  9. HDU 2188 悼念512汶川大地震遇难同胞——选拔志愿者(巴什博弈)

    思路:若能给对方留下m+1,就可以胜.否则败. #include <iostream> using namespace std; int main() { int t,n,m;cin> ...

  10. coredata 关系的删除规则

    http://blog.csdn.net/Hello_Hwc/article/details/46375517 关系的删除规则-Delete Rule Deny 关系的destination中只要有一 ...