原创 古明地觉 2024-11-19 10:29 北京
上一篇文章我们说了 Python 函数的底层实现,并且还演示了如何通过函数的类型对象自定义一个函数,以及如何获取函数的参数。虽然这在工作中没有太大意义,但是可以让我们深刻理解函数的行为。
那么接下来看看函数是如何调用的。
在介绍调用之前,我们需要补充一个知识点。
def foo(): passclass A: def foo(self): passprint(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 + bfoo(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 名字空间的一种打包和运输方式。