起步
python中, 任何东西都是一个对象, 函数也不例外, 函数对象结构 PyFunctionObject
:
[functionobject.h]
typedef struct {
PyObject_HEAD
PyObject *func_code; // 编译后的 PyCodeObject 对象
PyObject *func_globals; // 运行时的global名字空间
PyObject *func_defaults; // 默认参数(tuple or null)
PyObject *func_kwdefaults; // 带声明的参数默认值 dict or null
PyObject *func_closure; // 闭包函数集合, null or tuple
PyObject *func_doc; // 函数的文档 PyStringObject
PyObject *func_name; // 函数名, __name__ 属性, 是PyStringObject对象
PyObject *func_dict; // 函数的 __dict__ 属性, PyDictObject 或 null
PyObject *func_weakreflist;
PyObject *func_module; // 函数的__module__ ,可以是任意对象
PyObject *func_annotations; // 注释, null or dict
PyObject *func_qualname; /* The qualified name */
} PyFunctionObject;
在python中, 有两个对象都和函数有关, PyCodeObject和PyFunctionObject, 这两个对象的区别是, PyCodeObject对象是一段python代码的静态表示, 对一个code block会产生一个且只有一个PyCodeObject. 而PyFunctionObject则不同, PyFunctionObject是python代码运行时动态产生的, 它是在执行一个 def
语句时候创建的. 在PyFunctionObject中, 也会包含函数的静态信息, 这些信息保存在 func_code
中, 这个成员指向的是函数代码对应的PyCodeObject对象, 此外, PyFunctionObject还包含一些函数在执行时必要的动态信息, 即上下文, 如 func_globals, 就表明函数执行是关联了global名字空间. global 作用域中的符号和变量值必须在运行时才确定.
函数对象的创建
以无参函数调用入手, 因为不用去考虑复杂的参数传递机制的细节, 而将关注点放在函数调用的流程框架上:
def f():
print("this is a function")
f()
其编译的code和字节码如下:
co_consts : (<code object f at 0x000001C5F7EFE660, file "compile.py", line 1>, 'f', None)
co_names : ('f',)
co_stacksize : 2
1 0 LOAD_CONST 0 (<code object f at 0x000001C5F7EFE660, file "compile.py", line 1>)
2 LOAD_CONST 1 ('f')
4 MAKE_FUNCTION 0
6 STORE_NAME 0 (f)
3 8 LOAD_NAME 0 (f)
10 CALL_FUNCTION 0
12 POP_TOP
14 LOAD_CONST 2 (None)
16 RETURN_VALUE
这段代码的编译共产生了两个PyCodeObject对象, 一个是这个文件, 另一个是函数f():
TARGET(MAKE_FUNCTION) {
PyObject *qualname = POP(); // 函数的名字
PyObject *codeobj = POP(); // 函数对应的PyCodeObject对象
PyFunctionObject *func = (PyFunctionObject *)
PyFunction_NewWithQualName(codeobj, f->f_globals, qualname);
Py_DECREF(codeobj);
Py_DECREF(qualname);
// 处理参数
...
PUSH((PyObject *)func);
DISPATCH();
}
在创建函数指令之前, 先通过LOAD_CONST将函数f对应的PyCodeObject对象压入运行时栈中. 然后以该对象和当前的PyFrameObject中的global名字空间, 通过 PyFunction_NewWithQualName
创建一个新的PyFunctionObject对象:
PyObject * PyFunction_NewWithQualName(PyObject *code, PyObject *globals, PyObject *qualname)
{
PyFunctionObject *op;
PyObject *doc, *consts, *module;
static PyObject *__name__ = PyUnicode_InternFromString("__name__");
// 创建PyFunctionObject对象
op = PyObject_GC_New(PyFunctionObject, &PyFunction_Type);
if (op == NULL)
return NULL;
op->func_weakreflist = NULL;
Py_INCREF(code);
op->func_code = code; // 设置PyCodeObject对象
Py_INCREF(globals);
op->func_globals = globals; // 设置名字空间
op->func_name = ((PyCodeObject *)code)->co_name; // 设置函数名
Py_INCREF(op->func_name);
op->func_defaults = NULL; /* No default arguments */
op->func_kwdefaults = NULL; /* No keyword only defaults */
op->func_closure = NULL;
consts = ((PyCodeObject *)code)->co_consts; // 函数中的常量对象表
if (PyTuple_Size(consts) >= 1) {
doc = PyTuple_GetItem(consts, 0);
if (!PyUnicode_Check(doc))
doc = Py_None;
}
else
doc = Py_None;
Py_INCREF(doc);
op->func_doc = doc; // 函数中的文档
...
_PyObject_GC_TRACK(op);
return (PyObject *)op;
}
在创建PyFunctionObject对象之后, MAKE_FUNCTION还会进行一些处理函数的参数动作, MAKE_FUNCTION指令中, 新建的function对象通过PUSH操作压入到运行时栈中.
函数调用
函数调用从 CALL_FUNCTION
指令开始, 前面在介绍了 print
信息输出时也有涉及到函数, 当时没有详细介绍函数机制, 现在重新认识一下函数调用:
TARGET(CALL_FUNCTION) {
PyObject **sp, *res;
sp = stack_pointer;
res = call_function(&sp, oparg, NULL);
stack_pointer = sp;
PUSH(res);
if (res == NULL) {
goto error;
}
DISPATCH();
}
这个指令中, 获得了当前运行时栈的栈顶指针后就调用 call_function
了:
[ceval.c]
Py_LOCAL_INLINE(PyObject *) _Py_HOT_FUNCTION call_function(PyObject ***pp_stack, Py_ssize_t oparg, PyObject *kwnames)
{
PyObject **pfunc = (*pp_stack) - oparg - 1;
PyObject *func = *pfunc; // 获取PyFunctionObject 对象
PyObject *x, *w;
Py_ssize_t nkwargs = (kwnames == NULL) ? 0 : PyTuple_GET_SIZE(kwnames);
Py_ssize_t nargs = oparg - nkwargs;
PyObject **stack = (*pp_stack) - nargs - nkwargs;
/* Always dispatch PyCFunction first, because these are
presumed to be the most frequent callable object.
*/
if (PyCFunction_Check(func)) {
PyThreadState *tstate = PyThreadState_GET();
C_TRACE(x, _PyCFunction_FastCallKeywords(func, stack, nargs, kwnames));
}
else if (Py_TYPE(func) == &PyMethodDescr_Type) {
...
}
else {
...
if (PyFunction_Check(func)) {
x = _PyFunction_FastCallKeywords(func, stack, nargs, kwnames);
}
else {
x = _PyObject_FastCallKeywords(func, stack, nargs, kwnames);
}
Py_DECREF(func);
}
assert((x != NULL) ^ (PyErr_Occurred() != NULL));
/* Clear the stack of the function object. */
while ((*pp_stack) > pfunc) {
w = EXT_POP(*pp_stack);
Py_DECREF(w);
}
return x;
}
其中:
#define PyCFunction_Check(op) (Py_TYPE(op) == &PyCFunction_Type)
#define PyFunction_Check(op) (Py_TYPE(op) == &PyFunction_Type)
显然我们的函数创建对象时用的对象是 PyFunction_Type
, 因此按照if路径走的是 x = _PyFunction_FastCallKeywords(func, stack, nargs, kwnames);
. 分析调用前的变量的值都是啥, pp_stack
是在 CALL_FUNCTION
指令代码中传入的当时运行时栈的栈顶指针, oparg
是0 , 传进来的 kwnames
是NULL. 因此 pfunc = pp_stack - 1
, 所以 *pfunc
就是func, 即在MAKE_FUNCTION中创建的PyFunctionObject对象. 后面的 nkwargs
和 nargs
显然都是0.
[call.c]
PyObject * _PyFunction_FastCallKeywords(PyObject *func, PyObject **stack,
Py_ssize_t nargs, PyObject *kwnames)
{
PyCodeObject *co = (PyCodeObject *)PyFunction_GET_CODE(func); // 获得PyCodeObject对象
PyObject *globals = PyFunction_GET_GLOBALS(func); // 获得globals名字空间
PyObject *argdefs = PyFunction_GET_DEFAULTS(func); // 默认参数
PyObject *kwdefs, *closure, *name, *qualname;
PyObject **d;
Py_ssize_t nkwargs = (kwnames == NULL) ? 0 : PyTuple_GET_SIZE(kwnames);
Py_ssize_t nd;
if (co->co_kwonlyargcount == 0 && nkwargs == 0 &&
co->co_flags == (CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE))
{
if (argdefs == NULL && co->co_argcount == nargs) {
return function_code_fastcall(co, stack, nargs, globals);
}
else if (nargs == 0 && argdefs != NULL && co->co_argcount == PyTuple_GET_SIZE(argdefs)) {
return function_code_fastcall(co, stack, PyTuple_GET_SIZE(argdefs),
globals);
}
}
...
return _PyEval_EvalCodeWithName((PyObject*)co, globals, (PyObject *)NULL,
stack, nargs,
nkwargs ? &PyTuple_GET_ITEM(kwnames, 0) : NULL,
stack + nargs,
nkwargs, 1,
d, (int)nd, kwdefs,
closure, name, qualname);
}
这段代码中, 包含了两条执行路径, 无参和有参的情况, f函数是无参, 显然执行了 return function_code_fastcall(co, stack, nargs, globals);
.:
static PyObject* _Py_HOT_FUNCTION function_code_fastcall(PyCodeObject *co, PyObject **args, Py_ssize_t nargs,
PyObject *globals)
{
PyFrameObject *f;
PyThreadState *tstate = PyThreadState_GET();
PyObject **fastlocals;
Py_ssize_t i;
PyObject *result;
// 创建新的PyFrameObject
f = _PyFrame_New_NoTrack(tstate, co, globals, NULL);
if (f == NULL) {
return NULL;
}
fastlocals = f->f_localsplus;
// 处理参数
for (i = 0; i < nargs; i++) {
Py_INCREF(*args);
fastlocals[i] = *args++;
}
result = PyEval_EvalFrameEx(f,0);
...
return result;
}
使用code对象和当前线程创建了一个新的PyFrameObject, 然后 PyEval_EvalFrameEx
执行栈帧, 也就是真正进入了所谓的函数调用状态, 在新的栈帧中执行代码,.
函数执行时的名字空间
在创建PyFunctionObject时,有一个参数globals, 这个globals最终成为函数f对象的PyFrameObject中global名字空间-- f_globals. 在那时说明 LOAD_NAME
指令时, 搜索顺序是:f_locals, f_globals, f_builtins. 这意味着, 当前的PyFrameObject对象中的与新建的PyFrameObject对象的global名字空间是同一个.
函数参数的实现
python中参数的形式很多样, 可以分为四种类型:
- 位置参数: fun(a, b), a, b是位置参数
- 键参数: fun(a, b, name='python'), 其中name就是键参数
- 扩展位置参数: fun(a, b, *lst), 其中lst是扩展位置参数
- 扩展键参数: fun(a, b, **kw), 其中kw是扩展键参数
扩展位置参数是允许在函数调用时调用参数的数量个数可变. 参数的处理过程需要回顾一下 call_function
:
[ceval.c]
Py_LOCAL_INLINE(PyObject *) _Py_HOT_FUNCTION call_function(PyObject ***pp_stack, Py_ssize_t oparg, PyObject *kwnames)
{
PyObject **pfunc = (*pp_stack) - oparg - 1;
PyObject *func = *pfunc; // 获得PyFunctionObject对象
PyObject *x, *w;
Py_ssize_t nkwargs = (kwnames == NULL) ? 0 : PyTuple_GET_SIZE(kwnames);// 键参数个数
Py_ssize_t nargs = oparg - nkwargs; // 位置参数个数
PyObject **stack = (*pp_stack) - nargs - nkwargs;
...
}
虚拟机执行 CALL_FUNCTION
指令时, 首先获得一个指令参数oparg, 这个参数中表示了调用函数时需要的函数参数个数信息. python存在四种参数形式, 而则oparg只是个数字, 显然不能详细表示四种参数具体信息, 实际上它表示了一共有多少个参数.
python的每个指令都是占用两个字节, 第一个字节存指令, 第二字节存放参数个数, 这意味这oparg值最多255, 也就是说python中函数参数个数最多255个.
def f(a, b):
print(a)
f(1, b=2)
函数调用时用:
8 LOAD_NAME 0 (f)
10 LOAD_CONST 2 (1)
12 LOAD_CONST 3 (2)
14 LOAD_CONST 4 (('b',))
16 CALL_FUNCTION_KW 2
当有键参数时, 调用了 CALL_FUNCTION_KW
这个实现和 CALL_FUNCTION
差不多:
TARGET(CALL_FUNCTION_KW) {
PyObject **sp, *res, *names;
names = POP();
assert(PyTuple_CheckExact(names) && PyTuple_GET_SIZE(names) <= oparg);
sp = stack_pointer;
res = call_function(&sp, oparg, names);
stack_pointer = sp;
PUSH(res);
...
DISPATCH();
}
只是在 call_function
中多个一个 names
参数, 在CALL_FUNCTION
传的是NULL. 如果我们调用是 f(1, 2)
那么 (nkwargs, nargs)
的值就是 (0, 2), 而 f(1, b=2)
则是(1, 1). 可见 nkwargs, nargs 能反应真实位置参数和键参数的个数. 键参数是个tuple类型, 因此 oparg-len(nkwargs) 就是nargs的个数了.
位置参数的传递
调用f(1, 2) 是需要将1和2分别赋值给a和b的. 先不分析键参数, 来看看f函数对应的指令:
2 0 LOAD_GLOBAL 0 (print)
2 LOAD_FAST 0 (a)
4 CALL_FUNCTION 1
6 POP_TOP
3 8 LOAD_GLOBAL 0 (print)
10 LOAD_FAST 1 (b)
12 CALL_FUNCTION 1
14 POP_TOP
16 LOAD_CONST 0 (None)
18 RETURN_VALUE
参数的使用上用的是 LOAD_FAST
:
TARGET(LOAD_FAST) {
PyObject *value = GETLOCAL(oparg);
Py_INCREF(value);
PUSH(value);
FAST_DISPATCH();
}
这个命令显然和 LOAD_NAME
区别仅仅是, LOAD_FAST只从locals名字空间获取, 而LOAD_NAME则会根据LGB规则顺序获取. 那么这些参数是怎么进入locals名字空间的呢, 也就是说在 call_function
的时候, 参数列表应该注入到f的栈帧对象的locals中:
static PyObject* _Py_HOT_FUNCTION function_code_fastcall(PyCodeObject *co, PyObject **args, Py_ssize_t nargs,
PyObject *globals)
{
...
// 创建新的PyFrameObject
f = _PyFrame_New_NoTrack(tstate, co, globals, NULL);
fastlocals = f->f_localsplus;
// 处理参数
for (i = 0; i < nargs; i++) {
fastlocals[i] = *args++;
}
...
}
显而易见了.
位置参数的默认值
如果函数有默认值, 如:
def f(a=1, b=2):
print(a)
print(b)
f()
多余的指令就不贴了, 默认参数的处理在创建PyFunctionObject对象时候处理的:
0 LOAD_CONST 5 ((1, 2))
2 LOAD_CONST 2 (<code object f at 0x000001A51BCDE6F0, file "compile.py", line 1>)
4 LOAD_CONST 3 ('f')
6 MAKE_FUNCTION 1
在创建函数对象时, 默认参数值以tuple形式入栈. 回头再看看 MAKE_FUNCTION
命令:
TARGET(MAKE_FUNCTION) {
PyObject *qualname = POP();
PyObject *codeobj = POP();
PyFunctionObject *func = (PyFunctionObject *) PyFunction_NewWithQualName(codeobj, f->f_globals, qualname);
// 处理参数
if (oparg & 0x08) {
assert(PyTuple_CheckExact(TOP()));
func ->func_closure = POP();
}
if (oparg & 0x04) {
assert(PyDict_CheckExact(TOP()));
func->func_annotations = POP();
}
if (oparg & 0x02) { // 键参数
assert(PyDict_CheckExact(TOP()));
func->func_kwdefaults = POP();
}
if (oparg & 0x01) { // 位置参数
assert(PyTuple_CheckExact(TOP()));
func->func_defaults = POP();
}
PUSH((PyObject *)func);
DISPATCH();
}
默认参数存在了 func->func_defaults
中. 如果在CALL_FUNCTION指令中, 发现传递的位置参数个数小于fun对象中的参数, 那python需要为函数设定默认参数:
[ceval.c]
PyObject * _PyEval_EvalCodeWithName(/* 省略参数 */) {
...
// 传递的参数个数小于fun对象定义的参数个数时
if (argcount < co->co_argcount) {
Py_ssize_t m = co->co_argcount - defcount;
Py_ssize_t missing = 0;
if (n > m)
i = n - m;
else
i = 0;
for (; i < defcount; i++) {
// 赋值默认值
if (GETLOCAL(m+i) == NULL) {
PyObject *def = defs[i];
Py_INCREF(def);
SETLOCAL(m+i, def);
}
}
}
...
}
这段代码的作用也就是如果函数申明是def f(a=1, b=2)
而调用时 f(3)
那么就把2赋值给locals空间的b中.
扩展位置参数和扩展键参数
前面已经有看到扩展位置参数其实是个tuple, 因此可以猜想扩展键参数其实就是个dict对象.如果我们声明一个函数如下:
def f(value, *lst, **keys):
pass
f(-1, 1, 2, 3, a=4, b=5)
对应的code和指令为:
co_consts : (<code object f at 0x00000238A685E6F0, file "compile.py", line 1>, 'f', 1, 2, 3, 4, 5, ('a', 'b'), None, -1)
co_names : ('f',)
co_nlocals : 0
co_stacksize : 8
co_varnames : ()
1 0 LOAD_CONST 0 (<code object f at 0x00000238A685E6F0, file "compile.py", line 1>)
2 LOAD_CONST 1 ('f')
4 MAKE_FUNCTION 0
6 STORE_NAME 0 (f)
3 8 LOAD_NAME 0 (f)
10 LOAD_CONST 9 (-1)
12 LOAD_CONST 2 (1)
14 LOAD_CONST 3 (2)
16 LOAD_CONST 4 (3)
18 LOAD_CONST 5 (4)
20 LOAD_CONST 6 (5)
22 LOAD_CONST 7 (('a', 'b'))
24 CALL_FUNCTION_KW 6
26 POP_TOP
28 LOAD_CONST 8 (None)
30 RETURN_VALUE
当含有键参数时采用了 CALL_FUNCTION_KW
指令, 前面说了, 这个指令和CALL_FUNCTION
就是多了传了键参数的键组成了tuple.
PyObject * _PyEval_EvalCodeWithName(/* 省略参数 */) {
...
// 先设置键参数默认值, 这是在函数声明时用的默认值
if (co->co_kwonlyargcount > 0) {
Py_ssize_t missing = 0;
for (i = co->co_argcount; i < total_args; i++) {
PyObject *name;
if (GETLOCAL(i) != NULL)
continue;
name = PyTuple_GET_ITEM(co->co_varnames, i);
if (kwdefs != NULL) {
PyObject *def = PyDict_GetItem(kwdefs, name);
if (def) {
Py_INCREF(def);
SETLOCAL(i, def);
continue;
}
}
missing++;
}
}
// 处理扩展参数位置
kwcount *= kwstep;
for (i = 0; i < kwcount; i += kwstep) {
PyObject **co_varnames;
PyObject *keyword = kwnames[i];
PyObject *value = kwargs[i];
Py_ssize_t j;
...
if (PyDict_SetItem(kwdict, keyword, value) == -1) {
goto fail;
}
continue;
}
...
}
python在编译一个函数时, 如果在其形式参数中有*lst
这样的扩展位置参数形式或 **keys
时, 那么python会在编译得到的PyCodeObject对象中co_flags中添加标识.