位置参数的传递

前面我们已经分析了无参函数的调用过程,我们来看看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. angular2 基于webpack环境搭建

    目录结构: angular-quickstart |_ ts |_ app.ts |_ index.ts |_ index.html |_ package.json |_ tsconfig.json ...

  2. hystrix 应用问题

    1.问题总结, 如果项目中使用了ThreadLocal,注意hystix创建新线程时,ThreadLocal中存的是之前线程中的数据,在hystix线程中获取不到 2.问题 throwable异常参数 ...

  3. Java并发(二):基础概念

    并发编程的第二部分,先来谈谈发布(Publish)与逸出(Escape); 发布是指:对象能够在当前作用域之外的代码中使用,例如:将对象的引用传递到其他类的方法中,对象的引用保存在其他类可以访问的地方 ...

  4. linux touch和vi建立的文件是什么文件类型的

    都是acsii类型的文本文档,但是也可以建立其他格式的,比如vi newFile.c(c是c语言动态链接库格式)

  5. 造成socket.error: [Errno 99] Cannot assign requested

    socket.error: [Errno 99] Cannot assign requested address 网上你去搜,基本都是说bind的时候,地址已经被用了,都是胡扯.地址被用报的错误应该是 ...

  6. C# List的使用

    1.所需引入的命名空间: using System.Collections.Generic; 2.初始化 [1]空: List<int> list = new List<int> ...

  7. Redis哨兵原理详解

    一.概述 Redis哨兵(以下称哨兵)是为Redis提供一个高可靠解决方案,对一定程序上的错误,可以不需要人工干预自行解决. 哨兵功能还有监视.事件通知.配置功能.以下是哨兵的功能列表: 监控:不间断 ...

  8. 对比java和python对比

    对比java和python 对比java和python 2011年04月18日 1.难易度而言.python远远简单于java. 2.开发速度.Python远优于java 3.运行速度.java远优于 ...

  9. UIView Border color

    // // UIView+Borders.h // // Created by Aaron Ng on 12/28/13. // Copyright (c) 2013 Delve. All right ...

  10. 关于“为何Unicode中文字符占取2个字节,而 UTF-8却占3个字节”的网络解释修正

    学到编码时,有个疑问——好好的占2字节的Unicode不用,却要用占3字节的UTF-8编码.发明 UTF-8的初衷不就是为了修正Unicode中任何字符至少占用2个字节的弊端吗? 虽然UTF-8英文字 ...