12.0 序
函数是任何一门编程语言都具备的基本元素,它可以将多个动作组合起来,一个函数代表了一系列的动作。当然在调用函数时,会干什么来着。对,要在运行时栈中创建栈帧,用于函数的执行。
在python中,PyFrameObject
对象就是一个对栈帧的模拟,所以我们即将也会看到,python虚拟机在执行函数调用时会动态地创建新的PyFrameObject
对象。随着函数调用链的增长,这些PyFrameObject对象之间也会形成一条PyFrameObject对象链,这条链就是对象x86平台上运行时栈的模拟
12.1 PyFunctionObject对象
我们说过python中一切皆对象,函数也不例外。在python中,函数这种抽象机制是通过PyFunctionObject
对象实现的
typedef struct {
PyObject_HEAD //头部信息,不用多说
PyObject *func_code; /* 对应的函数编译后的PyCodeObject对象 */
PyObject *func_globals; /* 函数运行时的global命名空间 */
PyObject *func_defaults; /* 默认参数,tuple或者NULL */
PyObject *func_kwdefaults; /* 关键字默认参数,dict或者NULL */
PyObject *func_closure; /* 用于实现闭包 */
PyObject *func_doc; /* 函数的文档,PyUnicodeObject对象 */
PyObject *func_name; /* 函数名,PyUnicodeObject对象 */
PyObject *func_dict; /* 属性字典,dict或者NULL */
PyObject *func_weakreflist; /* 弱引用列表 */
PyObject *func_module; /* 函数的模块 */
PyObject *func_annotations; /* 注解 */
PyObject *func_qualname; /* 和name类似 */
} PyFunctionObject;
我们通过python来看看这些属性吧,首先这些属性都是func_xxx
格式的,在python中可以通过__xxx__
的形式获取
-
func_code:对应的字节码
def foo(): pass print(foo.__code__) # <code object foo at 0x000001DE153DD3A0, file "C:/Users/satori/Desktop/love_minami/a.py", line 1>
-
func_globals:global命名空间
name = "xxx" def foo(): pass # __globals__其实就是外部的globals print(foo.__globals__["name"]) # xxx print(foo.__globals__ == globals()) # True
-
func_defaults:默认参数
def foo(name="satori", age=16): pass print(foo.__defaults__) # (\'satori\', 16) def bar(): pass print(bar.__defaults__) # None
-
func_kwdefaults:默认的关键字参数。
怎么理解呢?意思就是这些参数不仅仅有默认值,而且还必须要通过关键字的方式传递
def foo(name="satori", age=16): pass # 打印是为None的,这是因为虽然有默认值,但是它并不要求必须通过关键字的方式传递 print(foo.__kwdefaults__) # None # 我在前面加上一个*,表示后面的参数就必须通过关键字的方式传递 # 因为如果不通过关键字的话,那么无论多少个位置参数都会被*给吸收掉 # 无论如何也是传递不到name,age的 # 我们经常会看到*args,这是因为我们需要这些参数,所以可以通过args来拿到这些参数 # 但是这里我们不需要,我们只是希望后面的参数必须通过关键字参数传递,因为前面写一个*即可 # 当然写*args或者其他的也可以,但是我们用不到,所以写一个*即可 def bar(*, name="satori", age=16): pass # 此时就打印了默认值,因为这是只能通过kw(关键字)传递的参数的默认值 print(bar.__kwdefaults__) # {\'name\': \'satori\', \'age\': 16}
-
func_closure:闭包
def foo(): x = 123 y = 456 def bar(): nonlocal x nonlocal y return bar # 查看的是闭包里面nonlocal的值 # 这里有两个nonlocal,所以foo().__closure__是一个有两个元素的元组 print(foo().__closure__) # (<cell at 0x000001DF68BE5BB0: int object at 0x00007FFE96C9A5E0>, <cell at 0x000001DF68BFFF10: int object at 0x000001DF68BF6C10>) print(foo().__closure__[0].cell_contents) # 123 print(foo().__closure__[1].cell_contents) # 456 # 注意:查看闭包属性我们使用的是内层函数,不是外层的foo
-
func_doc:函数的文档
def foo(name, age): """ 接收一个name和age, 返回一句话 my name is $name, age is $age """ return f"my name is {name}, age is {age}" print(foo.__doc__) """ 接收一个name和age, 返回一句话 my name is $name, age is $age """
-
func_name:函数名
def foo(name, age): pass print(foo.__name__) # foo
-
func_dict:属性字典
def foo(name, age): pass # 一般函数的属性字典都会空,属性字典基本上在类里面使用 print(foo.__dict__) # {}
-
func_weakreflist:弱引用列表
python无法获取这个属性,没有提供相应的接口
-
func_module:函数所在的模块
def foo(name, age): pass print(foo.__module__) # __main__
-
func_annotations:注解
def foo(name: str, age: int): pass print(foo.__annotations__) # {\'name\': <class \'str\'>, \'age\': <class \'int\'>}
-
func_qualname:和func_name类似
def foo(name: str, age: int): pass print(foo.__qualname__) # foo
在PyFunctionObject
对象中,我们看到了很多域。但是有两个域都和函数有关:PyCodeObject
对象和PyFunctionObject
对象,这两个对象非常重要。PyCodeObject对象是对一段代码的静态表示,python对源代码编译之后,会对每一个code block都生成一个、且唯一一个PyCodeObject,这个PyCodeObject对象中包含了这个code block中的一些静态信息,所谓静态信息是可以从源代码中看到的信息。比如code block中有一个a = 1这样的表达式,那么符号a和值1、以及它们之间的联系就是一种静态的信息,这些信息会分别存在PyCodeObject对象的常量池:co_consts
、符号表:co_names
、以及字节码序列:co_code
中,这些信息是编译的时候就可以得到的,因此PyCodeObject对象是编译时候的结果。
而PyFunctionObject则不同,PyFunctionObject对象是python代码在运行时动态产生的,更准确的说,是在执行一个def语句的时候创建的。在PyFunctionObject对象中,也会包含这些函数的静态信息,这些信息存储在func_code中,实际上,func_code一定会指向与函数代码对应的PyCodeObject对象。除此之外,PyFunctionObject对象中还包含了一些函数在执行是所必须的动态信息,即上下文信息,比如func_globals,就是函数在执行时关联的global作用域(globals)。global作用域中的符号和值必须在运行时才能确定,所以这部分必须在运行时动态创建,无法存储在PyCodeObject中。
对于一段python代码,其对应的PyCodeObject对象只有一个,但是代码对应的PyFunctionObject对象却可以有多个,比如一个函数多次调用,python会在运行时创建多个PyFunctionObject对象,而每一个PyFunctionObject对象的func_code域都会关联到这个PyCodeObject
12.2 无参函数调用
12.2.1 函数对象的创建
我们先从无参的函数调用开始,因为这是最简单的。
a.py
def foo():
print("this is a function")
foo()
b.py
import dis
code = compile(open("a.py", encoding="utf-8").read(), "a.py", "exec")
dis.dis(code)
"""
1 0 LOAD_CONST 0 (<code object foo at 0x000002807F87EF50, file "a.py", line 1>)
2 LOAD_CONST 1 (\'foo\')
4 MAKE_FUNCTION 0
6 STORE_NAME 0 (foo)
5 8 LOAD_NAME 0 (foo)
10 CALL_FUNCTION 0
12 POP_TOP
14 LOAD_CONST 2 (None)
16 RETURN_VALUE
Disassembly of <code object foo at 0x000002807F87EF50, file "a.py", line 1>:
2 0 LOAD_GLOBAL 0 (print)
2 LOAD_CONST 1 (\'this is a function\')
4 CALL_FUNCTION 1
6 POP_TOP
8 LOAD_CONST 0 (None)
10 RETURN_VALUE
"""
显然这个代码中出现了两个PyCodeObject对象,一个对应整个py文件,另一个则是对应函数foo。我们看到上面源代码的1和5行是py文件对应的PyCodeObject对象,而第2行则是对应函数foo的PyCodeObject对象
code = compile(open("a.py", encoding="utf-8").read(), "a.py", "exec")
print(type(code)) # <class \'code\'>
print(type(code.co_consts[0])) # <class \'code\'>
print(code.co_name) # <module>
print(code.co_consts[0].co_name) # foo
可以看到,函数foo对应的PyCodeObject对象是a.py这个模块对应的PyCodeObject对象的常量池co_consts中的一个元素。因为在对a.py创建PyCodeObject对象的时候,发现了一个函数foo,那么会对函数foo继续创建一个PyCodeObject对象(每一个代码块都会对应一个PyCodeObject对象),而函数foo对应的PyCodeObject对象则是模块a.py对应的PyCodeObject对象的co_consts常量池当中的一个元素。我们可以看一个再复杂一点的例子
# a1.py
def foo():
def bar():
print("this is a function")
def foo1():
pass
foo()
# 首先code是什么?显然是a1.py对应的PyCodeObject对象
code = compile(open("a1.py", encoding="utf-8").read(), "a.py", "exec")
# 而foo和foo1显然是模块级别的函数,那么这两位应该都是模块对应的PyCodeObject对象的常量池里面的元素
print(code.co_consts[0].co_name) # foo
print(code.co_consts[2].co_name) # foo1
# 至于索引、也就是在常量池中的位置,我们目前不需要关心,只要确定在常量池里面即可
# 而我们看到foo里面还嵌入了一个bar,这是一个闭包。
# 然而即便如此,它毕竟是在foo里面,那么按照之前的逻辑,显然bar对应的PyCodeObject对象也应该在foo对应的PyCodeObject对象的co_consts中
# code.con_consts[0]是foo对应的PyCodeObject对象,那么code.con_consts[0].co_consts[0]是不是就是bar对应的字节码对象呢
# 答案是猜对了一半,确实在里面,只不过索引不是0,而是1
print(code.co_consts[0].co_consts[1].co_name) # bar
# 还是那句话,我们只是确定位置,至于顺序,也就是这里的索引,我们暂时不追究
通过以上例子,我们发现,字节码是嵌套的。在介绍字节码对象的时候,我们说了,每一个code block(函数、类等等)
都会创建一个字节码对象。现在我们又看到了,根据层级来分的话,内层代码块对应的PyCodeObject对象是最近的外层代码块对应的PyCodeObject对象的常量池co_consts中的一个元素。而最外层则是模块对应的PyCodeObject对象,因此这就意味着我们通过最外层的PyCodeObject对象可以找到所有的PyCodeObject,显然这是毋庸置疑的。而这里和栈帧也是对应的,栈帧我们说过也是层层嵌套的。执行字节码的时候会创建对应的栈帧,而内层栈帧通过f_back可以找到外层、也就是调用者对应的栈帧,当然这里我们之前的章节已经说过了,这里再提一遍。
我们再来观察一下之前的a.py对应的字节码
1 0 LOAD_CONST 0 (<code object foo at 0x000002807F87EF50, file "a.py", line 1>)
2 LOAD_CONST 1 (\'foo\')
4 MAKE_FUNCTION 0
6 STORE_NAME 0 (foo)
5 8 LOAD_NAME 0 (foo)
10 CALL_FUNCTION 0
12 POP_TOP
14 LOAD_CONST 2 (None)
16 RETURN_VALUE
Disassembly of <code object foo at 0x000002807F87EF50, file "a.py", line 1>:
2 0 LOAD_GLOBAL 0 (print)
2 LOAD_CONST 1 (\'this is a function\')
4 CALL_FUNCTION 1
6 POP_TOP
8 LOAD_CONST 0 (None)
10 RETURN_VALUE
我们发现之前看到源代码的行号都是从上往下、依次增大的,这很好理解,毕竟一条一条解释嘛。但是我们看到,这里对于函数来说,发生了变化,先执行了第五行,之后再执行第二行。如果是从python层面的函数调用来理解的话,很容易一句话就解释了,因为函数只有在调用的时候才会执行。但是从字节码的角度来理解的话,我们发现函数的声明和实现是分离的,是在不同的PyCodeObject对象中。确实如此,第一个行代码和第二行代码虽然是一个整体,但是python虚拟机在实现这个函数的时候,却在物理上将它们分离开了。第一行字节码指令序列必须在a.py对应的PyCodeObject对象中,这一点也很好理解。
因为我们之前说过,函数即变量。我们是可以把函数当成是普通的变量来处理的,函数名就相当于变量名,函数体就相当于是函数值。而foo函数显然是a.py的最外层中定义的一个函数,这就意味着我们可以通过a.foo找到它,那么foo是不是要出现在a.py对应的字节码(PyCodeObject)对象中符号表co_names里面呢?foo对应的PyCodeObject对象是不是要出现在a.py对应的字节码对象的常量池co_consts里面的。
code = compile(open("a1.py", encoding="utf-8").read(), "a.py", "exec")
print(code.co_names) # (\'foo\', \'foo1\')
所以我们大致能理清逻辑了,每一个代码块都会创建一个PyCodeObject对象,那么0 LOAD_CONST
显然就是将函数foo对应字节码load进来,在2 LOAD_CONST
将符号、或者说是变量名foo给load进来。然后调用4 MAKE_FUNCTION
,这里的MAKE_FUNCTION
我们暂时先不管,不过从名字也能看出来,这个之前的BUILD LIST
比较类似,相当于使用字节码对象MAKE
一个FUNCTION
,然后6 STORE_NAME
将"变量名->foo"和"变量值->FUNCTION"作为一个entry存储在命名空间中,这是显然是global命名空间。
然而这个函数是什么是构建的呢?显然是从def foo():
这条代码处完成的。从语法上将这是函数的声明语句,但是从虚拟机的角度来看这其实是函数对象的创建语句。
两者是分离的,a.py对应的字节码对象中的foo指向foo对应的字节码,两者是分离的,当然这里的分离指的是两个不同的字节码对象。
python虚拟机在执行def语句时,会动态地创建一个函数,即PyFunctionObject对象,显然这是靠我们之前说的MAKE FUNCTION
指令完成的。
//ceval.c
TARGET(MAKE_FUNCTION) {
PyObject *qualname = POP(); //弹出符号表中的函数名
PyObject *codeobj = POP(); //弹出对应的字节码对象
//创建PyFunctionObject对象
PyFunctionObject *func = (PyFunctionObject *)
PyFunction_NewWithQualName(codeobj, f->f_globals, qualname);
Py_DECREF(codeobj);
Py_DECREF(qualname);
if (func == NULL) {
goto error;
}
//下面是设置闭包、注解等属性
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();
}
我们看到在MAKE FUNCTION
之前,先进行了LOAD CONST
,显然是将foo对应的字节码对象和符号foo压入到了栈中。所以在执行MAKE FUNCTION
的时候,首先就是将这个字节码对象以及对应符号弹出栈,然后再加上当前PyFrameObject对象中维护的global命名空间f_globals对象为参数,三者作为参数传入PyFunction_NewWithQualName函数中,从而构建出相应的PyFunctionObject
对象。而这个f_globals就是函数foo在运行时的global命名空间,而函数的global命名空间和模块级别的global是一样的,当然和模块级别的local命名空间也是一样的,因为对于模块来说,local和global是一样的。
def foo():
def bar():
return globals()
return bar
# 即使当中嵌入了一个闭包,我们的结论依旧是正确的
print(foo()() == globals() == locals()) # True
下面我们来看看PyFunction_NewWithQualName
是如何构造出一个函数的。
//Object/funcobject.c
PyObject *
PyFunction_NewWithQualName(PyObject *code, PyObject *globals, PyObject *qualname)
{
//要返回的PyFunctionObject,这里先声明一下
PyFunctionObject *op;
//doc、consts、module这些我们之前都见过的属性
PyObject *doc, *consts, *module;
//函数名
static PyObject *__name__ = NULL;
if (__name__ == NULL) {
__name__ = PyUnicode_InternFromString("__name__");
if (__name__ == NULL)
return NULL;
}
//为PyFunctionObject申请内存空间,类型为function
op = PyObject_GC_New(PyFunctionObject, &PyFunction_Type);
if (op == NULL)
return NULL;
//下面都是设置相关属性
op->func_weakreflist = NULL;
Py_INCREF(code);
op->func_code = code;
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;
op->func_dict = NULL;
op->func_module = NULL;
op->func_annotations = NULL;
module = PyDict_GetItem(globals, __name__);
if (module) {
Py_INCREF(module);
op->func_module = module;
}
if (qualname)
op->func_qualname = qualname;
else
op->func_qualname = op->func_name;
Py_INCREF(op->func_qualname);
_PyObject_GC_TRACK(op);
return (PyObject *)op;
}
在创建了PyFunctionObject
对象之后,MAKE FUNCTION
还会进行一些处理函数参数的动作,由于目前的foo是一个无参函数,所以这里暂时先略过。在MAKE FUNCTION
之后,新建的PyFunctionObject
对象就被会压入栈中,然后下面的6 STORE_NAME
则显然是将foo
和PyFunctionObject对象
组合成一个entry存储在global命名空间中。
12.2.2 函数调用
下面我们来看函数是如何调用的。首先肯定要8 LOAD_NAME
,把foo对应的value加载进来,然后就是我们熟悉的CALL_FUNCTION
,之前在print的时候就已经遇见了,但只是随便提一下。
1 0 LOAD_CONST 0 (<code object foo at 0x000002807F87EF50, file "a.py", line 1>)
2 LOAD_CONST 1 (\'foo\')
4 MAKE_FUNCTION 0
6 STORE_NAME 0 (foo)
5 8 LOAD_NAME 0 (foo)
10 CALL_FUNCTION 0
12 POP_TOP
14 LOAD_CONST 2 (None)
16 RETURN_VALUE
Disassembly of <code object foo at 0x000002807F87EF50, file "a.py", line 1>:
2 0 LOAD_GLOBAL 0 (print)
2 LOAD_CONST 1 (\'this is a function\')
4 CALL_FUNCTION 1
6 POP_TOP
8 LOAD_CONST 0 (None)
10 RETURN_VALUE
那么现在我们是时候该研究一下这个CALL_FUNCTION
了,究竟生得哪般模样!
PREDICTED(CALL_FUNCTION);
TARGET(CALL_FUNCTION) {
PyObject **sp, *res;
//获取运行时栈栈顶指针
sp = stack_pointer;
//直接杀入call_function,我们看到sp是一个二级指针
//又传入了&sp,那么call_function的第一个参数应该是一个三级指针
res = call_function(&sp, oparg, NULL);
stack_pointer = sp;
PUSH(res);
if (res == NULL) {
goto error;
}
DISPATCH();
}
#define PyCFunction_Check(op) (Py_TYPE(op) == &PyCFunction_Type)
#define PyFunction_Check(op) (Py_TYPE(op) == &PyFunction_Type)
//ceval.c
Py_LOCAL_INLINE(PyObject *) _Py_HOT_FUNCTION
call_function(PyObject ***pp_stack, Py_ssize_t oparg, PyObject *kwnames)
{
//获取PyFunctionObject对象,因为pp_stack是在CALL_FUNCTION指令中传入的栈顶指针
//传入的oparg是0,kwnames是NULL,这里的pfunc就是MAKE_FUNCTION中创建的PyFunctionObject对象
PyObject **pfunc = (*pp_stack) - oparg - 1;
//因此这里的func和pfunc是一样的
PyObject *func = *pfunc;
PyObject *x, *w;
//处理参数,对于我们当前的函数来说,这里的nkwargs和nargs都是0
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.
*/
//我们看到这里还有cfunction,这个cfunction是什么先不管
if (PyCFunction_Check(func)) {
PyThreadState *tstate = PyThreadState_GET();
C_TRACE(x, _PyCFunction_FastCallKeywords(func, stack, nargs, kwnames));
}
//这里的method这不需要关心
else if (Py_TYPE(func) == &PyMethodDescr_Type) {
PyThreadState *tstate = PyThreadState_GET();
if (nargs > 0 && tstate->use_tracing) {
PyObject *self = stack[0];
func = Py_TYPE(func)->tp_descr_get(func, self, (PyObject*)Py_TYPE(self));
if (func != NULL) {
C_TRACE(x, _PyCFunction_FastCallKeywords(func,
stack+1, nargs-1,
kwnames));
Py_DECREF(func);
}
else {
x = NULL;
}
}
else {
x = _PyMethodDescr_FastCallKeywords(func, stack, nargs, kwnames);
}
}
else {
if (PyMethod_Check(func) && PyMethod_GET_SELF(func) != NULL) {
PyObject *self = PyMethod_GET_SELF(func);
Py_INCREF(self);
func = PyMethod_GET_FUNCTION(func);
Py_INCREF(func);
Py_SETREF(*pfunc, self);
nargs++;
stack--;
}
else {
Py_INCREF(func);
}
//这里是关键,通过_PyFunction_FastCallKeywords对PyFunctionObject对象进行调用
//传入func, stack, nargs, kwnames,并把返回结果赋值给了PyObject *x
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));
while ((*pp_stack) > pfunc) {
w = EXT_POP(*pp_stack);
Py_DECREF(w);
}
return x;
}
python虚拟机通过PyFunction_Check
进行检查之后,就会进入_PyFunction_FastCallKeywords
。这里需要关注的是里面的第一个参数func,同时这个func也是需要被上面PyFunction_Check
检查的对象,显然这个func,就是通过def foo:
这个代码块对应的PyCodeObject创建的PyFunctionObject对象。
PyObject *
_PyFunction_FastCallKeywords(PyObject *func, PyObject *const *stack,
Py_ssize_t nargs, PyObject *kwnames)
{
//获取PyFunctionObject的字节码
PyCodeObject *co = (PyCodeObject *)PyFunction_GET_CODE(func);
//获取global命名空间
PyObject *globals = PyFunction_GET_GLOBALS(func);
//默认参数
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;
//检测
assert(PyFunction_Check(func));
assert(nargs >= 0);
assert(kwnames == NULL || PyTuple_CheckExact(kwnames));
assert((nargs == 0 && nkwargs == 0) || stack != NULL);
//我们观察一下下面的return
//一个是function_code_fastcall,一个是最后的_PyEval_EvalCodeWithName
//由于我们的函数没有参数,因此这里走的是快速通道
//function_code_fastcall里面逻辑很简单,直接抽走当前PyFunctionObject里面PyCodeObject和函数运行时的global命名空间等信息
//根据PyCodeObject对象直接为其创建一个PyFrameObject对象,然后PyEval_EvalFrameEx执行栈帧
//也就是真正的进入了函数调用,执行函数里面的代码
if (co->co_kwonlyargcount == 0 && nkwargs == 0 &&
(co->co_flags & ~PyCF_MASK) == (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)) {
stack = &PyTuple_GET_ITEM(argdefs, 0);
return function_code_fastcall(co, stack, PyTuple_GET_SIZE(argdefs),
globals);
}
}
kwdefs = PyFunction_GET_KW_DEFAULTS(func);
closure = PyFunction_GET_CLOSURE(func);
name = ((PyFunctionObject *)func) -> func_name;
qualname = ((PyFunctionObject *)func) -> func_qualname;
if (argdefs != NULL) {
d = &PyTuple_GET_ITEM(argdefs, 0);
nd = PyTuple_GET_SIZE(argdefs);
}
else {
d = NULL;
nd = 0;
}
//如果有参数的话,现在会走这一步,逻辑会复杂一些,不过这些都是后话了。
//但是显然最终也会经过PyEval_EvalFrameEx
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);
}
因此我们看到,总共有两条路径,分别针对无参和有参,但是最终殊途同归、都会走到PyEval_EvalFrameEx那里。然后虚拟机在新的栈帧中执行新的PyCodeObject,而这个PyCodeObject就是函数对应的PyCodeObject,也就是函数foo里面的那条print
语句对应的字节码
但是到这里恐怕就有人有疑问了,我们之前说过PyFrameObject是根据PyCodeObject创建的,而PyFunctionObject也是根据PyCodeObject创建的,那么PyFrameObject和PyFunctionObject之间有啥关系呢?如果把PyCodeObject比喻成"男人"的话,那么PyFunctionObject就是男人的"基友",PyFrameObject就是男人的"媳妇"。其实在PyEval_EvalFrameEx执行栈帧的时候,PyFunctionObject的影响就已经消失了,真正对栈帧产生影响的是PyFunctionObject里面的PyCodeObject对象和global命名空间。也就是说,PyFunctionObject辛苦一场,实际上是为别人做了嫁衣。PyFunctionObject主要是对字节码和global命名空间的一种打包和运输方式。
我们之前提到了快速通道,那么函数是通过什么来判断是否可以进入快速通道呢?答案是通过函数参数的形式来决定是否可以进入快速通道
12.3 函数执行时的命名空间
现在我们对函数调用机制有了一个大致的认识,另外我们发现在最终的函数调用时,有一个参数叫globals,这个globals最终成为和函数foo对应的PyFrameObject对应的global命名空间:f_globals
另外还记得当初对LOAD_NAME
指令的分析吗?我们说过在执行该指令时,python会依次从:f_locals
、f_globals
、f_builtins
中进行搜索。在function_code_fastcall
中传入的globals将成为在新的栈帧中执行函数foo时候的global命名空间。而在执行MAKE_FUNCTION
的指令代码中(对应函数)
,这个globals就是当前PyFrameObject对象(对应模块)
中的f_globals。这就意味着,执行a.py字节码指令序列时对应的global命名空间和执行函数foo字节码指令序列时对应的global命名空间实际上是一个命名空间。实际上这个命名空间是通过PyFunctionObject对象的携带,和字节码指令序列对应的PyCodeObject对象一起被传入到新的栈帧当中的。
但是为什么要将globals传到新创建的栈帧当中呢?这不废话吗?只有传了globals,函数内部才能在找不到变量的时候去外部找啊,如果你globals都不传,那不就为空了吗?即便外面有,你也找不到啊。
另外,我们说创建变量,会把符号和值作为一个entry放到f_locals里面的,但是对于模块级别的函数来说,f_locals和f_globals指向的是一个东西,因为模块已经是最外层了,也就不会有什么更外层的作用域了。但是对于函数来说,它的locals和globals是不一样的。
并且我们还能看到一个有趣的现象,如果我们在foo下面再定义一个bar函数,那么在foo中是可以调用bar函数的,即使bar函数定义在foo的下面。因为在执行foo的时候,首先要执行模块,为模块创建一个栈帧,并且此时函数foo、bar都已经作为PyFunctionObject对象在模块对应的栈帧的f_locals(f_globals)里面了。而执行foo的时候,会抽出里面的PyCodeObject,然后创建栈帧。foo调用bar,但foo的f_locals里面没有bar,可f_globals里面是有的,因为这和模块的f_globals是一样的,所以是可以找到bar这个函数对应PyFunctionObject的,然后从里面抽出bar对应的PyCodeObject继续为其创建栈帧,执行。所以这和c语言有个很大的不同,c语言中函数是否可以调用是通过源代码中出现的位置定义的,而python则是基于运行时的命名空间决定的。而在执行foo之前,在为模块创建栈帧的时候,foo和bar都已经被包装成PyFunctionObject对象存在了模块的global(local)命名空间中了。
12.4 函数参数的实现
函数,最大的特点就是可以传入参数,否则就只能单纯的封装,这样未免太无趣了。对于python来说,参数会传什么对于函数来说是不知道的,函数体内部只是利用参数做一些事情,比如调用参数的get方法,但是到底能不能调用get方法,就取决于你给参数传的值到底是什么了。因此可以把参数看成是一个占位符,我们假设有这么个东西,直接把它当成已存在的变量或者常量去进行操作,然后调用的时候,将某个值传进去赋给相应的参数,然后参数对应着传入的具体的值将逻辑走一遍即可。
12.4.1 参数类别
在python中,函数的参数根据形式的不同可以分为四种类别
位置参数(positional argument):
foo(a, b),a和b便是位置参数关键字参数(keyword argument):
foo(a, b, name="satori"),name便是关键字参数扩展位置参数(excess positional argument)
:foo(*agrs)扩展关键字参数(excess keyword argument):
foo(**kwargs)
我们下面来看一下python的call_function是如何处理函数信息的。
//ceval.c
Py_LOCAL_INLINE(PyObject *) _Py_HOT_FUNCTION
call_function(PyObject ***pp_stack, Py_ssize_t oparg, PyObject *kwnames)
{
//获取PyFunctionObject对象
PyObject **pfunc = (*pp_stack) - oparg - 1;
PyObject *func = *pfunc;
PyObject *x, *w;
/*当python虚拟机在开始执行MAKE_FUNCTION指令时,会先获取一个指令参数oparg
oparg里面记录函数的参数个数信息,包括位置参数和关键字参数的个数。
虽然扩展位置参数和扩展关键字参数是更高级的用法,但是本质上也是由多个位置参数、多个关键字参数组成的。
这就意味着,虽然python中存在四种参数,但是只要记录位置参数和关键字参数的个数,就能知道一共有多少个参数,进而知道一共需要多大的内存来维护参数。
而且python的每个指令都是两个字节,第一个字节存放指令序列本身,第二个字节存放参数个数,既然是一个字节,说明最多只允许有255个参数,不过这已经足够了。
*/
//nkwargs就是关键字参数的个数,nargs是位置参数的个数
Py_ssize_t nkwargs = (kwnames == NULL) ? 0 : PyTuple_GET_SIZE(kwnames);
Py_ssize_t nargs = oparg - nkwargs;
PyObject **stack = (*pp_stack) - nargs - nkwargs;
...
...
}
另外还有一个co_nlocals和co_argcount。注意:从名字也能看出来这个不是PyFunctionObject里面的,而是PyCodeObject里面的。co_nlocals,我们之前说过,这是函数内部局部变量的个数,co_argcount是参数的个数。实际上,函数参数和函数局部变量是非常密切的,某种意义上函数参数就是一种函数局部变量,它们在内存中是连续放置的。当python需要为函数申请局部变量的内存空间时,就需要通过co_nlocals知道局部变量的总数,既然如此那还要co_argcount干什么呢?别急,看个例子
def foo(a, b, c, d=1):
pass
print(foo.__code__.co_argcount) # 4
print(foo.__code__.co_nlocals) # 4
def foo(a, b, c, d=1):
a = 1
b = 1
print(foo.__code__.co_argcount) # 4
print(foo.__code__.co_nlocals) # 4
def foo(a, b, c, d=1):
aa = 1
print(foo.__code__.co_argcount) # 4
print(foo.__code__.co_nlocals) # 5
函数的参数也是一个局部变量,因此co_nlocals是参数的个数加上函数体中新创建的局部变量的个数(注意是新创建的,比如参数有一个a,但是函数体里面的变量还是a,相当于重新赋值了,因此还是相当于一个参数)
,但是co_argcount则是存储记录参数的个数。因此一个很明显的结论:对于任意一个函数,co_nlocals至少是大于等于co_argcount的
def foo(a, b, c, d=1, *args, **kwargs):
pass
print(foo.__code__.co_argcount) # 4
print(foo.__code__.co_nlocals) # 6
另外我们看到,对于扩展位置参数、扩展关键字参数来说,co_argcount是不算在内的,因为你完全可以不传递,因为直接当成0来算。而对于co_nlocals来说,我们在函数体内部肯定是拿到args和kwargs来说的,而这可以看成是两个参数。因此co_argcount是4,co_nlocals是6。其实所有的扩展位置参数是存在了一个PyTupleObject对象中的,所有的扩展关键字参数是存储在一个PyDictObject对象中的。而即使我们多传、或者不传,对于co_argcount和co_nlocals来说,都不会有任何改变了,因为这两者的值是在编译的时候就已经确定了的。
12.4.2 位置参数的传递
下面我们就来看看位置参数是如何传递的
a.py
def f(name, age):
age = age + 5
print(name, age)
age = 5
print(age)
f("satori", age)
对应字节码如下,直接先解释一下,当然到现在,这已经是很简单的了,对于很多人来说
加载字节码,谁的字节吗?显然是函数f对应的代码块
1 0 LOAD_CONST 0 (<code object f at 0x0000024C189C1030, file "f", line 1>)
将f这个符号load进来
2 LOAD_CONST 1 (\'f\')
将f的字节码包装成一个PyFunctionObject对象
4 MAKE_FUNCTION 0
将符号f和PyFunctionObject组成一个entry放入f_locals(f_globals)对应的PyDictObject里面
6 STORE_NAME 0 (f)
此时跳到了第6行,load常量5,依旧是组成entry存储起来,由于这是在全局中,所以是STORE_NAME,而不是STORE_FAST
6 8 LOAD_CONST 2 (5)
10 STORE_NAME 1 (age)
此时调用函数,当然是print函数,由于都是在全局中,所以是LOAD_NAME
7 12 LOAD_NAME 2 (print)
14 LOAD_NAME 1 (age)
CALL_FUNCTION调用print
16 CALL_FUNCTION 1
从栈顶将元素弹出来,并打印
18 POP_TOP
此时开始了f函数的调用,调用之前肯定也要准备一下
于是从PyDictObject中将刚才存储的f对应的PyFunctionObject对象、以及"satori"这个字符串常量load进来
f和age是在全局变量中的,所以是LOAD_NAME,而"satori"字符串是一个常量,所以是LOAD_CONST
9 20 LOAD_NAME 0 (f)
22 LOAD_CONST 3 (\'satori\')
24 LOAD_NAME 1 (age)
调用函数,此时跳到了源代码的第2行,因为遇见函数调用就会创建新的栈帧,并把代码执行的控制权交给新创建的栈帧,执行完了在返回,类似于递归,一层一层创建、执行,然后一层一层返回
26 CALL_FUNCTION 2
下面三条字节码的逻辑就无需解释了
28 POP_TOP
30 LOAD_CONST 4 (None)
32 RETURN_VALUE
Disassembly of <code object f at 0x0000024C189C1030, file "f", line 1>:
我们说过参数age是一个局部变量,直接LOAD_FAST,5则是LOAD_CONST
2 0 LOAD_FAST 1 (age)
2 LOAD_CONST 1 (5)
加法运算
4 BINARY_ADD
此时的age函数体里面创建的局部变量,局部变量的存储是有优化的,所以是STORE_FAST
6 STORE_FAST 1 (age)
下面则是LOAD_GLOBAL,会判断函数里面有没有定义print,但是显然没有,于是前往global、builtin命名空间里面去找,所以是LOAD_GLOBAL,而如果是在外层的话,则是LOAD_NAME
至于有没有LOAD_BUILTIN,实际是没有的,因为builtin是在global里面,我们通过global这个PyDictObject的__bultin__属性是可以找到builtin命名空间的、或者说是builtin对应的PyDictObject
3 8 LOAD_GLOBAL 0 (print)
而name和age则是里面的常量,则是LOAD_FAST
10 LOAD_FAST 0 (name)
12 LOAD_FAST 1 (age)
调用函数
14 CALL_FUNCTION 2
从栈顶弹出元素,并打印
16 POP_TOP
返回默认为None,LOAD_CONST,向None、True、False这些关键字都是LOAD_CONST
18 LOAD_CONST 0 (None)
返回
20 RETURN_VALUE
字节码虽然解释完了, 但是最重要的还是没有说。f(name, age),这里的name和age显然是外层定义的,但是外层定义的这两个变量是怎么传给函数f的。下面我们通过源码重新分析:
9 20 LOAD_NAME 0 (f)
22 LOAD_CONST 3 (\'satori\')
24 LOAD_NAME 1 (age)
26 CALL_FUNCTION 2
我们注意到CALL_FUNCTION
上面有三条指令,其实当这三条指令执行完毕之后,函数需要的参数已经被压入了运行时栈中。
通过_PyFunction_FastCallKeywords函数,然后执行function_code_fastcall
//call.c
static PyObject* _Py_HOT_FUNCTION
function_code_fastcall(PyCodeObject *co, PyObject *const *args, Py_ssize_t nargs,
PyObject *globals)
{
PyFrameObject *f;
PyThreadState *tstate = PyThreadState_GET();
PyObject **fastlocals;
Py_ssize_t i;
PyObject *result;
assert(globals != NULL);
assert(tstate != NULL);
//创建与函数对应的PyFrameObject,我们看到参数是co,所以是根据字节码指令来创建的
f = _PyFrame_New_NoTrack(tstate, co, globals, NULL);
if (f == NULL) {
return NULL;
}
//关键:拷贝函数参数,从运行时栈到PyFrameObject.f_localsplus
fastlocals = f->f_localsplus;
...
return result;
}
从源码中我们看到通过_PyFrame_New_NoTrack创建了函数f对应的PyFrameObject对象,参数是f对应的PyFunctionObject对象中保存的PyCodeObject对象。随后,python虚拟机将参数逐个拷贝到新建的PyFrameObject对象的f_localsplus中。可在分析python虚拟机框架时,我们知道,这个f_localsplus所指向的内存块里面也存储了python虚拟机所使用的那个运行时栈。那么参数所占的内存和运行时栈所占的内存有什么关联呢?
//frameobject.c
//这个是_PyFrame_New_NoTrack,对外暴露的是PyFrame_New,但是本质上调用了这个
PyFrameObject* _Py_HOT_FUNCTION
_PyFrame_New_NoTrack(PyThreadState *tstate, PyCodeObject *code,
PyObject *globals, PyObject *locals)
{
PyFrameObject *back = tstate->frame;
PyFrameObject *f;
PyObject *builtins;
Py_ssize_t i;
...
...
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) {
//为f_localsplus申请extras的内存空间
f = PyObject_GC_NewVar(PyFrameObject, &PyFrame_Type,
extras);
...
...
f->f_code = code;
//获得f_localsplus中出去运行时栈,剩余的内存数
extras = code->co_nlocals + ncells + nfrees;
f->f_valuestack = f->f_localsplus + extras;
...
...
f->f_lasti = -1;
f->f_lineno = code->co_firstlineno;
f->f_iblock = 0;
f->f_executing = 0;
f->f_gen = NULL;
f->f_trace_opcodes = 0;
f->f_trace_lines = 1;
return f;
}
前面提到,在函数对应的PyCodeObject对象的co_nlocals域中,包含着函数参数的个数,因为函数参数也是局部符号的一种。所以从f_localsplus开始,extras中一定有供函数参数使用的内存。或者说,函数的参数存放在运行时栈之前的那段内存中。
另外从_PyFrame_New_NoTrack当中我们可以看到,在f_localsplus中存储函数参数的空间和运行时栈的空间在逻辑上是分离的,并不是共享同一片内存,尽管它们是连续的。这两者是鸡犬相闻,但又泾渭分明、老死不相往来。
在处理完参数之后,还没有进入PyEval_EvalFrameEx,所以此时运行时栈是空的。但是函数的参数已经位于f_localsplus中了。所以这时新建PyFrameObject对象的f_localsplus就是这样:
12.4.3 位置参数的访问
当参数拷贝的动作完成之后,就会进入新的PyEval_EvalFrameEx,开始真正的f的调用动作。
2 0 LOAD_FAST 1 (age)
2 LOAD_CONST 1 (5)
4 BINARY_ADD
6 STORE_FAST 1 (age)
梦回指令集
LOAD_FAST:在函数里面load一个局部变量
LOAD_GLOBAL:在函数里面load一个全局变量、或者内置变量
LOAD_NAME:在外层模块中load一个变量
LOAD_CONST:不限范围,只要load的内容是一个常量
STORE_FAST:在函数里面创建一个局部变量
STORE_NAME:在外层模块中创建一个全局变量
STORE_GLOBAL:在外层模块或者函数里面创建一个被global声明的变量
首先对参数的读写,肯定是通过LOAD_FAST
,LOAD_CONST
,STORE_FAST
这几条指令集完成的
//ceval.c
PyObject* _Py_HOT_FUNCTION
_PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag)
{
...
...
fastlocals = f->f_localsplus;
...
}
#define GETLOCAL(i) (fastlocals[i])
[LOAD_FAST]
TARGET(LOAD_FAST) {
PyObject *value = GETLOCAL(oparg);
if (value == NULL) {
format_exc_check_arg(PyExc_UnboundLocalError,
UNBOUNDLOCAL_ERROR_MSG,
PyTuple_GetItem(co->co_varnames, oparg));
goto error;
}
Py_INCREF(value);
PUSH(value);
FAST_DISPATCH();
}
[STORE_FAST]
PREDICTED(STORE_FAST);
TARGET(STORE_FAST) {
PyObject *value = POP();
SETLOCAL(oparg, value);
FAST_DISPATCH();
}
所以我们发现,LOAD_FAST和STORE_FAST这一对指令是以f_localsplus这一片内存为操作目标的,指令0 LOAD_FAST 1 (age)
的结果是将f_localsplus[1]对应的对象压入到运行时栈中,而我们刚才也看到f_localsplus[1]中存放的正是age。而在完成加法操作之后,又将结果通过STORE_FAST放入到f_localsplus[1]中,这样就实现了对a的更新,那么以后在print(a)的时候,得到的结果就是10了。
现在关于python的位置参数在函数调用时是如何传递的、在函数执行又是如何被访问,已经真相大白了。在调用函数时,python将函数参数的值从左至右依次压入到运行时栈中,而在call_function中通过调用_PyFunction_FastCallKeywords,进而调用function_code_fastcall,而在function_code_fastcall中,又将这些参数依次的拷贝到新建的与函数对应的PyFrameObject对象的f_localsplus中。最终的效果就是,python虚拟机将函数调用时使用的参数,从左至右依次地存放在新建的PyFrameObject对象的f_localsplus中。
因此在访问函数参数时,python虚拟机并没有按照通常访问符号的做法,去查什么命名空间,而是直接通过一个索引(偏移位置)
来访问f_localsplus中存储的符号对应的值,是的,f_localsplus存储的是符号(变量名)
,并不是具体的值,所以python传参的方式都是引用传递,不会像golang一样,都是拷贝一份,否则那python效率就太低了,至于值是否改变,则取决于对应的值是可变对象还是不可变对象,而不是像其他编程语言那样通过传值或者传指针来决定是否改变。因此这种通过索引(偏移位置)
来访问参数的方式也正是位置参数的由来。
12.4.4 位置参数的默认值
可能有人看到位置参数的默认值
这几个字会感到懵逼,这难道不是关键字参数吗?其实位置参数、关键字参数一般是通过调用来体现的,而不是定义函数的时候。你在调用的时候,使用顺序将实参和形参进行对应的方式来传递参数时候,那么你传递的就是位置参数,如果是通过关键字的方式传递,那么传递的就是位置参数。比如:def foo(a, b=1)
,其中的a和b准确的说都是形参,或者说都是参数吧,但是由于b有了默认值,所以b也叫缺省参数或者默认参数。所以我们通过foo(2)调用时,并没有给b传值,但是定义的时候b=1,可以看做是默认给b传了一个1,即foo(2, 1),这个1就是默认值。所以位置参数、关键字参数是通过调用来体现的,而不是定义,也就是针对于实参的,是根据实参的传递方式来分类的,和形参是没有关系的。
下面就来考察一下默认值机制
a.py
def foo(a=1, b=2):
print(a + b)
foo()
foo(b=3)
1 0 LOAD_CONST 7 ((1, 2))
2 LOAD_CONST 2 (<code object foo at 0x0000015C4C5610E0, file "f", line 1>)
4 LOAD_CONST 3 (\'foo\')
6 MAKE_FUNCTION 1 (defaults)
8 STORE_NAME 0 (foo)
5 10 LOAD_NAME 0 (foo)
12 CALL_FUNCTION 0
14 POP_TOP
6 16 LOAD_NAME 0 (foo)
18 LOAD_CONST 4 (3)
20 LOAD_CONST 5 ((\'b\',))
22 CALL_FUNCTION_KW 1
24 POP_TOP
26 LOAD_CONST 6 (None)
28 RETURN_VALUE
Disassembly of <code object foo at 0x0000015C4C5610E0, file "f", line 1>:
2 0 LOAD_GLOBAL 0 (print)
2 LOAD_FAST 0 (a)
4 LOAD_FAST 1 (b)
6 BINARY_ADD
8 CALL_FUNCTION 1
10 POP_TOP
12 LOAD_CONST 0 (None)
14 RETURN_VALUE
我们对比一下开始的没有默认参数的函数,会发现相比于无默认参数的函数,有默认参数的函数,除了load函数体对应的PyCodeObject、和foo这个符号之外,会先将默认参数的值给load进来,将这三者都压入运行时栈。但是我们发现这是默认参数是组合成一个元组的形式入栈的,而且我们再来观察一下MAKE_FUNCTION
这个指令,我们发现后面的参数是1 (defaults),之前的都是0,那么这个1是什么呢?而且又提示了我们一个defaults,我们知道PyFunctionObject对象有一个func_defaults,这两者之间有关系吗?那么带着这些疑问再来看看MAKE_FUNCTION
指令。
[MAKE_FUNCTION]
TARGET(MAKE_FUNCTION) {
//获取PyCodeObject、func_name,并创建PyFunctionObject
PyObject *qualname = POP();
PyObject *codeobj = POP();
PyFunctionObject *func = (PyFunctionObject *)
PyFunction_NewWithQualName(codeobj, f->f_globals, qualname);
Py_DECREF(codeobj);
Py_DECREF(qualname);
if (func == NULL) {
goto error;
}
//处理参数,这个是针对于闭包的
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();
}
//默认关键字参数,我们发现确实是存储在func_defaults里面
if (oparg & 0x01) {
assert(PyTuple_CheckExact(TOP()));
func->func_defaults = POP();
}
PUSH((PyObject *)func);
DISPATCH();
}
通过以上命令我们很容易看出,MAKE_FUNCTION
指令除了创建PyFunctionObject对象,并且还会处理参数的默认值。MAKE_FUNCTION
指令参数表示当前运行时栈中是存在默认值的,但是默认值具体多少个通过参数是看不到的,因为默认值都会按照顺序塞到一个PyTupleObject对象里面,所以整体相当于是一个。然后会调用PyFunction_SetDefaults
将该PyTupleObject对象设置为PyFunctionObject.func_defaults的值,在python层面可以使用__defaults__
访问。如此一来,函数参数的默认值也成为了PyFunctionObject对象的一部分,函数和其参数的默认值最终被python虚拟机捆绑在了一起,它和PyCodeObject、global命名空间一样,也被塞进了PyFunctionObject这个大包袱。所以说PyFunctionObject这个嫁衣做的是很彻底的,工具人PyFunctionObject对象,给个赞。
//functionobject.c
int
PyFunction_SetDefaults(PyObject *op, PyObject *defaults)
{
//两个参数,一个PyFunctionObject、一个PyTupleObject
//检测机制,不用管
if (!PyFunction_Check(op)) {
PyErr_BadInternalCall();
return -1;
}
if (defaults == Py_None)
defaults = NULL;
else if (defaults && PyTuple_Check(defaults)) {
Py_INCREF(defaults);
}
else {
PyErr_SetString(PyExc_SystemError, "non-tuple default args");
return -1;
}
//设置
Py_XSETREF(((PyFunctionObject *)op)->func_defaults, defaults);
return 0;
}
函数的第一次调用:foo()
//call.c
//这个是通过ceval.c里面的call_function调用的
PyObject *
_PyFunction_FastCallKeywords(PyObject *func, PyObject *const *stack,
Py_ssize_t nargs, PyObject *kwnames)
{
//获取PyFunctionObject的PyCodeObject
PyCodeObject *co = (PyCodeObject *)PyFunction_GET_CODE(func);
//获取PyFunctionObject的f_globals
PyObject *globals = PyFunction_GET_GLOBALS(func);
//看这里,这一步显然是获取PyFunctionObject里面的func_defaults
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;
assert(PyFunction_Check(func));
assert(nargs >= 0);
assert(kwnames == NULL || PyTuple_CheckExact(kwnames));
assert((nargs == 0 && nkwargs == 0) || stack != NULL);
if (co->co_kwonlyargcount == 0 && nkwargs == 0 &&
(co->co_flags & ~PyCF_MASK) == (CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE))
{
//是否进入快速通道,首先要满足argdefs == NULL,但是我们发现*argdefs是有值的
//所以argdefs这个指针是不为NULL的,因此进入通道失败。所以函数如果有默认参数,是不会进入快速通道的。
if (argdefs == NULL && co->co_argcount == nargs) {
//另外我们发现即使函数定义的时候没有默认参数,但是我们调用的时候通过关键字参数传参话,也不会进入快速通道
//首先这里的nargs是通过call_function函数传递的,而这个nargs在call_function函数中是Py_ssize_t nargs = oparg - nkwargs;
//所以这里的nargs就是传递的参数个数减去通过关键字参数方式传递的参数个数
//而co_argcount是函数参数的总个数,所以一旦哪怕有一个参数使用了关键字参数的方式传递,都会造成两者不相等,从而无法进入快速通道
//因此在CPython中,一个函数若想进入快速通道,只要满足以下两点即可
/*
1.函数定义的时候不可以有默认参数
2.函数调用时,必须都通过位置参数指定。
*/
return function_code_fastcall(co, stack, nargs, globals);
}
//但是这样的条件毕竟太苛刻了,毕竟参数哪能没有默认值呢?所以python还提供了一种进入快速通道的方式
//我们发现在有默认的前提下,如果还能满足nargs==0 && co->co_argcount == PyTuple_GET_SIZE(argdefs)也能进入快速通道
//co->co_argcount == PyTuple_GET_SIZE(argdefs)是要求函数的参数个数必须等于默认参数的个数,也就是函数参数全是默认参数
//nargs==0则是需要传入的参数个数减去通过关键字参数传递的参数个数等于0,即要么不传参(都是用默认参数)、要么全部都通过关键字参数的方式传参。
//这种方式也可以进入快速通道
else if (nargs == 0 && argdefs != NULL
&& co->co_argcount == PyTuple_GET_SIZE(argdefs)) {
/* function called with no arguments, but all parameters have
a default value: use default values as arguments .*/
stack = &PyTuple_GET_ITEM(argdefs, 0);
return function_code_fastcall(co, stack, PyTuple_GET_SIZE(argdefs),
globals);
}
}
//如果以上两点都无法满足的话,那么就没办法了,只能走常规方法了
//这里是获取函数的一些属性,默认关键字参数、闭包等等
kwdefs = PyFunction_GET_KW_DEFAULTS(func);
closure = PyFunction_GET_CLOSURE(func);
name = ((PyFunctionObject *)func) -> func_name;
qualname = ((PyFunctionObject *)func) -> func_qualname;
//这里则是获取默认参数的值的地址和默认参数的个数
if (argdefs != NULL) {
d = &PyTuple_GET_ITEM(argdefs, 0);
nd = PyTuple_GET_SIZE(argdefs);
}
else {
d = NULL;
nd = 0;
}
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);
}
_PyEval_EvalCodeWithName
是一个非常重要的函数,在后面分析扩展位置参数和扩展关键字参数是还会遇到。
PyObject *
_PyEval_EvalCodeWithName(PyObject *_co, PyObject *globals, PyObject *locals,
PyObject *const *args, Py_ssize_t argcount, //位置参数的信息
PyObject *const *kwnames, PyObject *const *kwargs,//关键字参数的信息
Py_ssize_t kwcount, int kwstep,
PyObject *const *defs, Py_ssize_t defcount,//默认参数的信息
PyObject *kwdefs, PyObject *closure,
PyObject *name, PyObject *qualname)
{
PyCodeObject* co = (PyCodeObject*)_co;
PyFrameObject *f;
PyObject *retval = NULL;
PyObject **fastlocals, **freevars;
PyThreadState *tstate;
PyObject *x, *u;
const Py_ssize_t total_args = co->co_argcount + co->co_kwonlyargcount;
Py_ssize_t i, n;
PyObject *kwdict;
...
...
以上是关于《python解释器源码剖析》第12章--python虚拟机中的函数机制的主要内容,如果未能解决你的问题,请参考以下文章
《python解释器源码剖析》第4章--python中的list对象
《python解释器源码剖析》第13章--python虚拟机中的类机制
《python解释器源码剖析》第10章--python虚拟机中的一般表达式
《python解释器源码剖析》第11章--python虚拟机中的控制流