14-函数在底层的数据结构、以及它的创建方式 楔子 函数是任何一门编程语言都具备的基本元素,它可以将多个动作组合起来,一个函数代表了一系列的动作。当然我们之前说函数也是一个变量,该变量指向一个函数。而且在调用函数时会干什么来着,没错,要在运行时栈中创建栈帧,用于函数的执行。
那么下面就来看看函数在C中是如何实现的,生得一副什么模样。
PyFunctionObject对象 我们说过Python中一切皆对象,函数也不例外。在Python中,函数这种抽象机制是通过PyFunctionObject
对象实现的,位于 *Include/funcobject.h* 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 typedef struct { PyObject_HEAD PyObject *func_code; PyObject *func_globals; PyObject *func_defaults; PyObject *func_kwdefaults; PyObject *func_closure; PyObject *func_doc; PyObject *func_name; PyObject *func_dict; PyObject *func_weakreflist; PyObject *func_module; PyObject *func_annotations; PyObject *func_qualname; vectorcallfunc vectorcall; } PyFunctionObject;
PyFunctionObject的这些成员都是以func开头的,比如:func_name,但是我们在Python中获取的时候直接通过__name__获取即可。
func_code:函数的字节码
1 2 3 4 5 6 7 def foo (a, b, c ): pass code = foo.__code__ print (code) print (code.co_varnames)
func_globals:global命名空间
1 2 3 4 5 6 7 8 def foo (a, b, c ): pass name = "夏色祭" print (foo.__globals__) print (foo.__globals__ == globals ())
func_defaults:函数参数的默认值
1 2 3 4 5 6 7 8 9 10 11 12 13 def foo (name="夏色祭" , age=-1 ): pass print (foo.__defaults__) def bar (): pass print (bar.__defaults__)
func_kwdefaults:只能通过关键字参数传递的”参数”和”该参数的默认值”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def foo (name="夏色祭" , age=-1 ): pass print (foo.__kwdefaults__) def bar (*, name="夏色祭" , age=-1 ): pass print (bar.__kwdefaults__)
func_closure:闭包对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def foo (): name = "夏色祭" age = -1 def bar (): nonlocal name nonlocal age return bar print (foo().__closure__) print (foo().__closure__[0 ].cell_contents) print (foo().__closure__[1 ].cell_contents)
func_doc:函数的文档
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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:函数名
1 2 3 4 5 def foo (name, age ): pass print (foo.__name__)
func_dict:属性字典
1 2 3 4 5 6 def foo (name, age ): pass print (foo.__dict__)
func_weakreflist:弱引用列表
Python无法获取这个属性,底层没有提供相应的接口。
func_module:函数所在的模块
1 2 3 4 5 def foo (name, age ): pass print (foo.__module__)
func_annotations:注解
1 2 3 4 5 def foo (name: str , age: int ): pass print (foo.__annotations__)
func_qualname:全限定名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 def foo (): pass print (foo.__name__, foo.__qualname__) class A : def foo (self ): pass print (A.foo.__name__, A.foo.__qualname__)
在PyFunctionObject的定义中,我们看到一个func_code成员,指向了一个PyCodeObject对象,我们说函数就是根据这个PyCodeObject对象创建的。因为我们知道一个PyCodeObject对象是对一段代码的静态表示,Python编译器在将源代码进行编译之后,对里面的每一个代码块(code block)
都会生成一个、并且是唯一一个PyCodeObject对象,这个PyCodeObject对象中包含了这个代码块中的一些静态信息,也就是可以从源代码中看到的信息。
比如:某个函数对应的code block中有一个 name = “夏色祭” 这样的表达式,那么符号”a”和对应的值1、以及它们之间的联系就是静态信息。这些信息会被静态存储起来,符号”a”会被存在符号表co_varnames
中、值1会被存在常量池co_consts
中、这两者之间是一个赋值,因此会有两条指令LOAD_CONSTS和STORE_FAST存在字节码指令序列co_code
中。
这些信息是编译的时候就可以得到的,因此PyCodeObject对象是编译时候的结果。
但是PyFunctionObject对象是何时产生的呢?实际上PyFunctionObject对象是Python代码在运行时动态产生的,更准确的说,是在执行一个def语句的时候创建的。
当Python虚拟机在当前栈帧中执行字节码时发现了def语句,那么就代表发现了新的PyCodeObject对象,因为它们是可以层层嵌套的。所以虚拟机会根据这个PyCodeObject对象创建对应的PyFunctionObject对象,然后将函数名和函数体对应的PyFunctionObject对象组成键值对放在当前的local空间中。
显然在PyFunctionObject对象中,也会包含这些函数的静态信息,这些信息存储在func_code中,实际上,func_code一定会指向与函数对应的PyCodeObject对象。除此之外,PyFunctionObject对象中还包含了一些函数在执行时所必须的动态信息,即上下文信息。比如func_globals,就是函数在执行时关联的global作用域(globals),说白了就是让你在局部变量找不到的时候能够找全局变量,可如果连global空间都没有的话,那即便想找也无从下手呀。而global作用域中的符号和值必须在运行时才能确定,所以这部分必须在运行时动态创建,无法存储在PyCodeObject中,所以要根据PyCodeObject对象创建PyFunctionObject对象,相当于一个封装。总之一切的目的,都是为了更好的执行字节码。
我们举个栗子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 name = "夏色祭" age = -1 def foo (): pass print (locals ())
PyFrameObject和PyFunctionObject对象的区别与联系 PyFrameObject
和PyFunctionObject
是Python解释器内部用于实现函数调用和执行的两个重要数据结构。它们之间有以下区别和联系:
区别:
PyFrameObject
表示Python解释器的执行栈帧,包含了函数调用时的局部变量、参数、返回值等信息。每当Python解释器执行一个函数时,就会在执行栈上创建一个新的PyFrameObject
,并将该栈帧推入执行栈中。当函数执行完毕时,该栈帧将被弹出执行栈。
PyFunctionObject
表示Python中的函数对象,包含了函数的代码、参数、默认值等信息。每当Python解释器遇到一个函数定义时,就会创建一个新的PyFunctionObject
对象,并将其保存在内存中。在函数调用时,Python解释器会创建一个新的PyFrameObject
对象,并将其绑定到相应的PyFunctionObject
对象上,从而完成函数调用。
联系:
PyFrameObject
和PyFunctionObject
之间存在密切的联系,因为它们一起实现了Python解释器中的函数调用和执行过程。具体来说,当Python解释器执行一个函数时,它会创建一个新的PyFrameObject
对象,并将其绑定到相应的PyFunctionObject
对象上。在函数执行过程中,PyFrameObject
对象将存储函数的局部变量、参数和返回值等信息,并通过Python解释器的执行栈来管理函数调用的层次关系。当函数执行完毕时,Python解释器会弹出执行栈,并将PyFrameObject
对象从内存中释放掉。
总的来说,PyFrameObject
和PyFunctionObject
是Python解释器中实现函数调用和执行的两个重要数据结构。PyFrameObject
用于存储函数调用时的局部变量、参数和返回值等信息,而PyFunctionObject
用于存储函数的代码、参数和默认值等信息。它们之间紧密地协作,以实现Python函数的调用和执行过程。
函数对象如何创建 我们现在已经看清了函数的模样,它在底层对应PyFunctionObject对象,并且它和PyCodeObject对象关系密切。那么Python底层又是如何完成PyCodeObject对象到PyFunctionObject对象之间的转变呢?想了解这其中的奥秘,就必须要从字节码入手。
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 s = """ name = "夏色祭" def foo(a, b): print(a, b) foo(1, 2) """ import disdis.dis(compile (s, "func" , "exec" )) 2 0 LOAD_CONST 0 ('夏色祭' ) 2 STORE_NAME 0 (name) 3 4 LOAD_CONST 1 (<code object foo at 0x000001EE0CBA72F0 , file "func" , line 3 >) 6 LOAD_CONST 2 ('foo' ) 8 MAKE_FUNCTION 0 10 STORE_NAME 1 (foo) 6 12 LOAD_NAME 1 (foo) 14 LOAD_CONST 3 (1 ) 16 LOAD_CONST 4 (2 ) 18 CALL_FUNCTION 2 20 POP_TOP 22 LOAD_CONST 5 (None ) 24 RETURN_VALUE Disassembly of <code object foo at 0x000001EE0CBA72F0 , file "func" , line 3 >: 4 0 LOAD_GLOBAL 0 (print ) 2 LOAD_FAST 0 (a) 4 LOAD_FAST 1 (b) 6 CALL_FUNCTION 2 8 POP_TOP 10 LOAD_CONST 0 (None ) 12 RETURN_VALUE
显然这个代码中出现了两个PyCodeObject对象,一个对应整个py文件,另一个则是对应函数foo。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 s = """ name = "夏色祭" def foo(a, b): print(a, b) foo(1, 2) """ co = compile (s, "func" , "exec" ) print (co.co_consts) print (co.co_name) print (co.co_consts[1 ].co_name)
可以看到,”函数foo对应的PyCodeObject对象”是”py文件对应的PyCodeObject对象”的常量池co_consts中的一个元素。因为在对py文件创建PyCodeObject对象的时候,发现了一个函数代码块foo,那么会对函数代码块foo继续创建一个PyCodeObject对象(每一个代码块都会对应一个PyCodeObject对象),而函数foo对应的PyCodeObject对象则是py文件对应的PyCodeObject对象的co_consts常量池当中的一个元素。
通过以上例子,我们发现PyCodeObject对象是嵌套的。之前我们我们说过,每一个code block(函数、类等等)
都会对应一个PyCodeObject对象。现在我们又看到了,根据层级来分的话,”内层代码块对应的PyCodeObject对象”是”最近的外层代码块对应的PyCodeObject对象”的常量池co_consts中的一个元素。而最外层则是模块对应的PyCodeObject对象,因此这就意味着我们通过最外层的PyCodeObject对象可以找到所有的PyCodeObject对象,显然这是毋庸置疑的。而这里和栈帧也是对应的,栈帧我们说过也是层层嵌套的,而内层栈帧通过f_back可以找到外层、也就是调用者对应的栈帧,当然这里我们之前的章节已经说过了,这里再提一遍。
这里再来重新看一下上面的字节码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 2 0 LOAD_CONST 0 ('夏色祭' ) 2 STORE_NAME 0 (name) 3 4 LOAD_CONST 1 (<code object foo at 0x000001EE0CBA72F0 , file "func" , line 3 >) 6 LOAD_CONST 2 ('foo' ) 8 MAKE_FUNCTION 0 10 STORE_NAME 1 (foo) 6 12 LOAD_NAME 1 (foo) 14 LOAD_CONST 3 (1 ) 16 LOAD_CONST 4 (2 ) 18 CALL_FUNCTION 2 20 POP_TOP 22 LOAD_CONST 5 (None) 24 RETURN_VALUE Disassembly of <code object foo at 0x000001EE0CBA72F0 , file "func" , line 3 >: 4 0 LOAD_GLOBAL 0 (print) 2 LOAD_FAST 0 (a) 4 LOAD_FAST 1 (b) 6 CALL_FUNCTION 2 8 POP_TOP 10 LOAD_CONST 0 (None) 12 RETURN_VALUE
显然dis模块自动帮我们分成了两部分,上面是模块的字节码,下面是函数的字节码。首先函数很简单我们就不看了,直接看模块的。
首先开头的LOAD_CONST和STORE_NAME显然是 name = “夏色祭” 对应的指令。然后我们看4 LOAD_CONST
,这条指令也是加载了一个常量,但这个常量是一个PyCodeObject对象;6 LOAD_CONST
则是将字符串常量”foo”、即函数名加载了进来,然后通过MAKE_FUNCTION指令构建一个PyFunctionObject对象;然后10 STORE_NAME
,让符号foo指向这个PyFunctionObject对象。再下面就是函数调用了,函数调用的具体细节我们之后会详细说。
并且我们还看到一个有趣的现象,那就是源代码的行号。我们发现之前看到源代码的行号都是从上往下、依次增大的,这很好理解,毕竟一条一条解释嘛。但是这里却发生了变化,先执行了第6行,之后再执行第4行。如果是从Python层面的函数调用来理解的话,很容易一句话就解释了,因为函数只有在调用的时候才会执行。但是从字节码的角度来理解的话,我们发现函数的声明和实现是分离的,是在不同的PyCodeObject对象中。确实如此,虽然一个函数名和函数体是一个整体,但是Python虚拟机在实现这个函数的时候,却在物理上将它们分离开了,构建函数的字节码指令序列必须在模块对应的PyCodeObject对象中。
我们之前说过,函数即变量。我们是可以把函数当成是普通的变量来处理的,函数名就相当于变量名,函数体就相当于是变量指向的值。而foo函数显然是在全局中定义的一个函数,那么foo是不是要出现在py文件对应的PyCodeObject对象的符号表co_names里面呢?foo对应的PyCodeObject对象是不是要出现在py文件对应的PyCodeObject对象的常量池co_consts里面呢?
至此,函数的结构就已经非常清晰了。
所以函数名和函数体是分离的,它们存在不同的PyCodeObject对象当中。分析完结构之后,我们的重点就在于那个MAKE_FUNCTION指令了,我们说当遇到def foo(a, b)
的时候,在语法上将这是函数的声明语句,但是从虚拟机的角度来看这其实是函数对象的创建语句。所以下面我们就要分析一下这个指令,看看它到底是怎么将一个PyCodeObject对象变成一个PyFunctionObject对象的。
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 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之前,先进行了LOAD CONST,显然是将foo对应的字节码对象和符号foo压入到了栈中。所以在执行MAKE FUNCTION的时候,首先就是将这个字节码对象以及对应符号弹出栈,然后再加上当前PyFrameObject对象中维护的global名字空间f_globals对象,三者作为参数传入PyFunction_NewWithQualName函数中,从而构建出相应的PyFunctionObject对象。
下面我们来看看PyFunction_NewWithQualName是如何构造出一个函数的,它位于 *Objects/funcobject.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 68 69 70 71 72 73 74 PyObject * PyFunction_NewWithQualName (PyObject *code, PyObject *globals, PyObject *qualname) { PyFunctionObject *op; PyObject *doc, *consts, *module; static PyObject *__name__ = NULL ; if (__name__ == NULL ) { __name__ = PyUnicode_InternFromString("__name__" ); if (__name__ == NULL ) return NULL ; } 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 ; op->func_kwdefaults = NULL ; op->func_closure = NULL ; op->vectorcall = _PyFunction_Vectorcall; 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_GetItemWithError(globals, __name__); if (module) { Py_INCREF(module); op->func_module = module; } else if (PyErr_Occurred()) { Py_DECREF(op); return NULL ; } 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; }
所以通过MAKE_FUNCTION我们便创建了PyFunctionObject对象,然后它会被压入栈中,再通过STORE_NAME将符号foo和PyFunctionObject对象组成一个entry,存储在当前栈帧的local名字空间中,当然也是global名字空间。只不过为了和函数保持统一,我们都说成local名字空间,只不过不同的作用域对应的local空间是不一样的。
当然了我们说函数对象的类型是<class 'function'>
,但是这个类底层没有暴露给我们,但是我们依旧可以通过曲线救国的方式进行获取。
1 2 3 4 5 6 def f (): pass print (type (f)) print (type (lambda : None ))
所以我们可以仿照底层的思路,通过<class 'function'>
来创建一个函数对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 gender = "female" def f (name, age ): return f"name: {name} , age: {age} , gender: {gender} " code = f.__code__ new_f = type (f)(code, globals (), "根据f创建的new_f" ) print (new_f.__name__) print (new_f("夏色祭" , -1 ))
是不是很神奇呢?另外我们说函数在访问gender指向的对象时,显然先从自身的符号表中找,如果没有那么回去找全局变量。这是因为,我们在创建函数的时候将global名字空间传进去了,如果我们不传递呢?
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 gender = "female" def f (name, age ): return f"name: {name} , age: {age} , gender: {gender} " code = f.__code__ try : new_f = type (f)(code, None , "根据f创建的new_f" ) except TypeError as e: print (e) new_f1 = type (f)(code, {}, "根据f创建的new_f1" ) print (new_f1.__name__) try : print (new_f1("夏色祭" , -1 )) except NameError as e: print (e)
因此现在我们又在Python的角度上理解了一遍,为什么Python中的函数能够在局部变量找不到的时候,去找全局变量,原因就在于构建函数的时候,将global名字空间交给了函数。使得函数可以在global空间进行变量查找,所以它才能够找到全局变量。而我们这里给了一个空字典,那么显然就找不到gender这个变量了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 gender = "female" def f (name, age ): return f"name: {name} , age: {age} , gender: {gender} " code = f.__code__ new_f = type (f)(code, {"gender" : "萌妹子" }, "根据f创建的new_f" ) print (new_f("夏色祭" , -1 ))
此外我们还可以为函数指定默认值:
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 gender = "female" def f (name, age ): return f"name: {name} , age: {age} , gender: {gender} " code = f.__code__ new_f = type (f)(code, {"gender" : "屑女仆" }, "根据f创建的new_f" ) new_f.__defaults__ = ("神乐mea" , 38 ) print (new_f()) new_f1 = type (f)(code, {"gender" : "屑女仆" }, "根据f创建的new_f1" ) new_f1.__defaults__ = (38 ,) try : new_f1() except TypeError as e: print (e) print (new_f1("神楽めあ" )) """ 但是问题来了, 为什么在设置默认值的时候要从后往前呢? 首先如果默认值的个数和参数的个数正好匹配, 那么相安无事, 如果不匹配那么只能是默认值的个数小于参数个数 如果是从后往前, 那么(38,)就意味着38设置为age的默认值, name就必须由我们在调用的时候传递 但如果是从前往后, 那么(38,)就意味着38设置为name的默认值, age就必须由我们在调用的时候来传递 但是问题来了, 如果38设置为name的默认值, 这会是什么情况? 显然等价于: def new_f1(name=38, age): ...... 你认为这样的函数能够通过编译吗?显然是不行的, 因为默认参数必须在非默认参数的后面 """
当然,这种设置默认值的方式显然也可以使用于通过def定义的函数,因为我们上面的new_f、new_f1和f都是<class 'function'>
对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 gender = "female" def f (name, age ): return f"name: {name} , age: {age} , gender: {gender} " print (f.__defaults__) f.__defaults__ = ("夏色祭" , -1 ) print (f())
另外我们说,默认值的个数一定要小于等于参数的个数,但如果大于呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 gender = "female" def f (name, age ): return f"name: {name} , age: {age} , gender: {gender} " print (f.__defaults__) f.__defaults__ = ("夏色祭" , -1 , "神乐mea" , 38 ) print (f())
*想不到Python中的函数可以玩出这么多新花样,现在你是不是对函数有了一个更深刻的认识了呢?当然目前介绍的只是函数的一小部分内容,还有函数如何调用、位置参数和关键字参数如何解析、对于有默认值的参数如何在我们不传递的时候使用默认值以及在我们传递的时候使用我们传递的值、*args和*kwargs又如何解析、闭包怎么做到的、还有装饰器等等等等,这些我们接下来会单独用几篇博客详细说。
因为放在一篇博客里面的话,字数至少要好几万,而我使用的Markdown编辑器typora在字数达到一万五的时候就会出现明显卡顿,要是一下子都写完的话,绝对卡到爆,而且越往后越卡,这对我而言也是个痛苦。而且函数的内容也比较多,我们就多用一些篇幅去介绍它吧。
判断函数都有哪些参数 最后我们再来看看我们如何检测一个函数有哪些参数,首先函数的局部变量(包括参数)
在编译是就已经确定,会存在符号表co_varnames中。
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 def f (a, b, /, c, d, *args, e, f, **kwargs ): g = 1 h = 2 varnames = f.__code__.co_varnames print (varnames)""" 首先co_varnames打印的符号表是有顺序的, 参数永远在函数内部定义的局部变量的前面 g和h就是函数内部定义的局部变量, 所以它在所有的后面 如果是参数的话, 那么*和**会位于最后面, 其它参数位置不变, 所以除了g和h, 最后面的就是args和kwargs """ posonlyargcount = f.__code__.co_posonlyargcount print (posonlyargcount) print (varnames[: posonlyargcount]) argcount = f.__code__.co_argcount print (argcount) print (varnames[: 4 ]) print (varnames[posonlyargcount: 4 ]) kwonlyargcount = f.__code__.co_kwonlyargcount print (kwonlyargcount) print (varnames[argcount: argcount + kwonlyargcount]) """ 在介绍PyCodeObject对象的时候, 我们说里面有一个co_flags成员 它是专门用来判断参数中是否有*args和**kwargs的 """ flags = f.__code__.co_flags step = argcount + kwonlyargcount if flags & 0x04 : print (varnames[step]) step += 1 if flags & 0x08 : print (varnames[step])
如果我们定义的不是*args,只是一个*,那么它就不是参数了。
1 2 3 4 5 6 7 8 9 10 def f (a, b, *, c ): pass print (f.__code__.co_varnames) print (f.__code__.co_flags & 0x04 ) print (f.__code__.co_flags & 0x08 )
小结 这一次我们简单的分析了一下函数在底层对应的数据结构,以及如何创建一个函数,并且还在Python的层面上做了一些小trick。最后我们也分析了如何通过PyCodeObject对象来检索Python中的参数,以及相关种类,当然标准库中的inspect模块也是这么做的。当然说白了,其实是我们模仿人家的思路做的。