楔子
上一篇文章我们说了 Python 函数的底层实现,并且还演示了如何通过函数的类型对象自定义一个函数,以及如何获取函数的参数。虽然这在工作中没有太大意义,但是可以让我们深刻理解函数的行为。
那么接下来看看函数是如何调用的。
PyCFunctionObject
在介绍调用之前,我们需要补充一个知识点。
def foo():
pass
class A:
def foo(self):
pass
print(type(foo)) # <class 'function'>
print(type(A().foo)) # <class 'method'>
print(type(sum)) # <class 'builtin_function_or_method'>
print(type("".join)) # <class 'builtin_function_or_method'>
如果采用 Python 实现,那么函数的类型是 function,方法的类型是 method。而如果采用原生的 C 实现,那么函数和方法的类型都是 builtin_function_or_method。
关于方法,等我们介绍类的时候再说,先来看看函数。
所以函数分为两种:
Python 实现的函数,在底层由 PyFunctionObject 结构体实例表示,其类型对象 <class 'function'> 在底层由 PyFunction_Type 表示。
C 实现的函数(还有方法),在底层由 PyCFunctionObject 结构体实例表示,其类型对象 <class 'builtin_function_or_method'> 在底层由 PyCFunction_Type 表示。
像我们使用 def 关键字定义的就是 Python 实现的函数,而内置函数则是 C 实现的函数,它们在底层对应不同的结构,因为 C 实现的函数可以有更快的执行方式。
函数的调用
我们来调用一个函数,看看它的字节码是怎样的。
import dis
code_string = """
def foo(a, b):
return a + b
foo(1, 2)
"""
dis.dis(compile(code_string, "<file>", "exec"))
字节码指令如下:
0 RESUME 0
# 加载 PyCodeObject 对象,压入运行时栈
2 LOAD_CONST 0 (<code object foo at 0x7f...>)
# 从栈顶弹出 PyCodeObject 对象,构建函数
4 MAKE_FUNCTION 0
# 将符号 foo 和函数对象绑定起来,存储在名字空间中
6 STORE_NAME 0 (foo)
8 PUSH_NULL
# 加载全局变量 foo,压入运行时栈
10 LOAD_NAME 0 (foo)
# 加载常量 1,压入运行时栈
12 LOAD_CONST 1 (1)
# 加载常量 2,压入运行时栈
14 LOAD_CONST 2 (2)
# 弹出 foo 和参数,进行调用
# 指令参数 2,表示给调用的函数传递了两个参数
# 函数调用结束后,将返回值压入栈中
16 CALL 2
# 因为没有用变量保存,所以从栈顶弹出返回值并丢弃
24 POP_TOP
# 隐式的 return None
26 RETURN_CONST 3 (None)
# 函数内部逻辑对应的字节码,比较简单,就不说了
Disassembly of <code object foo at 0x7f6...>:
0 RESUME 0
2 LOAD_FAST 0 (a)
4 LOAD_FAST 1 (b)
6 BINARY_OP 0 (+)
10 RETURN_VALUE
我们看到函数调用使用的是 CALL 指令,那么这个指令都做了哪些事情呢?
TARGET(CALL) {
// ...
// 运行时栈从栈底到栈顶的元素分别是:NULL, 函数, 参数1, 参数 2, ...
// 至于为啥会有一个 NULL,我们再看一下刚才的字节码指令就明白了
// 在 LOAD_NAME 将函数对象的指针压入运行时栈之前,先执行了 PUSH_NULL
// 所以栈底元素是 NULL,不过问题又来了,为啥要往栈里面压入一个 NULL 呢
// PUSH_NULL 这个指令我们之前也见过,只不过当时没有解释
// 它是干嘛的,接下来你就会明白
// oparg 表示给函数传递的参数的个数,所以 args 指向第一个参数
PyObject **args = (stack_pointer - oparg);
// 等价于 *(args - 1),显然这是函数
PyObject *callable = stack_pointer[-(1 + oparg)];
// *(args - 2) 毫无疑问就是栈底元素 NULL
// 但它却被赋值为 method,难道和方法有关吗?
PyObject *method = stack_pointer[-(2 + oparg)];
PyObject *res; // 返回值
#line 2653 "Python/bytecodes.c"
// 如果 method 不为 NULL,说明执行的不是普通的函数,而是方法
// 所谓方法其实就是将函数和 self 绑定起来的结果
int is_meth = method != NULL;
int total_args = oparg;
// 总之现在我们明白为什么要压入一个 NULL 了,就是为了和方法调用保持统一
// 如果调用的是方法,那么栈里的元素就是:函数, self, 参数1, 参数2, ...
// 方法是对函数和 self 的绑定,调用方法本质上还是在调用函数
// 只不过调用的时候,会自动传递 self,举个例子
/*
* class A:
* def foo(self):
* pass
*
* a = A()
*/
// 如果是 A.foo,那么拿到的就是普通的函数
// 因为函数定义在类里面,所以 A.foo 也叫类的成员函数,但它依旧是一个普通的函数
// 如果是 a.foo,那么拿到的就是方法,它会将 A.foo 和实例对象 a 自身绑定起来
// 调用方法时会自动传递 self,所以 a.foo() 本质上就是 A.foo(a)
if (is_meth) { // 当 is_meth 为真时
callable = method; // method 才是要调用的 callable
args--; // 此时 self 变成了真正意义上的第一个参数,因此 args--
total_args++; // 参数个数加 1,因此 total_args++
}
// 通过 PUSH_NULL,可以让函数和方法的调用对应同一个指令
// 当然,即使不考虑方法,提前 PUSH 一个 NULL 在逻辑上也是正确的
// 因为任何函数都有返回值,执行完之后要设置在栈顶的位置
// 而一开始 PUSH 的 NULL 正好为返回值预留了空间
// ...
// 如果调用的函数,那么栈里的元素是:NULL, 函数, 参数1, 参数2, ...
// 如果调用的方法,那么栈里的元素是:函数, self, 参数1, 参数2, ...
// 但对于方法而言,栈里的元素还有一种情况:NULL, 方法, 参数1, 参数2, ...
// 对于这种情况,要将方法里面的函数和 self 提取出来
// 所以当 is_meth 为 0,但 callable 的类型是 <class 'method'> 时
if (!is_meth && Py_TYPE(callable) == &PyMethod_Type) {
is_meth = 1; // 将 is_meth 设置为 1
args--; // args 依旧向前移动一个位置
total_args++; // 参数总个数加 1
// 获取方法里面的实例对象
PyObject *self = ((PyMethodObject *)callable)->im_self;
// args 向前移动一个位置之后,它指向了目前方法所在的位置
// 将该位置的值换成 self
args[0] = Py_NewRef(self);
// 获取方法里面的函数
method = ((PyMethodObject *)callable)->im_func;
// 将 args 的前一个位置的值设置成函数
args[-1] = Py_NewRef(method);
Py_DECREF(callable);
callable = method;
// 所以之前栈里的元素是:NULL, 方法, 参数1, 参数2, ...
// args 之前也指向`参数1`,但在 args-- 之后,便指向了`方法`
// 等到将 args[0] 设置成 self,将 args[-1] 设置成函数之后
// 栈里的元素就变成了:函数, self, 参数1, 参数2, ...
}
// 到这里为止,不管是调用函数还是调用方法,逻辑都变得统一了
// 此时变量 callable 指向实际要调用的函数
// args 指向第一个参数,total_args 表示参数的个数
int positional_args = total_args - KWNAMES_LEN();
// 函数在初始化时,它的 vectorcall 字段会被设置为 _PyFunction_Vectorcall
// 所以对于函数来讲,下面这个条件是成立的,因此可以被内联
if (Py_TYPE(callable) == &PyFunction_Type &&
tstate->interp->eval_frame == NULL &&
((PyFunctionObject *)callable)->vectorcall == _PyFunction_Vectorcall)
{
// 获取 co_flags
int code_flags = ((PyCodeObject*)PyFunction_GET_CODE(callable))->co_flags;
// 如果是函数的 PyCodeObject,那么 local 名字空间指定为 NULL
// 因为局部变量不是从 local 名字空间中加载的,而是静态访问的
PyObject *locals = code_flags & CO_OPTIMIZED ? NULL : \
Py_NewRef(PyFunction_GET_GLOBALS(callable));
// 在当前栈帧之上创建新的栈帧,初始化相关字段
// 然后推入到虚拟机为其准备的 C Stack 中
_PyInterpreterFrame *new_frame = _PyEvalFramePushAndInit(
tstate, (PyFunctionObject *)callable, locals,
args, positional_args, kwnames
);
kwnames = NULL;
// 将运行时栈清空
STACK_SHRINK(oparg + 2);
if (new_frame == NULL) {
goto error;
}
JUMPBY(INLINE_CACHE_ENTRIES_CALL);
frame->return_offset = 0;
DISPATCH_INLINED(new_frame);
}
// 到这里 callable 不是一个普通的 Python 函数,但它支持 vector 协议
// 进行调用
res = PyObject_Vectorcall(
callable, args,
positional_args | PY_VECTORCALL_ARGUMENTS_OFFSET,
kwnames);
// ...
kwnames = NULL;
assert((res != NULL) ^ (_PyErr_Occurred(tstate) != NULL));
Py_DECREF(callable);
for (int i = 0; i < total_args; i++) {
Py_DECREF(args[i]);
}
if (res == NULL) { STACK_SHRINK(oparg); goto pop_2_error; }
#line 3790 "Python/generated_cases.c.h"
STACK_SHRINK(oparg);
STACK_SHRINK(1);
stack_pointer[-1] = res;
next_instr += 3;
CHECK_EVAL_BREAKER();
DISPATCH();
}
当调用函数时,会执行 _PyFunction_Vectorcall,否则执行 PyObject_Vectorcall。
以上就是函数的调用逻辑,然后再补充一点,我们说 PyFrameObject 是根据 PyCodeObject 创建的,而 PyFunctionObject 也是根据 PyCodeObject 创建的,那么 PyFrameObject 和 PyFunctionObject 之间有啥关系呢?
如果把 PyCodeObject 比喻成妹子的话,那么 PyFunctionObject 就是妹子的备胎,PyFrameObject 就是妹子的心上人。其实在栈帧中执行指令的时候,PyFunctionObject 的影响就已经消失了。
也就是说,最终是 PyFrameObject 对象和 PyCodeObject 对象两者如胶似漆,跟 PyFunctionObject 对象之间没有关系,所以 PyFunctionObject 辛苦一场,实际上是为别人做了嫁衣。PyFunctionObject 主要是对 PyCodeObject 和 global 名字空间的一种打包和运输方式。