From f190b76a3fe249d57c1010fd409213232c5c573f Mon Sep 17 00:00:00 2001 From: satori1995 Date: Thu, 26 Dec 2024 13:36:36 +0800 Subject: [PATCH] =?UTF-8?q?=E5=87=BD=E6=95=B0=E6=98=AF=E6=80=8E=E4=B9=88?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E7=9A=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...02\345\270\270\347\232\204\357\274\237.md" | 66 ++--- ...10\346\240\267\345\255\220\357\274\237.md" | 240 ++++++++++++++++++ 2 files changed, 273 insertions(+), 33 deletions(-) create mode 100644 "src/65.\345\207\275\346\225\260\345\234\250\345\272\225\345\261\202\351\225\277\344\273\200\344\271\210\346\240\267\345\255\220\357\274\237.md" diff --git "a/src/64.\350\231\232\346\213\237\346\234\272\346\230\257\345\246\202\344\275\225\346\215\225\350\216\267\345\274\202\345\270\270\347\232\204\357\274\237.md" "b/src/64.\350\231\232\346\213\237\346\234\272\346\230\257\345\246\202\344\275\225\346\215\225\350\216\267\345\274\202\345\270\270\347\232\204\357\274\237.md" index 571352e..7342712 100644 --- "a/src/64.\350\231\232\346\213\237\346\234\272\346\230\257\345\246\202\344\275\225\346\215\225\350\216\267\345\274\202\345\270\270\347\232\204\357\274\237.md" +++ "b/src/64.\350\231\232\346\213\237\346\234\272\346\230\257\345\246\202\344\275\225\346\215\225\350\216\267\345\274\202\345\270\270\347\232\204\357\274\237.md" @@ -330,7 +330,7 @@ dis.dis(compile(code_string, "exception", "exec")) 但我们知道无论是哪种情况,都要执行 finally,所以开头有两个 SETUP_FINALLY 指令,但为什么会有两个呢?因为在 Python 的异常处理机制中,try-except-finally 结构会被编译成两个嵌套的异常处理块: + 第一个 SETUP_FINALLY 是为了处理 finally 块,会把 finally 块的地址压入栈中,它确保无论 try 块中是否发生异常,finally 块中的代码都会被执行; -+ 第二个 SETUP_FINALLY 实际上是在处理 except 块,在 Python 的字节码层面,except 块也是通过 SETUP_FINALLY 来实现的; ++ 第二个 SETUP_FINALLY 实际上是在处理 except 块,在 Python 的字节码层面,except 块也是通过 SETUP_FINALLY 实现的; 我们来看一下 SETUP_FINALLY 指令都干了什么。 @@ -364,7 +364,7 @@ PyFrame_BlockSetup(PyFrameObject *f, int type, int handler, int level) if (f->f_iblock >= CO_MAXBLOCKS) Py_FatalError("XXX block stack overflow"); // 栈帧有一个 f_blockstack 字段,它是 PyTryBlock 类型的数组 - // 当栈帧创建完毕之后,f_blockstack 的内存就已经申请好了 + // 当栈帧创建完毕后,f_blockstack 的内存就已经申请好了 // 因此当需要创建 PyTryBlock 实例时,只需从 f_blockstack 里面获取即可 b = &f->f_blockstack[f->f_iblock++]; // 设置 PyTryBlock 实例的字段,这个结构体一会儿说 @@ -391,7 +391,7 @@ PyFrame_BlockSetup(PyFrameObject *f, int type, int handler, int level) 结果没有问题,当然啦,如果不是恶意代码,我个人认为不会存在嵌套层级如此之深的异常捕获。 -然后再来看看里面的 f_iblock,它表示对应的 PyTryBlock 在 f_blockstack 数组中的索引。栈帧刚创建时,f_iblock 的值为 0,每当执行 SETUP_FINALLY 执行时,就会从 f_blockstack 数组中获取一个 PyTryBlock,然后 f_iblock 自增 1。那么 PyTryBlock 长什么样子呢? +然后再来看看里面的 f_iblock,它表示对应的 PyTryBlock 在 f_blockstack 数组中的索引。栈帧刚创建时,f_iblock 的值为 0,每当执行 SETUP_FINALLY 指令时,就会从 f_blockstack 数组中获取一个 PyTryBlock,然后 f_iblock 自增 1。那么 PyTryBlock 长什么样子呢? ~~~C // Include/frameobject.h @@ -402,14 +402,14 @@ typedef struct { } PyTryBlock; ~~~ -b_type 表示 block 的种类,因为存在着多种用途的 PyTryBlock 对象,除了 SETUP_FINALLY 之外还有 SETUP_WITH 等。然后在 PyFrame_BlockSetup 中我们看到它被设置成了参数 type,而参数 type 接收的就是当前虚拟机正在执行的字节码指令。因此 PyTryBlock 具有多种用途,具体用于哪种则基于字节码指令进行判断。 +b_type 表示 block 的种类,因为存在多种用途的 PyTryBlock 对象,除了 SETUP_FINALLY 之外还有 SETUP_WITH 等。然后在 PyFrame_BlockSetup 中我们看到它被设置成了参数 type,而参数 type 接收的就是当前虚拟机正在执行的字节码指令。因此 PyTryBlock 具有多种用途,具体用于哪种则基于字节码指令进行判断。 b_handler 表示处理程序的字节码偏移量,即跳转目标,比如当前的 try 语句块,当 try 执行完了或者执行出错后,要跳转到什么位置呢?在 SETUP_FINALLY 指令中我们看到它被设置成了 INSTR_OFFSET() + oparg,所以它会跳转到 finally 或 except 所在的位置。 b_level 表示进入 try 语句块时的 STACK_LEVEL(),即运行时栈的深度,或者说当前运行时栈的元素个数,因为当发生异常时,要把运行时栈恢复到进入 try 语句块时的状态。 ~~~python -# 假设此时运行是时栈深度是 n +# 假设此时运行时栈的深度是 n try: # SETUP_FINALLY 执行时 b_level = n ... # 执行内部逻辑 raise Exception("error") # 在这里不幸发生异常,那么要将栈恢复到 b_level 记录的深度 n @@ -430,7 +430,7 @@ except Exception: ![](./images/207.png) -取出两块 PyTryBlock,第一块对应 finally,会无条件执行,第二块对应 except,异常捕获的时候用。至于具体做的稍后再说,我们先回到抛异常的地方看看。 +取出两块 PyTryBlock,第一块对应 finally,会无条件执行,第二块对应 except,异常捕获的时候用。至于具体怎么做的稍后再说,我们先回到抛异常的地方看看。 ~~~C // 加载 @@ -453,7 +453,7 @@ case TARGET(RAISE_VARARGS): { // raise 一个异常有三种方式 // 1)重新抛出当前异常,只写一个 raise 即可,此时 oparg = 0 // 2)抛出指定异常:raise exc,此时 oparg = 1 - // 3)抛出执行异常并指定原因:raise exc from cause,此时 oparg = 2 + // 3)抛出指定异常并指定原因:raise exc from cause,此时 oparg = 2 // 所以当前的 oparg = 1 switch (oparg) { case 2: @@ -489,8 +489,8 @@ exception_unwind: PyTryBlock *b = &f->f_blockstack[--f->f_iblock]; // EXCEPT_HANDLER 定义在 opcode.h 中,值为 257,用于异常处理 - // 但比较特殊的是,它不是一个指令,这么做的原因后续会看到 - if (b->b_type == EXCEPT_HANDLER) { + // 但比较特殊的是,它不是一个指令,至于相关细节后续会看到 + if (b->b_type == EXCEPT_HANDLER) { // 如果两者相等的话 // 这是一个宏,所做的事情如下 // 从线程状态对象中拿到 exc_info,这个和 python 里的 sys.exc_info() 等价 // 然后从栈顶弹出 exc_type、exc_value、exc_traceback,并设置在 exc_info 中 @@ -558,7 +558,7 @@ exception_unwind: PUSH(tb); PUSH(val); PUSH(exc); - // 绝对跳转,跳转偏移量为 handler 的指令 + // 绝对跳转,跳转到偏移量为 handler 的指令 JUMPTO(handler); /* Resume normal execution */ goto main_loop; @@ -573,9 +573,9 @@ assert(retval == NULL); assert(_PyErr_Occurred(tstate)); ~~~ -我们看到虚拟机调用 PUSH 将旧异常和新异常的 exc_traceback、exc_value、exc_type 分别压入运行时栈中,并且知道此时开发者已经为异常处理做好了准备,所以接下来的异常处理工作,则需要交给开发者指定的代码来解决。于是内部调用了 `JUMPTO(b->b_handler)`,将虚拟机将要执行的下一条指令设置为异常处理代码编译后所得到的第一条字节码指令。 +我们看到虚拟机调用 PUSH 将旧异常和新异常的 exc_traceback、exc_value、exc_type 分别压入运行时栈中,并且知道此时开发者已经为异常处理做好了准备,所以接下来的异常处理工作,则需要交给开发者指定的代码来解决。于是内部调用了 `JUMPTO(b->b_handler)`,将虚拟机要执行的下一条指令设置为异常处理代码编译后所得到的第一条字节码指令。 -因为第一个弹出的是 PyTryBlock 的 b_handler 为 16,那么虚拟机将要执行的下一条指令就是偏移量为 16 的那条指令,而这条指令就是 DUP_TOP,异常处理代码对应的第一条字节码指令。 +因为第一个弹出的 PyTryBlock 的 b_handler 为 16,那么虚拟机将要执行的下一条指令就是偏移量为 16 的指令,而这条指令就是 DUP_TOP。 ~~~C // 在上面的 exception_unwind 标签中调用了 6 个 PUSH @@ -603,12 +603,12 @@ assert(_PyErr_Occurred(tstate)); // 目前栈里面有 6 个元素,栈顶元素是新异常的 exc_type // 所以 POP_TOP 之后,栈顶元素就变成了新异常的 exc_value 24 POP_TOP - // 弹出栈顶的 exc_value,赋值给变量 e,导致 except Exception as e 完成 + // 弹出栈顶的 exc_value,赋值给变量 e,到此 except Exception as e 完成 // 我们看到这个过程其实也是一个变量赋值,字节码为 STORE_NAME 26 STORE_NAME 2 (e) // 继续弹出栈顶元素,此时是新异常的 exc_traceback 28 POP_TOP - // 这里为啥又出现一个 SETUP_FINALLY 指令,很简单 + // 这里为啥又出现一个 SETUP_FINALLY 指令?很简单 /* try: pass @@ -625,7 +625,7 @@ assert(_PyErr_Occurred(tstate)); finally: del e */ - // 所以这里会多出一个 SETUP_FINALLY,因为内部有嵌套了一个 try ... finally + // 所以这里会多出一个 SETUP_FINALLY,因为内部又嵌套了一个 try ... finally // 至于这么做的原因,我们稍后解释 30 SETUP_FINALLY 12 (to 44) // 以下四条指令对应 except 子句内的 print(e) @@ -633,11 +633,11 @@ assert(_PyErr_Occurred(tstate)); 34 LOAD_NAME 2 (e) 36 CALL_FUNCTION 1 38 POP_TOP - // except 子句执行完毕,调用 POP_BLOCK + // except 子句里的代码执行完毕,调用 POP_BLOCK // 该指令内部会交还 PyTryBlock,然后 f_iblock-- - // 注意:这一步交换的 PyTryBlock,是 except 内部的 finally 对应的 PyTryBlock + // 注意:这一步交还的 PyTryBlock,是 except 内部的 finally 对应的 PyTryBlock 40 POP_BLOCK - // BEGIN_FINALLY 会往栈顶 PUSH 一个 NULL 进去,表示 finally 的开始 + // BEGIN_FINALLY 会往栈顶 PUSH 一个 NULL 进去,标识 finally 的开始 // 注意:这个 finally 是 except 子句内部的 finally,是解释器自动生成的 42 BEGIN_FINALLY // 将 e 赋值为 None,然后将 e 删除掉 @@ -645,8 +645,8 @@ assert(_PyErr_Occurred(tstate)); 46 STORE_NAME 2 (e) 48 DELETE_NAME 2 (e) 50 END_FINALLY - // 到此整个 except 执行完毕,执行 POP_EXCEPT,交还对应的 PyTryBlock - // 然后在该指令内部还会恢复之前保存的异常状态,我们上面看到,解析往运行时栈里面推了 6 个元素 + // 到此整个 except 执行完毕,然后执行 POP_EXCEPT,交还对应的 PyTryBlock + // 然后在该指令内部还会恢复之前保存的异常状态,我们上面看到,解释器往运行时栈里面推了 6 个元素 // 前 3 个已经被弹出了,还剩下旧异常的 exc_type、exc_value、exc_traceback // 那么将它们继续弹出,赋值给 exc_info 里面的字段,相当于恢复成之前的异常状态 52 POP_EXCEPT @@ -654,24 +654,24 @@ assert(_PyErr_Occurred(tstate)); 54 JUMP_FORWARD 2 (to 58) ~~~ -上面的指令有点多,我们再单独解释一下,首先在 exception_unwind 标签内部获取 `tstate->exc_info` 的三个字段,它们是旧异常的 exc_type、exc_value、exc_traceback,然后压入运行时栈。然后通过 _PyErr_Fetch 获取 tstate 里面的新异常的 exc_type、exc_value、exc_traceback,用它们更新 `tstate->exc_info`,然后再压入运行时栈。 +上面的内容有点多,我们再单独解释一下,首先在 exception_unwind 标签内部获取 `tstate->exc_info` 的三个字段,它们是旧异常的 exc_type、exc_value、exc_traceback,然后压入运行时栈。再通过 _PyErr_Fetch 获取 tstate 里面的新异常的 exc_type、exc_value、exc_traceback,用它们更新 `tstate->exc_info`,然后再压入运行时栈。 所以此时运行时栈里面有 6 个元素。 ![](./images/208.png) -然后使用 JUMPTO 跳转到偏移量为 `b->b_handler` 的指令,对于当前来说就是 except 子句对应的指令。 +然后使用 JUMPTO 跳转到偏移量为 `b->b_handler` 的指令,对于当前来说就是 except 子句对应的指令,然后执行。 -+ 首先通过 DUP_TOP 指令将栈顶元素拷贝一份并入栈。 ++ 通过 DUP_TOP 将栈顶元素拷贝一份并入栈。 + 通过 LOAD_NAME 将 except 子句指定的异常类型入栈。 -+ 通过 COMPARE_OP 将栈顶进行比较,将比较结果入栈。 ++ 通过 COMPARE_OP 将栈顶的两个元素弹出,进行比较,将比较结果入栈。 + 再通过 POP_JUMP_IF_FALSE 将比较结果弹出,进行判断,由于新异常的 exc_type 和 except 子句指定的异常类型都是 Exception,所以结果为 True。 我们画张图,展示一下运行时栈的变化过程。 ![](./images/209.png) -然后是 30 SETUP_FINALLY,原因我们也解释了。 +然后是 30 SETUP_FINALLY,至于为什么会多出这条指令,原因我们也解释了。 ~~~Python e = 2.71 @@ -698,7 +698,7 @@ except Exception as e: del e ~~~ -所以会又产生一个 SETUP_FINALLY 指令,至于解释器这么做的动机我们稍后解释。 +所以又产生了一个 SETUP_FINALLY 指令,至于解释器这么做的动机我们稍后解释。 然后偏移量为 32、34、36、38 的指令就无需解释了,这几条指令执行完毕后,except 语句块里的代码就执行完了。然后执行 40 POP_BLOCK,它会交还当前 except 内部的 finally 语句块对应的 PyTryBlock,然后将 f_iblock 自减 1。 @@ -774,7 +774,7 @@ case TARGET(POP_EXCEPT): { 74 RETURN_VALUE ~~~ -因此在异常机制的实现中,最重要的就是线程状态以及栈帧对象中 f_blockstack 里存放的 PyTryBlock 对象。首先根据线程状态可以判断当前是否发生了异常,而 PyTryBlock 对象则告诉虚拟机,开发者是否为异常设置了except 和 finally,虚拟机异常处理的流程就是在线程所处的状态和 PyTryBlock 的共同作用下完成的。 +因此在异常机制的实现中,最重要的就是线程状态以及栈帧对象的 f_blockstack 字段里面存放的 PyTryBlock 对象。首先根据线程状态可以判断当前是否发生了异常,而 PyTryBlock 对象则告诉虚拟机,开发者是否为异常设置了 except 和 finally,虚拟机异常处理的流程就是在线程所处的状态和 PyTryBlock 的共同作用下完成的。 当然啦,我们这里分析的是异常能捕获的情况,如果不能捕获呢?具体细节可以自己写段代码测试一下,这里我们直接画张图总结一下。 @@ -818,13 +818,13 @@ Traceback (most recent call last): File "/Users/.../main.py", line 2, in h 1 / 0 -# 函数 h 的 traceback -> tb_next 为 None,证明错误是发生在函数 h 中 -# 在模块中调用函数 f 相当于导火索,然后一层一层输出,最终定位到函数 h -# 然后再将之前设置在线程状态对象中的异常类型和异常值打印出来即可 +# 函数 h 的 traceback->tb_next 为 None,证明错误是发生在函数 h 中 +# 而在模块中调用函数 f 相当于导火索,然后一层一层输出,最终定位到函数 h +# 最后再将之前设置在线程状态对象中的异常类型和异常值打印出来即可 ZeroDivisionError: division by zero ~~~ -模块中调用了函数 f,函数 f 调用了函数 g,函数 g 调用了函数 h。然后在函数 h 中执行出错了,但又没有异常捕获,那么会将执行权交给函数 g 对应的栈帧,但是函数 g 也没有异常捕获,那么再将执行权交给函数 f 对应的栈帧。所以调用的时候栈帧一层一层创建,当执行完毕或者出现异常时,栈帧再一层层回退。 +模块中调用了函数 f,函数 f 调用了函数 g,函数 g 调用了函数 h。然后在函数 h 中执行出错了,但又没有异常捕获,那么会将执行权交给函数 g 对应的栈帧,但是函数 g 也没有异常捕获,那么再将执行权交给函数 f 对应的栈帧。所以调用的时候,栈帧一层层创建,当执行完毕或者出现异常时,栈帧再一层层回退。 ![](./images/211.png) @@ -851,7 +851,7 @@ except Exception as e: # 而如果不 del e 的话,那么异常对象不会被销毁 # 此外还有一个原因,通过 __traceback__ 可以拿到当前的回溯栈,即 traceback 对象 print(e.__traceback__) # - # 而 traceback 对象保存当前的栈帧,然后栈帧又保存了包含变量 e 的名字空间 + # 而 traceback 对象保存了当前的栈帧,然后栈帧又保存了包含变量 e 的名字空间 print(e.__traceback__.tb_frame.f_locals["e"] is e) # True # 相信你能猜到这会带来什么后果,没错,就是循环引用 # 因此在 except 结束时会隐式地 del e @@ -867,7 +867,7 @@ NameError: name 'e' is not defined ## 小结 -本篇文章我们就分析了异常捕获的实现原理,总的来说并不难。另外在更高的版本中,所有的信息会静态保存在一张异常跳转表中,速度会更快。当然啦,在不报错时,异常捕获对程序性能没有任何影响,所以放心使用。 +本篇文章我们就分析了异常捕获的实现原理,总的来说并不难。另外在更高的版本中,所有的信息会静态保存在一张异常跳转表中,速度会更快。当然啦,在没有报错时,异常捕获对程序性能没有任何影响,所以放心使用。 ---- diff --git "a/src/65.\345\207\275\346\225\260\345\234\250\345\272\225\345\261\202\351\225\277\344\273\200\344\271\210\346\240\267\345\255\220\357\274\237.md" "b/src/65.\345\207\275\346\225\260\345\234\250\345\272\225\345\261\202\351\225\277\344\273\200\344\271\210\346\240\267\345\255\220\357\274\237.md" new file mode 100644 index 0000000..2e3d03c --- /dev/null +++ "b/src/65.\345\207\275\346\225\260\345\234\250\345\272\225\345\261\202\351\225\277\344\273\200\344\271\210\346\240\267\345\255\220\357\274\237.md" @@ -0,0 +1,240 @@ +## 楔子 + +函数是任何一门编程语言都具备的基本元素,它可以将多个动作组合起来,一个函数代表了一系列的动作。而且在调用函数时会干什么来着,没错,要创建栈帧,用于函数的执行。 + +那么下面就来看看函数在 C 中是如何实现的,生得一副什么模样。 + +## PyFunctionObject + +Python 一切皆对象,函数也不例外,函数这种抽象机制在底层是通过 PyFunctionObject 结构体实现的。 + +~~~C +// Include/funcobject.h + +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; +~~~ + +我们来解释一下这些字段,并实际获取一下,看看它们在 Python 中是如何表现的。 + +**func_code:函数对应的 PyCodeObject 对象** + +~~~python +def foo(a, b, c): + pass + +code = foo.__code__ +print(code) # +print(code.co_varnames) # ('a', 'b', 'c') +~~~ + +函数便是基于 PyCodeObject 构建的。 + +**func_globals:global 名字空间** + +~~~python +def foo(a, b, c): + pass + +name = "古明地觉" +print(foo.__globals__) # {..., 'name': '古明地觉'} +# 拿到的其实就是外部的 global 名字空间 +print(foo.__globals__ is globals()) # True +~~~ + +函数内部之所以可以访问全局变量,就是因为它保存了全局名字空间。 + +**func_defaults:函数参数的默认值** + +~~~python +def foo(name="古明地觉", age=16): + pass +# 打印的是默认值 +print(foo.__defaults__) # ('古明地觉', 16) + +def bar(): + pass +# 没有默认值的话,__defaults__ 为 None +print(bar.__defaults__) # None +~~~ + +注:默认值只会创建一次,所以默认值不应该是可变对象。 + +**func_kwdefaults:只能通过关键字参数传递的 "参数" 和 "该参数的默认值" 组成的字典** + +~~~python +def foo(name="古明地觉", age=16): + pass +# 打印为 None,这是因为虽然有默认值 +# 但并不要求必须通过关键字参数的方式传递 +print(foo.__kwdefaults__) # None + +def bar(name="古明地觉", *, age=16): + pass +print(bar.__kwdefaults__) # {'age': 16} +~~~ + +加上一个 \* 表示后面的参数必须通过关键字的方式传递。 + +**func_closure:一个元组,包含了内层函数使用的外层作用域的变量,即 cell 变量。** + +~~~python +def foo(): + name = "古明地觉" + age = 17 + + def bar(): + print(name, age) + + return bar + + +# 内层函数 bar 使用了外层作用域中的 name、age 变量 +print(foo().__closure__) +""" +(, + ) +""" + +print(foo().__closure__[0].cell_contents) # 17 +print(foo().__closure__[1].cell_contents) # 古明地觉 +~~~ + +注意:查看闭包属性使用的是内层函数。 + +**func_doc:函数的 docstring** + +~~~python +def foo(): + """ + hi,欢迎来到我的小屋 + 遇见你真好 + """ + pass + +print(foo.__doc__) +""" + hi,欢迎来到我的小屋 + 遇见你真好 +""" +~~~ + +当我们在写 Python 扩展的时候,由于编译之后是一个 pyd,那么就会通过 docstring 来描述函数的相关信息。 + +**func_name:函数的名字** + +~~~python +def foo(name, age): + pass + +print(foo.__name__) # foo +~~~ + +当然不光是函数,还有方法、类、模块等都有自己的名字。 + +~~~python +import numpy as np + +print(np.__name__) # numpy +print(np.ndarray.__name__) # ndarray +print(np.array([1, 2, 3]).transpose.__name__) # transpose +~~~ + +除了 func_name 之外,函数还有一个 func_qualname 字段,表示全限定名。 + +~~~python +print(str.join.__name__) # join +print(str.join.__qualname__) # str.join +~~~ + +函数如果定义在类里面,那么它就叫类的成员函数,当然它本质上依旧是个函数,和普通函数并无区别。只是在获取全限定名的时候,会带上类名。 + +**func_dict:函数的属性字典** + +```python +def foo(name, age): + pass + +print(foo.__dict__) # {} +``` + +函数在底层也是由一个类实例化得到的,所以它也可以有自己的属性字典,只不过这个字典一般为空。 + +**func_weakreflist:弱引用列表** + +这里不做讨论。 + +**func_module:函数所在的模块** + +~~~python +import numpy as np + +print(np.array.__module__) # numpy +~~~ + +除了函数之外,类、方法、协程也有 \_\_module\_\_ 属性。 + +**func_annotations:函数的类型注解** + +~~~python +def foo(name: str, age: int): + pass + +# Python3.5 新增的语法,但只能用于函数参数 +# 而在 3.6 的时候,声明变量也可以使用这种方式 +# 特别是当 IDE 无法得知返回值类型时,便可通过类型注解的方式告知 IDE +# 这样就又能使用 IDE 的智能提示了 +print( + foo.__annotations__ +) # {'name': , 'age': } +~~~ + +像 FastAPI、Pydantic 等框架,都大量应用了类型注解。 + +**vectorcall:矢量调用协议** + +函数本质上也是一个实例对象,在调用时会执行类型对象的 tp_call,对应 Python 里的 \_\_call\_\_。但 tp_call 属于通用逻辑,而通用往往也意味着平庸,tp_call 在执行时需要创建临时元组和临时字典来存储位置参数、关键字参数,这些临时对象增加了内存分配和垃圾回收的开销。 + +如果只是一般的实例对象倒也没什么,但函数不同,它作为实例对象注定是要被调用的。所以底层对它进行了优化,引入了速度更快的 vectorcall,即矢量调用。而一个实例对象如果支持矢量调用,那么它也必须支持普通调用,并且两者的结果是一致的,如果对象不支持矢量调用,那么会退化成普通调用。 + +## 小结 + +以上就是函数的底层结构,在 Python 里面是由 \ 实例化得到的。 + +```python +def foo(name, age): + pass + +# 就是 C 里面的 PyFunction_Type +print(foo.__class__) # +``` + +但这个类底层没有暴露给我们,所以不能直接用,因为函数通过 def 创建即可,不需要通过类型对象来创建。 + +后续会介绍更多关于函数相关的知识。 + +------ + +  + +**欢迎大家关注我的公众号:古明地觉的编程教室。** + +![](./images/qrcode_for_gh.jpg) + +**如果觉得文章对你有所帮助,也可以请作者吃个馒头,Thanks♪(・ω・)ノ。** + +![](./images/supports.png) \ No newline at end of file