15-函数在底层是如何被调用的 楔子 在上一篇博客中,我们说了Python函数的底层实现,并且还演示了如何自定义一个函数,虽然这在工作中没有太大意义,但是可以让我们深刻理解函数的行为。此外我们还介绍了如何获取函数的参数,而这一次我们就来看看函数如何调用的。
函数的调用 1 2 3 4 5 6 7 8 9 10 11 s = """ def foo(): a, b = 1, 2 return a + b foo() """ if __name__ == '__main__' : import dis dis.dis(compile (s, "call_function" , "exec" ))
我们以一个非常简单的函数为例,看看它的字节码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 2 0 LOAD_CONST 0 (<code object foo at 0x00000219BA3F1450 , file "call_function" , line 2 >) 2 LOAD_CONST 1 ('foo' ) 4 MAKE_FUNCTION 0 6 STORE_NAME 0 (foo) 6 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 0x00000219BA3F1450 , file "call_function" , line 2 >: 3 0 LOAD_CONST 1 ((1 , 2 )) 2 UNPACK_SEQUENCE 2 4 STORE_FAST 0 (a) 6 STORE_FAST 1 (b) 4 8 LOAD_FAST 0 (a) 10 LOAD_FAST 1 (b) 12 BINARY_ADD 14 RETURN_VALUE
还是那句话,模块有一个PyCodeObject对象,函数也有一个PyCodeObject对象,只不过后者是在前者的常量池当中。而且dis模块在显示字节码的时候,自动帮我们分开了,我们从上到下依次捋一遍。
0 LOAD_CONST 0 (<code object......: 遇到def关键字知道这是一个函数, 所以会加载其对应的PyCodeObject对象
2 LOAD_CONST 1 ('foo'): 加载函数名
4 MAKE_FUNCTION 0: 通过MAKE_FUNCTION指令构造一个函数
6 STORE_NAME 0 (foo): 将符号"foo"和上一步得到的函数绑定起来, 存储在local空间中, 这个local空间显然是模块的local空间、即global空间
8 LOAD_NAME 0 (foo): 注意这一步是在调用的时候发生的, 将变量foo加载进来
10 CALL_FUNCTION 0: 通过CALL_FUNCTION指令调用该函数(我们后面将要分析的重点), 后面的0表示参数个数
12 POP_TOP: 将上一步函数的返回值从运行时栈的顶部弹出
14 LOAD_CONST 2 (None): 加载返回值None
16 RETURN_VALUE: 将返回值返回
模块对应的字节码就是上面那样,再来看看函数的,事实上对于现在的你来说已经很简单了。
0 LOAD_CONST 1 ((1, 2)): 从常量池中加载元组, 我们说对于列表而言是先将内部的元素一个一个加载进来、然后通过BUILD_LIST构建一个列表, 但是对于元组来说则可以直接加载, 原因就是元组内的元素指向对象的地址不可以变
2 UNPACK_SEQUENCE 2: 解包
4 STORE_FAST 0 (a): 将解包得到两个常量中的第一个常量赋值给a
6 STORE_FAST 0 (B): 将解包得到两个常量中的第二个常量赋值给b
8 LOAD_FAST 0 (a): 加载局部变量a
10 LOAD_FAST 1 (b): 加载局部变量b
12 BINARY_ADD: 执行加法运算
14 RETURN_VALUE: 将返回值返回
所以从目前来看,这些字节码已经没什么难度了,但是我们看到调用函数是用过CALL_FUNCTION指令,那么这个指令都做了哪些事情呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 case TARGET (CALL_FUNCTION) : { PREDICTED(CALL_FUNCTION); PyObject **sp, *res; sp = stack_pointer; res = call_function(tstate, &sp, oparg, NULL ); stack_pointer = sp; PUSH(res); if (res == NULL ) { goto error; } DISPATCH(); }
然后重点是call_function函数,我们来看一下,同样位于 *ceval.c* 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 #define PyCFunction_Check(op) (Py_TYPE(op) == &PyCFunction_Type) #define PyFunction_Check(op) (Py_TYPE(op) == &PyFunction_Type) Py_LOCAL_INLINE(PyObject *) _Py_HOT_FUNCTION call_function (PyThreadState *tstate, PyObject ***pp_stack, Py_ssize_t oparg, PyObject *kwnames) { PyObject **pfunc = (*pp_stack) - oparg - 1 ; PyObject *func = *pfunc; 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; if (tstate->use_tracing) { x = trace_call_function(tstate, func, stack , nargs, kwnames); } else { x = _PyObject_Vectorcall(func, stack , nargs | PY_VECTORCALL_ARGUMENTS_OFFSET, kwnames); } assert((x != NULL ) ^ (_PyErr_Occurred(tstate) != NULL )); while ((*pp_stack) > pfunc) { w = EXT_POP(*pp_stack); Py_DECREF(w); } return x; } static PyObject *trace_call_function (PyThreadState *tstate, PyObject *func, PyObject **args, Py_ssize_t nargs, PyObject *kwnames) { PyObject *x; if (PyCFunction_Check(func)) { C_TRACE(x, _PyObject_Vectorcall(func, args, nargs, kwnames)); return x; } else if (Py_TYPE(func) == &PyMethodDescr_Type && nargs > 0 ) { PyObject *self = args[0 ]; func = Py_TYPE(func)->tp_descr_get(func, self, (PyObject*)Py_TYPE(self)); if (func == NULL ) { return NULL ; } C_TRACE(x, _PyObject_Vectorcall(func, args+1 , nargs-1 , kwnames)); Py_DECREF(func); return x; } return _PyObject_Vectorcall(func, args, nargs | PY_VECTORCALL_ARGUMENTS_OFFSET, kwnames); }
然后会调用 *_PyFunction_FastCallDict* 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 PyObject * _PyFunction_FastCallDict(PyObject *func, PyObject *const *args, Py_ssize_t nargs, PyObject *kwargs) { PyCodeObject *co = (PyCodeObject *)PyFunction_GET_CODE(func); PyObject *globals = PyFunction_GET_GLOBALS(func); PyObject *argdefs = PyFunction_GET_DEFAULTS(func); PyObject *kwdefs, *closure, *name, *qualname; PyObject *kwtuple, **k; PyObject **d; Py_ssize_t nd, nk; PyObject *result; assert(func != NULL ); assert(nargs >= 0 ); assert(nargs == 0 || args != NULL ); assert(kwargs == NULL || PyDict_Check(kwargs)); if (co->co_kwonlyargcount == 0 && (kwargs == NULL || PyDict_GET_SIZE(kwargs) == 0 ) && (co->co_flags & ~PyCF_MASK) == (CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE)) { if (argdefs == NULL && co->co_argcount == nargs) { return function_code_fastcall(co, args, nargs, globals); } else if (nargs == 0 && argdefs != NULL && co->co_argcount == PyTuple_GET_SIZE(argdefs)) { args = _PyTuple_ITEMS(argdefs); return function_code_fastcall(co, args, PyTuple_GET_SIZE(argdefs), globals); } } nk = (kwargs != NULL ) ? PyDict_GET_SIZE(kwargs) : 0 ; if (nk != 0 ) { Py_ssize_t pos, i; kwtuple = PyTuple_New(2 * nk); if (kwtuple == NULL ) { return NULL ; } k = _PyTuple_ITEMS(kwtuple); pos = i = 0 ; while (PyDict_Next(kwargs, &pos, &k[i], &k[i+1 ])) { Py_INCREF(k[i]); Py_INCREF(k[i+1 ]); i += 2 ; } assert(i / 2 == nk); } else { kwtuple = NULL ; k = NULL ; } 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_ITEMS(argdefs); nd = PyTuple_GET_SIZE(argdefs); } else { d = NULL ; nd = 0 ; } result = _PyEval_EvalCodeWithName((PyObject*)co, globals, (PyObject *)NULL , args, nargs, k, k != NULL ? k + 1 : NULL , nk, 2 , d, nd, kwdefs, closure, name, qualname); Py_XDECREF(kwtuple); return result; }
因此我们看到,总共有两条路径,分别针对无参和有参,但是最终殊途同归、都会走到PyEval_EvalFrameEx那里。然后虚拟机在新的栈帧中执行新的PyCodeObject,而这个PyCodeObject就是函数对应的PyCodeObject。
但是到这里恐怕就有人有疑问了,我们之前说过PyFrameObject是根据PyCodeObject创建的,而PyFunctionObject也是根据PyCodeObject创建的,那么PyFrameObject和PyFunctionObject之间有啥关系呢?
如果把PyCodeObject比喻成”妹子”的话,那么PyFunctionObject就是妹子的”备胎”,PyFrameObject就是妹子的”心上人”。其实PyEval_EvalFrameEx在栈帧中执行的时候,PyFunctionObject的影响就已经消失了,真正对栈帧产生影响的是PyFunctionObject里面的PyCodeObject对象和global名字空间。也就是说,最终是PyFrameObject对象和PyCodeObject对象两者如胶似漆,跟PyFunctionObject对象之间没有关系,所以PyFunctionObject辛苦一场,实际上是为别人做了嫁衣。PyFunctionObject主要是对PyCodeObject和global名字空间的一种打包和运输方式。
另外我们这里提到了快速通道,那么函数是通过什么来判断是否可以进入快速通道呢?答案是通过函数参数的形式来决定是否可以进入快速通道,下面我们就来看看函数中参数的实现。
函数参数的实现 函数最大的特点就是可以传入参数,否则就只能单纯的封装,这样未免太无趣了。对于Python来说,参数会传什么对于函数来说是不知道的,函数体内部只是利用参数做一些事情,比如调用参数的get方法,但是到底能不能调用get方法,就取决于你给参数传的值到底是什么了。因此可以把参数看成是一个占位符,我们假设有这么个东西,直接把它当成已存在的变量或者常量去进行操作,然后调用的时候,将某个值传进去赋给相应的参数,然后参数对应着传入的具体的值将逻辑走一遍即可。
参数类别 在Python中,调用函数时所传递的参数根据形式的不同可以分为四种类别:
位置参数(positional argument): foo(a, b), a和b通过位置参数传递
关键字参数(keyword argument): foo(a=1, b=2), a和b通过关键字参数
扩展位置参数(excess positional argument): foo(*args), args通过扩展位置参数传递
**扩展关键字参数(excess keyword argument)**:foo(**kwargs), kwargs通过扩展位置参数传递
我们下面来看一下python的call_function是如何处理函数信息的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Py_LOCAL_INLINE(PyObject *) _Py_HOT_FUNCTION call_function (PyThreadState *tstate, PyObject ***pp_stack, Py_ssize_t oparg, PyObject *kwnames) { PyObject **pfunc = (*pp_stack) - oparg - 1 ; PyObject *func = *pfunc; 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;
而且Python的每个指令都是两个字节,第一个字节存放指令序列本身,第二个字节存放参数个数,既然是一个字节,说明最多只允许有255个参数,不过这已经足够了。但是在Python3.8中,这个限制被打破了。
1 2 3 4 5 6 7 [root@iZ2ze3ik2oh85c6hanp0hmZ ~] Traceback (most recent call last): File "1.py" , line 8 , in <module> print (exec (s)) File "<string>" , line 2 SyntaxError: more than 255 arguments [root@iZ2ze3ik2oh85c6hanp0hmZ ~]
以我阿里云上的Python3.6为例,发现参数不能超过255个,但是在Python3.8的时候,即使有1000000个参数也是没问题的。所以Python3.8的源码变动是有些大的,3.6和3.7实际上是差不多的,虚拟机实现代码甚至和Python2也高度相似。但是在Python3.8,变动就有点大了。
Python函数内部局部变量信息,可以通过co_nlocals和co_argcount来获取。从名字也能看出来这个不是PyFunctionObject里面的,而是PyCodeObject里面的。co_nlocals,我们之前说过,这是函数内部局部变量的个数,co_argcount是参数的个数。实际上,函数参数和函数局部变量是非常密切的,某种意义上函数参数就是一种函数局部变量,它们在内存中是连续放置的。当Python需要为函数申请局部变量的内存空间时,就需要通过co_nlocals知道局部变量的总数。不过既然如此,那还要co_argcount干什么呢?别急,看个例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def foo (a, b, c, d=1 ): pass print (foo.__code__.co_argcount) print (foo.__code__.co_nlocals) def foo (a, b, c, d=1 ): a = 1 b = 1 print (foo.__code__.co_argcount) print (foo.__code__.co_nlocals) def foo (a, b, c, d=1 ): aa = 1 print (foo.__code__.co_argcount) print (foo.__code__.co_nlocals)
函数的参数也是一个局部变量,因此co_nlocals是参数的个数加上函数体中新创建的局部变量的个数。注意函数参数也是一个局部变量,比如参数有一个a,但是函数体里面的变量还是a,相当于重新赋值了,因此还是相当于一个参数。但是co_argcount则是存储记录参数的个数。因此一个很明显的结论:对于任意一个函数,co_nlocals至少是大于等于co_argcount的
。
1 2 3 4 5 6 def foo (a, b, c, d=1 , *args, **kwargs ): pass print (foo.__code__.co_argcount) print (foo.__code__.co_nlocals)
另外我们看到,对于扩展位置参数、扩展关键字参数来说,co_argcount是不算在内的,因为你完全可以不传递,所以直接当成0来算。而对于co_nlocals来说,我们在函数体内部肯定是能拿到args和kwargs的,而这可以看成是两个参数。因此co_argcount是4,co_nlocals是6。其实所有的扩展位置参数是存在了一个PyTupleObject对象中的,所有的扩展关键字参数是存储在一个PyDictObject对象中的。而即使我们多传、或者不传,对于co_argcount和co_nlocals来说,都不会有任何改变了,因为这两者的值是在编译的时候就已经确定了的。
位置参数的传递 下面我们就来看看位置参数是如何传递的:
1 2 3 4 5 6 7 8 9 10 11 12 s = f""" def f(name, age): age = age + 5 print(name, age) age = 5 f("satori", age) """ if __name__ == '__main__' : import dis dis.dis(compile (s, "call_function" , "exec" ))
字节码如下,我们来分析一下,当然基础的就一笔带过了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 2 0 LOAD_CONST 0 (<code object f at 0x00000224C3941450 , file "call_function" , line 2 >) 2 LOAD_CONST 1 ('f' ) 4 MAKE_FUNCTION 0 6 STORE_NAME 0 (f) 6 8 LOAD_CONST 2 (5 ) 10 STORE_NAME 1 (age) 7 12 LOAD_NAME 0 (f) 14 LOAD_CONST 3 ('satori' ) 16 LOAD_NAME 1 (age) 18 CALL_FUNCTION 2 20 POP_TOP 22 LOAD_CONST 4 (None) 24 RETURN_VALUE Disassembly of <code object f at 0x00000224C3941450 , file "call_function" , line 2 >: 3 0 LOAD_FAST 1 (age) 2 LOAD_CONST 1 (5 ) 4 BINARY_ADD 6 STORE_FAST 1 (age) 4 8 LOAD_GLOBAL 0 (print) 10 LOAD_FAST 0 (name) 12 LOAD_FAST 1 (age) 14 CALL_FUNCTION 2 16 POP_TOP 18 LOAD_CONST 0 (None) 20 RETURN_VALUE
字节码虽然解释完了, 但是最重要的还是没有说。f(name, age)
,这里的name和age显然是外层定义的,但是外层定义的这两个变量是怎么传给函数f的。下面我们通过源码重新分析:
1 2 3 7 12 LOAD_NAME 0 (f) 14 LOAD_CONST 3 ('satori' ) 16 LOAD_NAME 1 (age)
我们注意到CALL_FUNCTION
上面有三条指令,其实当这三条指令执行完毕之后,函数需要的参数已经被压入了运行时栈中。
通过 *_PyFunction_FastCallDict* 函数,然后执行function_code_fastcall。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 static PyObject* _Py_HOT_FUNCTIONfunction_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 ); 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 ); if (Py_REFCNT(f) > 1 ) { Py_DECREF(f); _PyObject_GC_TRACK(f); } else { ++tstate->recursion_depth; Py_DECREF(f); --tstate->recursion_depth; } return result; }
从源码中我们看到通过 *_PyFrame_New_NoTrack* 创建了函数f对应的PyFrameObject对象,参数是co对应的PyFunctionObject对象中保存的PyCodeObject对象。随后,Python虚拟机将参数逐个拷贝到新建的PyFrameObject对象的f_localsplus中。可在分析Python虚拟机框架时,我们知道,这个f_localsplus所指向的内存块里面也存储了Python虚拟机所使用的那个运行时栈。那么参数所占的内存和运行时栈所占的内存有什么关联呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 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 = PyObject_GC_NewVar(PyFrameObject, &PyFrame_Type, extras); if (f == NULL ) { Py_DECREF(builtins); return NULL ; } } else { } f->f_code = code; 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_locals = NULL ; f->f_trace = NULL ; } 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就是这样:
位置参数的访问 当参数拷贝的动作完成之后,就会进入新的PyEval_EvalFrameEx,开始真正的f的调用动作。
1 2 3 4 3 0 LOAD_FAST 1 (age) 2 LOAD_CONST 1 (5 ) 4 BINARY_ADD 6 STORE_FAST 1 (age)
首先对参数的读写,肯定是通过LOAD_FAST
,LOAD_CONST
,STORE_FAST
这几条指令集完成的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 PyObject* _Py_HOT_FUNCTION _PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag) { ... ... fastlocals = f->f_localsplus; ... } #define GETLOCAL(i) (fastlocals[i]) case TARGET (LOAD_FAST) : { PyObject *value = GETLOCAL(oparg); if (value == NULL ) { format_exc_check_arg(tstate, PyExc_UnboundLocalError, UNBOUNDLOCAL_ERROR_MSG, PyTuple_GetItem(co->co_varnames, oparg)); goto error; } Py_INCREF(value); PUSH(value); FAST_DISPATCH(); } case TARGET (STORE_FAST) : { PREDICTED(STORE_FAST); PyObject *value = POP(); SETLOCAL(oparg, value); FAST_DISPATCH(); }
所以我们发现,LOAD_FAST和STORE_FAST这一对指令是以f_localsplus这一片内存为操作目标的,指令0 LOAD_FAST 1 (age)
的结果是将f_localsplus[1]对应的对象压入到运行时栈中。而在完成加法操作之后,又将结果通过STORE_FAST放入到f_localsplus[1]中,这样就实现了对a的更新,那么以后在print(a)的时候,得到的结果就是10了。
现在关于Python的位置参数在函数调用时是如何传递的、在函数执行又是如何被访问的,已经真相大白了。在调用函数时,Python将函数参数的值从左至右依次压入到运行时栈中,而在call_function中通过调用 *_PyFunction_FastCallDict* ,进而调用function_code_fastcall,而在function_code_fastcall中,又将这些参数依次拷贝到和PyFrameObject对象的f_localsplus中。最终的效果就是,Python虚拟机将函数调用时使用的参数,从左至右依次地存放在新建的PyFrameObject对象的f_localsplus中。
因此在访问函数参数时,python虚拟机并没有按照通常访问符号的做法,去查什么名字空间,而是直接通过一个索引(偏移位置)
来访问f_localsplus中存储的符号对应的值,是的,f_localsplus存储的是符号(变量名)
,并不是具体的值。因为我们说Python中的变量只是一个指针,至于值是否改变,则取决于对应的值是可变对象还是不可变对象,而不是像其他编程语言那样通过传值或者传指针来决定是否改变。因此这种通过索引(偏移位置)
来访问参数的方式也正是位置参数的由来。
默认参数 Python函数的一个特点就是支持默认参数,这是非常方便的,我们来看看实现机制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 s = """ def foo(a=1, b=2): print(a + b) foo() """ if __name__ == '__main__' : import dis dis.dis(compile (s, "default" , "exec" )) 2 0 LOAD_CONST 5 ((1 , 2 ))//我们看到在构造函数的时候就已经把默认值加载进来了 2 LOAD_CONST 2 (<code object foo at 0x000002076ED83BE0 , file "default" , line 2 >) 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 16 LOAD_CONST 4 (None ) 18 RETURN_VALUE Disassembly of <code object foo at 0x000002076ED83BE0 , file "default" , line 2 >: 3 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
指令。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 case TARGET (MAKE_FUNCTION) : { 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(); } 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对象,给个赞。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int PyFunction_SetDefaults (PyObject *op, PyObject *defaults) { 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函数为例,看看不同的调用方式对应的底层实现:
1 2 def foo (a=1 , b=2 ): print (a + b)
不传入参数,直接执行foo()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 PyObject * _PyFunction_FastCallDict(PyObject *func, PyObject *const *args, Py_ssize_t nargs, PyObject *kwargs) { PyCodeObject *co = (PyCodeObject *)PyFunction_GET_CODE(func); PyObject *globals = PyFunction_GET_GLOBALS(func); PyObject *argdefs = PyFunction_GET_DEFAULTS(func); PyObject *kwdefs, *closure, *name, *qualname; PyObject *kwtuple, **k; PyObject **d; Py_ssize_t nd, nk; PyObject *result; assert(func != NULL ); assert(nargs >= 0 ); assert(nargs == 0 || args != NULL ); assert(kwargs == NULL || PyDict_Check(kwargs)); if (co->co_kwonlyargcount == 0 && (kwargs == NULL || PyDict_GET_SIZE(kwargs) == 0 ) && (co->co_flags & ~PyCF_MASK) == (CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE)) { if (argdefs == NULL && co->co_argcount == nargs) { return function_code_fastcall(co, args, nargs, globals); } else if (nargs == 0 && argdefs != NULL && co->co_argcount == PyTuple_GET_SIZE(argdefs)) { args = _PyTuple_ITEMS(argdefs); return function_code_fastcall(co, args, PyTuple_GET_SIZE(argdefs), globals); } } nk = (kwargs != NULL ) ? PyDict_GET_SIZE(kwargs) : 0 ; if (nk != 0 ) { Py_ssize_t pos, i; kwtuple = PyTuple_New(2 * nk); if (kwtuple == NULL ) { return NULL ; } k = _PyTuple_ITEMS(kwtuple); pos = i = 0 ; while (PyDict_Next(kwargs, &pos, &k[i], &k[i+1 ])) { Py_INCREF(k[i]); Py_INCREF(k[i+1 ]); i += 2 ; } assert(i / 2 == nk); } else { kwtuple = NULL ; k = NULL ; } 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_ITEMS(argdefs); nd = PyTuple_GET_SIZE(argdefs); } else { d = NULL ; nd = 0 ; } result = _PyEval_EvalCodeWithName((PyObject*)co, globals, (PyObject *)NULL , args, nargs, k, k != NULL ? k + 1 : NULL , nk, 2 , d, nd, kwdefs, closure, name, qualname); Py_XDECREF(kwtuple); return result; }
_PyEval_EvalCodeWithName
是一个非常重要的函数,在后面分析扩展位置参数和扩展关键字参数是还会遇到。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 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; PyObject *x, *u; const Py_ssize_t total_args = co->co_argcount + co->co_kwonlyargcount; Py_ssize_t i, j, n; PyObject *kwdict; PyThreadState *tstate = _PyThreadState_GET(); assert(tstate != NULL ); if (globals == NULL ) { _PyErr_SetString(tstate, PyExc_SystemError, "PyEval_EvalCodeEx: NULL globals" ); return NULL ; } f = _PyFrame_New_NoTrack(tstate, co, globals, locals); if (f == NULL ) { return NULL ; } fastlocals = f->f_localsplus; freevars = f->f_localsplus + co->co_nlocals; if (co->co_flags & CO_VARKEYWORDS) { kwdict = PyDict_New(); if (kwdict == NULL ) goto fail; i = total_args; if (co->co_flags & CO_VARARGS) { i++; } SETLOCAL(i, kwdict); } else { kwdict = NULL ; } if (argcount > co->co_argcount) { n = co->co_argcount; } else { n = argcount; } for (j = 0 ; j < n; j++) { x = args[j]; Py_INCREF(x); SETLOCAL(j, x); } if (co->co_flags & CO_VARARGS) { u = _PyTuple_FromArray(args + n, argcount - n); if (u == NULL ) { goto fail; } SETLOCAL(total_args, u); } kwcount *= kwstep; for (i = 0 ; i < kwcount; i += kwstep) { if ((argcount > co->co_argcount) && !(co->co_flags & CO_VARARGS)) { too_many_positional(tstate, co, argcount, defcount, fastlocals); goto fail; } if (argcount < co->co_argcount) { Py_ssize_t m = co->co_argcount - defcount; Py_ssize_t missing = 0 ; for (i = argcount; i < m; i++) { if (GETLOCAL(i) == NULL ) { missing++; } } if (missing) { missing_arguments(tstate, co, missing, defcount, fastlocals); goto fail; } 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); } } } return retval; }
因此通过以上我们就知道了位置参数的默认值是怎么一回事了。
传入一个关键字参数,执行foo(b=3)
在对foo进行第二次调用的时候,我们指定了b=3,但是调用方式本质是一样的。在CALL_FUNCTION
之前,python虚拟机将PyUnicodeObject对象b和PyLongObject对象3压入了运行时栈。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 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; f = _PyFrame_New_NoTrack(tstate, co, globals, locals); if (co->co_flags & CO_VARKEYWORDS) { } else { } if (argcount > co->co_argcount) { n = co->co_argcount; } else { n = argcount; } for (j = 0 ; j < n; j++) { } if (co->co_flags & CO_VARARGS) { } 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 (keyword == NULL || !PyUnicode_Check(keyword)) { _PyErr_Format(tstate, PyExc_TypeError, "%U() keywords must be strings" , co->co_name); goto fail; } co_varnames = ((PyTupleObject *)(co->co_varnames))->ob_item; for (j = co->co_posonlyargcount; j < total_args; j++) { PyObject *name = co_varnames[j]; if (name == keyword) { goto kw_found; } } for (j = co->co_posonlyargcount; j < total_args; j++) { PyObject *name = co_varnames[j]; int cmp = PyObject_RichCompareBool( keyword, name, Py_EQ); if (cmp > 0 ) { goto kw_found; } else if (cmp < 0 ) { goto fail; } } assert(j >= total_args); if (kwdict == NULL ) { if (co->co_posonlyargcount && positional_only_passed_as_keyword(tstate, co, kwcount, kwnames)) { goto fail; } _PyErr_Format(tstate, PyExc_TypeError, "%U() got an unexpected keyword argument '%S'" , co->co_name, keyword); goto fail; } if (PyDict_SetItem(kwdict, keyword, value) == -1 ) { goto fail; } continue ; kw_found: if (GETLOCAL(j) != NULL ) { _PyErr_Format(tstate, PyExc_TypeError, "%U() got multiple values for argument '%S'" , co->co_name, keyword); goto fail; } Py_INCREF(value); SETLOCAL(j, value); } return retval; }
在编译时:Python会将函数的def语句中出现的符号都记录在符号表(co_varnames)里面。由于我们已经看到,在foo(b=3)的指令序列中,Python虚拟机在执行CALL_FUNCTION
指令之前会将关键字参数的名字都压入到运行时栈,那么在_PyEval_EvalCodeWithName
中就能利用运行时栈中保存的关键字参数的名字在Python编译时得到的co_varnames中进行查找。最妙的是,在co_varnames中定义的变量名的顺序是由规律的的。而且经过刚才的分析,我们也知道,在PyFrameObject对象的f_localsplus所维护的内存中,用于存储函数参数的内存也是按照相同规律排列的。所以在co_varnames中搜索到关键字参数的参数名时,我们可以直接根据所得到的序号信息直接设置f_localsplus中的内存,这就为默认参数设置了函数调用者希望的值。
因此我们可以再举个简单例子,总结一下。def foo(a, b, c, d=1,e=2, f=3),对于这样的一个函数。首先Python虚拟机知道调用者至少要给a、b、c传递参数。如果是foo(1),那么1会传递给a,但是b和c是没有接受到值的,所以报错。但如果是foo(1, e=4, c=2, b=3),还是老规矩1传递给a,发现依旧不够,这时候会把希望寄托于关键字参数上。并且我们说过f_localsplus维护的内存中存储的参数的顺序、co_varnames中参数的顺序都是一致的。所以关键字参数是不讲究顺序的,当找到了e=4,那么Python虚拟机通过co_varnames符号表,就知道把e设置为f_localsplus中索引为4的地方,c=2,设置为索引为2的地方,b=3,设置为索引为1的地方。那么当位置参数和关键字参数都是设置完毕之后,python虚拟机会检测需要传递的参数、也就是没有默认值的参数,调用者有没有全部传递。
但是这里再插一句,我们说关键字参数设置具体设置在f_localsplus中的哪一个地方,是通过将关键字参数名代入到co_varnames符号表里面查找所得到的的,但是如果这个关键字参数的参数名不在co_varnames里面,怎么办?另外在我们讲位置参数的时候,如果传递的位置参数,比co_argcount还要多,怎么办?对,聪明如你,肯定知道了,就是我们下面要介绍扩展关键字、扩展位置参数。
扩展位置参数和扩展关键字参数 之前我们看到了使用扩展位置参数和扩展关键字参数时指令参数个数的值,我们还是再看一遍吧。
1 2 3 4 5 6 def foo (a, b, *args, **kwargs ): pass print (foo.__code__.co_nlocals) print (foo.__code__.co_argcount)
**我们看到对于co_nlocals来说,它统计的是所有局部变量的个数,结果是4;但是对于co_argcount来说,统计的是不包括*args个*kwargs的所有参数的个数,因此结果是2。既然如此,那么也如我们之前所分析的,*args可以接收多个位置参数,但是最终这些参数都会放在args这个PyTupleObject对象里面;*kwargs可以接收多个关键字参数,但是这些关键字参数会组成一个PyDictObject对象,由kwargs指向。事实上也确实如此,即使不从源码的角度来分析,从Python的实际使用中我们也能得出这个结论。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def foo (*args, **kwargs ): print (args) print (kwargs) foo(1 , 2 , 3 , a=1 , b=2 , c=3 ) """ (1, 2, 3) {'a': 1, 'b': 2, 'c': 3} """ foo(*(1 , 2 , 3 ), **{"a" : 1 , "b" : 2 , "c" : 3 }) """ (1, 2, 3) {'a': 1, 'b': 2, 'c': 3} """
*当然啦,在传递的时候如果对一个元组或者列表、甚至是字符串使用*,那么会将这个可迭代对象直接打散,相当于传递了多个位置参数。同理如果对一个字典使用*,那么相当于传递了多个关键字参数。
下面我们就来看看扩展参数是如何实现的,首先还是进入到 *_PyEval_EvalCodeWithName* 这个函数里面来,当然这个函数应该很熟悉了,我们看看扩展参数的处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 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; PyObject *x, *u; const Py_ssize_t total_args = co->co_argcount + co->co_kwonlyargcount; Py_ssize_t i, j, n; PyObject *kwdict; f = _PyFrame_New_NoTrack(tstate, co, globals, locals); if (f == NULL ) { return NULL ; } fastlocals = f->f_localsplus; freevars = f->f_localsplus + co->co_nlocals; if (co->co_flags & CO_VARKEYWORDS) { kwdict = PyDict_New(); if (kwdict == NULL ) goto fail; i = total_args; if (co->co_flags & CO_VARARGS) { i++; } SETLOCAL(i, kwdict); } else { kwdict = NULL ; } if (argcount > co->co_argcount) { n = co->co_argcount; } else { n = argcount; } for (j = 0 ; j < n; j++) { x = args[j]; Py_INCREF(x); SETLOCAL(j, x); } if (co->co_flags & CO_VARARGS) { u = _PyTuple_FromArray(args + n, argcount - n); if (u == NULL ) { goto fail; } SETLOCAL(total_args, u); } 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 (keyword == NULL || !PyUnicode_Check(keyword)) { _PyErr_Format(tstate, PyExc_TypeError, "%U() keywords must be strings" , co->co_name); goto fail; } co_varnames = ((PyTupleObject *)(co->co_varnames))->ob_item; for (j = co->co_posonlyargcount; j < total_args; j++) { PyObject *name = co_varnames[j]; if (name == keyword) { goto kw_found; } } for (j = co->co_posonlyargcount; j < total_args; j++) { PyObject *name = co_varnames[j]; int cmp = PyObject_RichCompareBool( keyword, name, Py_EQ); if (cmp > 0 ) { goto kw_found; } else if (cmp < 0 ) { goto fail; } } assert(j >= total_args); if (kwdict == NULL ) { if (co->co_posonlyargcount && positional_only_passed_as_keyword(tstate, co, kwcount, kwnames)) { goto fail; } _PyErr_Format(tstate, PyExc_TypeError, "%U() got an unexpected keyword argument '%S'" , co->co_name, keyword); goto fail; } if (PyDict_SetItem(kwdict, keyword, value) == -1 ) { goto fail; } continue ; kw_found: if (GETLOCAL(j) != NULL ) { _PyErr_Format(tstate, PyExc_TypeError, "%U() got multiple values for argument '%S'" , co->co_name, keyword); goto fail; } Py_INCREF(value); SETLOCAL(j, value); } if ((argcount > co->co_argcount) && !(co->co_flags & CO_VARARGS)) { too_many_positional(tstate, co, argcount, defcount, fastlocals); goto fail; } return retval; }
Python在对参数进行处理的时候,机制还是很复杂的。我们知道Python在定义函数的时候,通过/可以使得/前面的参数必须通过位置参数传递,通过*可以使得*后面的参数必须通过位置参数传递,而我们在分析的时候是没有考虑这一点的。
*其实扩展关键字参数的传递机制和普通关键字参数的传递机制有很大的关系,我们之前分析函数参数的默认值机制已经看到了关键字参数的传递机制,这里我们再次看到了。对于关键字参数,不论是否扩展,都会把符号和值分别按照对应顺序放在两个数组里面。然后Python会按照索引的顺序,遍历存放符号的数组,对每一个符号都会和符号表co_varnames里面的符号逐个进行比对,发现在符号表中找不到我们传递的关键字参数的符号,那么就说明这是一个扩展关键字参数。然后就是我们在源码中看到的那样,如果函数定义了*kwargs,那么kwdict就不为空,会把扩展关键字参数直接设置进去,否则就报错了,提示接收到了一个不期待的关键字参数。
而且Python虚拟机也确实把该PyDictObject对象(kwargs)
放到了f_localsplus中,这个f_localsplus里面包含了所有的参数,不管是什么参数,都会在里面。但是kwargs一定是在最后面,至于*args理论上是没有顺序的,你是可以这么定义的:def foo(a, \*args, b)
,这样定义是完全没有问题的,只是此时的b就必须要通过关键字参数来传递了,因为如果不通过关键字参数的方式,那么无论多少个位置参数,都会止步于\*args
。之前也介绍过,假设只需要name,age, gender这三个参数,并且gender必须要通过关键字参数指定的话,那么就可以这么设计:def foo(name, age, \*, gender)
,我们看到连args都省去了,只保留一个\*
,这是因为我们定义了args也用不到,我们只是保证后面的gender必须通过关键字方式传递,所以只需要一个\*
就ok了。
另外在Python3.8中,注意只有Python3.8开始才支持,可以强制使用位置参数,语法是通过/
。
当然访问传递过来的扩展位置参数和扩展关键字参数就通过args对应的PyTupleObject和kwargs对应的PyDictObject操作就可以了。
此外,我们在分析参数的时候,一直是截取部分片段,没有从上到下整体分析,因此可以再对着源码自己看一遍。当然核心还是Python在处理函数参数时的机制,整体流程如下(先不考虑/和\*)
:
1. 获取所有通过位置参数传递的个数,然后循环遍历将它们从运行时栈依次拷贝到 f_localsplus 指定的位置中;
2. 计算出可以通过位置参数传递的参数个数,如果实际传递的位置参数个数大于可以通过位置参数传递个数,那么会检测是否存在 *args,如果存在,那么将多余的位置参数拷贝到一个元组中;不存在,则报错:TypeError: function() takes ‘m’ positional argument but ‘n’ were given,其中 n 大于 m,表示接收了多个位置参数;
3. 如果实际传递的位置参数个数小于等于可以通过位置参数传递个数,那么程序继续往下执行,检测关键字参数,它是通过两个数组来实现的,参数名和值是分开存储的;
4. 然后进行遍历,两层 for 循环,第一层 for 循环遍历存放关键字参数名的数组,第二层遍历符号表,会将传递参数名和符号表中的每一个符号进行比较;
**5. 如果指定了不在符号表中的参数名,那么会检测是否定义了 *kwargs,如果没有则报错:TypeError: function() got an unexpected keyword argument ‘xxx’,接收了一个不期望的参数 xxx;如果定义了 *kwargs,那么会设置在字典中;
6. 如果参数名在符号表中存在,那么跳转到 kw_found 标签,然后获取该符号对应的 value,如果 value 不为 NULL,那么证明该参数已经通过位置参数传递了,会报错:TypeError: function() got multiple values for argument ‘xxx’,提示函数的参数 xxx 接收了多个值;
7. 最终所有的参数都会存在 f_localsplus 中,然后检测是否存在对应的 value 为 NULL 的符号,如果存在,那么检测是否具有默认值,有则使用默认值,没有则报错:
所以Python在处理参数的大致流程是上面那样的,具体细节层面也很好理解,只是要处理各种各样的情况,导致看起来让人有点头疼。当然Python中的生成器和异步生成器的逻辑也在这个函数里面,我们后续系列中会分析。
小结 这一次我们分析了函数调用时候的场景,以及如何处理不同形式的参数,重点还是有一个整体性的认识。下一篇,我们将来分析闭包。