位置参数的传递

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

# cat demo2.py
def f(name, age):
age += 5
print("[", name, age, "]") age = 5
f("Robert", age)

  

我们用dis模块来编译一下对应的字节码:

# python2.5
……
>>> source = open("demo2.py").read()
>>> co = compile(source, "demo2.py", "exec")
>>> import dis
>>> dis.dis(co)
1 0 LOAD_CONST 0 (<code object f at 0x7ff044b1c648, file "demo2.py", line 1>)
3 MAKE_FUNCTION 0
6 STORE_NAME 0 (f) 6 9 LOAD_CONST 1 (5)
12 STORE_NAME 1 (age) 7 15 LOAD_NAME 0 (f)
18 LOAD_CONST 2 ('Robert')
21 LOAD_NAME 1 (age)
24 CALL_FUNCTION 2
27 POP_TOP
28 LOAD_CONST 3 (None)
31 RETURN_VALUE

  

再用dis模块编译下函数f对应的字节码:

>>> from demo2 import f
>>> dis.dis(f)
2 0 LOAD_FAST 1 (age)
3 LOAD_CONST 1 (5)
6 INPLACE_ADD
7 STORE_FAST 1 (age) 3 10 LOAD_CONST 2 ('[')
13 LOAD_FAST 0 (name)
16 LOAD_FAST 1 (age)
19 LOAD_CONST 3 (']')
22 BUILD_TUPLE 4
25 PRINT_ITEM
26 PRINT_NEWLINE
27 LOAD_CONST 0 (None)
30 RETURN_VALUE

  

在编译后的demo2.py中,CALL_FUNCTION指令之前,有三条LOAD指令,这三条LOAD指令执行完成后的运行时栈如图1-1所示:

图1-1   CALL_FUNCTION指令执行前的运行时栈

可以看到,函数需要的参数已经被压到运行时栈中了。接下来执行CALL_FUNCTION指令,其指令参数为2

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;
……
}

  

前面我们提到,CALL_FUNCTION的指令参数oparg中,低字节包含了位置参数的个数,所谓位置参数,就是如f中所见的一般函数。而oparg中高字节包含另一种参数的个数。因此na=2,nk=2,所以n=2。从栈顶指针pp_stack开始,回退2后,PyObject *func正确地指向了运行时栈中存储的那个代表着f的PyFunctionObject对象。然后,程序进入fast_function

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);
PyObject *argdefs = PyFunction_GET_DEFAULTS(func);
PyObject **d = NULL;
int nd = 0; ……
if (argdefs == NULL && co->co_argcount == n && nk == 0 &&
co->co_flags == (CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE))
{
PyFrameObject *f;
PyObject *retval = NULL;
PyThreadState *tstate = PyThreadState_GET();
PyObject **fastlocals, **stack;
int i; PCALL(PCALL_FASTER_FUNCTION);
assert(globals != NULL);
//[1]:创建与函数对应的PyFrameObject对象
f = PyFrame_New(tstate, co, globals, NULL);
//[2]:拷贝函数参数:从运行时栈到PyFrameObject.f_localsplus
fastlocals = f->f_localsplus;
stack = (*pp_stack) - n; for (i = 0; i < n; i++)
{
Py_INCREF(*stack);
fastlocals[i] = *stack++;
}
……
}
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));
}

  

在上述代码的[1]处创建了函数f对应的PyFrameObject对象,在这个过程中,函数f对应的PyFunctionObject对象中保存的PyCodeObject对象被传递给新创建的PyFrameObject对象。随后,在代码[2]处,Python虚拟机将参数逐个拷贝到新创建的PyFrameObject对象的f_localsplus中。f_localsplus所指向的内存块包含着Python虚拟机所使用的那个运行时栈,那么参数所占用的内存空间与运行时栈所占用的内存空间关系又是怎样的呢?答案就在PyFrame_New中

frameobject.c

PyFrameObject *
PyFrame_New(PyThreadState *tstate, PyCodeObject *code, PyObject *globals,
PyObject *locals)
{
PyFrameObject *back = tstate->frame;
PyFrameObject *f;
Py_ssize_t i; ……
if (code->co_zombieframe != NULL) {
……
}
else {
Py_ssize_t extras, ncells, nfrees;
ncells = PyTuple_GET_SIZE(code->co_cellvars);
nfrees = PyTuple_GET_SIZE(code->co_freevars);
extras = code->co_stacksize + code->co_nlocals + ncells +
nfrees;
if (free_list == NULL) {
//[1]:为f_localsplus申请extras的内存空间
f = PyObject_GC_NewVar(PyFrameObject, &PyFrame_Type,
extras);
if (f == NULL) {
Py_DECREF(builtins);
return NULL;
}
}
…… f->f_code = code;
//[2]:获得f_localsplus中除去运行时栈外,剩余的内存数
extras = code->co_nlocals + ncells + nfrees;
f->f_valuestack = f->f_localsplus + extras;
for (i=0; i<extras; i++)
f->f_localsplus[i] = NULL;
……
}
f->f_stacktop = f->f_valuestack;
……
return f;
}

  

在函数对应的PyCodeObject对象的co_nlocals域中,包含着函数的参数的个数,因为函数参数也是局部符号的一种。在上述代码[2]处,从f_localsplus开始,计算出的extras的那段内存中,一定有供函数参数使用的内存。换一种说法,函数参数存放在运行时栈之前的那片内存中

从PyFrame_New创建PyFrameObject对象的过程中可以看到,在f_localsplus中,用于存储函数参数的空间和运行时栈的空间逻辑上时分离的,并不是共享同一片内存,尽管这两块连续内存所指向的对象相同,但它们界限分明,井水不犯河水

处理完参数后,还没有进入PyEval_EvalFrameEx,所以这时运行时栈还是空的。但是,函数已就位于f_localsplus中。这时,新建的PyFrameObject对象的f_localsplus域如图1-1所示:

图1-1   进入PyEval_EvalFrameEx之前新建PyFrameObject对象的内存布局

位置参数的访问

当参数拷贝的动作完成后,就会进入新的PyEval_EvalFrameEx,开始真正的函数f的调用工作

def f(name, age):
age += 5
//字节码指令
0 LOAD_FAST 1 (age)
3 LOAD_CONST 1 (5)
6 INPLACE_ADD
7 STORE_FAST 1 (age)
print("[", name, age, "]")
……

  

age += 5所编译后的指令序列中,有两条是我们从来没有剖析过的,一条是LOAD_FAST,另一条是STORE_FAST,而正是这两条指令,完成函数参数的读写

ceval.c

PyObject * PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
{
register PyObject **fastlocals;
#define GETLOCAL(i) (fastlocals[i])
fastlocals = f->f_localsplus;
…… } #define GETLOCAL(i) (fastlocals[i])
case LOAD_FAST:
x = GETLOCAL(oparg);
if (x != NULL)
{
Py_INCREF(x);
PUSH(x);
goto fast_next_opcode;
}
format_exc_check_arg(PyExc_UnboundLocalError,
UNBOUNDLOCAL_ERROR_MSG,
PyTuple_GetItem(co->co_varnames, oparg));
break; #define SETLOCAL(i, value) \
do \
{ \
PyObject *tmp = GETLOCAL(i); \
GETLOCAL(i) = value; \
Py_XDECREF(tmp); \
} while (0)
case STORE_FAST:
v = POP();
SETLOCAL(oparg, v);
goto fast_next_opcode; PREDICTED(POP_TOP);

  

原来,LOAD_FAST和STORE_FAST这一对指令是以f_localsplus这片内存为操作目标的。指令"0   LOAD_FAST   1"的结果是将f_localsplus[1]中的对象压入到运行时栈中,而从图1-1中我们可以看到f_localsplus[1]存放的正是age。在完成加法操作后,又通过STORE_FAST将结果放入到f_localsplus[1]中,这样就实现了对变量age的更新,以后再print访问age参数时,得到的结果就是10了

关于Python中的函数的位置参数,我们对它在函数调用过程中是如何传递,在函数执行过程又是如何被访问和修改已经明白。在调用函数时,Python将函数参数从左至右压入到运行时栈中,在fast_function中,又将这些参数拷贝到新建的与函数对应的PyFrameObject对象的f_localsplus中。最终的效果是,Python虚拟机将函数调用时传入的参数,从左至右存放在新建的PyFrameObject对象的f_localsplus中

在访问函数参数时,Python虚拟机没有按照通常访问符号的做法,去访问名字空间,而是通过一个索引(偏移位置)来访问f_localsplus中存储的符号对应的对象。这种通过索引(偏移位置)进行访问的方法也正是“位置参数”名称的由来

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

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

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

  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虚拟机类机制之绑定方法和非绑定方法(七)

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

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

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

  9. Python虚拟机类机制之从class对象到instance对象(五)

    从class对象到instance对象 现在,我们来看看如何通过class对象,创建instance对象 demo1.py class A(object): name = "Python&q ...

随机推荐

  1. 关于锚点页内链接跳转出现问题(不响应,没有反应)的解决方法(ZT)

    我们知道,利用锚点可以实现页面链接跳转,也可以实现同一页面内的跳转功能. 例如:<a href="somepage.htm>某页面链接</a>  可以跳转链接到som ...

  2. 洛谷 P1969 积木大赛

    题目描述 春春幼儿园举办了一年一度的“积木大赛”.今年比赛的内容是搭建一座宽度为n的大厦,大厦可以看成由n块宽度为1的积木组成,第i块积木的最终高度需要是hi. 在搭建开始之前,没有任何积木(可以看成 ...

  3. JAVA基础之线程

    个人理解: 在相同的进程也就是运行同样的程序的前提下,线程越多效率越快!当然硬件也是个障碍!为了提高效率,可以多创建线程,但是也不是越多越好,这就需要了线程池进行管理!需要知道的线程实现的方法:继承T ...

  4. 缓存List并写入文件持久化

    LIfe is half spent before we know what is it. 缓存List并写入文件持久化 需要缓存一个List集合,比如缓存一个输入框中用户之前输入过的内容,下次当用户 ...

  5. Myeclipse发布第一个jsp页面及web project部署到tomcat上的几种方法

    菜鸟日记: 1:new web project: 2:fix the visiting  path of the tomcat,打开在安装目录下conf目录中的server.xml,在</Hos ...

  6. (三)我的JavaScript系列:不同调用方式的this指向

    人生自是有情痴,此恨不关风与月 今天所写的内容,是对之前的内容的总结和扩展.老实说,对于自己之前的一些杜撰和臆测,我并不是很满意.所以这篇博文,我希望能来点干货. 不同调用方式的this指向 在Jav ...

  7. 唱吧APP产品体验报告

  8. 常用的ement语法

    缩写语法: 介绍:Emmet 使用类似于 CSS 选择器的语法描述元素在生成的文档树中的位置及其属性. 声明:第一次写博客大家多多关照,如有错误或者需要补充的请到评论里留言,谢谢大家! 快速生成htm ...

  9. springboot超详细笔记

    一.Spring Boot 入门 1.Spring Boot 简介 简化Spring应用开发的一个框架: 整个Spring技术栈的一个大整合: J2EE开发的一站式解决方案: 2.微服务 2014,m ...

  10. JavaScript -- 条件语句和循环语句

    if语句 在我们开发程序的时候,经常会遇到选择题,例如,年龄大于18,你就可以抽烟喝酒烫头,年龄小于18,你就只能吃饭喝水.在我们的代码中,我们可以用if语句来实现这种判断 语法一: if( cond ...