From dfd1016bc4f6a209aee135bb8fff0a95740cea45 Mon Sep 17 00:00:00 2001 From: satori1995 Date: Fri, 27 Dec 2024 18:15:22 +0800 Subject: [PATCH] =?UTF-8?q?=E5=87=BD=E6=95=B0=E6=98=AF=E5=A6=82=E4=BD=95?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E5=85=B3=E9=94=AE=E5=AD=97=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E7=9A=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...02\346\225\260\347\232\204\357\274\237.md" | 214 ++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 "src/69.\345\207\275\346\225\260\346\230\257\345\246\202\344\275\225\350\247\243\346\236\220\345\205\263\351\224\256\345\255\227\345\217\202\346\225\260\347\232\204\357\274\237.md" diff --git "a/src/69.\345\207\275\346\225\260\346\230\257\345\246\202\344\275\225\350\247\243\346\236\220\345\205\263\351\224\256\345\255\227\345\217\202\346\225\260\347\232\204\357\274\237.md" "b/src/69.\345\207\275\346\225\260\346\230\257\345\246\202\344\275\225\350\247\243\346\236\220\345\205\263\351\224\256\345\255\227\345\217\202\346\225\260\347\232\204\357\274\237.md" new file mode 100644 index 0000000..35d1d9b --- /dev/null +++ "b/src/69.\345\207\275\346\225\260\346\230\257\345\246\202\344\275\225\350\247\243\346\236\220\345\205\263\351\224\256\345\255\227\345\217\202\346\225\260\347\232\204\357\274\237.md" @@ -0,0 +1,214 @@ +## 楔子 + +上一篇文章介绍了位置参数,下面来看一看关键字参数。另外函数还支持默认值,我们就放在一起介绍吧。 + +## 函数的默认值 + +简单看一个函数: + +~~~python +import dis + +code = """ +def foo(a=1, b=2): + print(a + b) + +foo() +""" + +dis.dis(compile(code, "", "exec")) +~~~ + +字节码指令如下: + +~~~C + // 构造函数的时候,默认值会被提前压入运行时栈 + 0 LOAD_CONST 5 ((1, 2)) + 2 LOAD_CONST 2 () + 4 LOAD_CONST 3 ('foo') + 6 MAKE_FUNCTION 1 (defaults) + 8 STORE_NAME 0 (foo) + // ... +~~~ + +相比无默认值的函数,有默认值的函数在加载 PyCodeObject 和函数名之前,会先将默认值以元组的形式给加载进来。 + +然后再来观察一下构建函数用的 MAKE_FUNCTION 指令,我们发现指令参数是 1,而之前都是 0,那么这个 1 代表什么呢?根据提示,我们看到了一个 defaults,它和函数的 func_defaults 有什么关系吗?带着这些疑问,我们再来回顾一下这个指令: + +~~~C +case TARGET(MAKE_FUNCTION): { + // 对于当前例子来说,栈里面有三个元素 + // 从栈顶到栈底分别是:函数名、PyCodeObject、默认值 + PyObject *qualname = POP(); // 弹出栈顶元素,得到函数名 + PyObject *codeobj = POP(); // 弹出栈顶元素,得到 PyCodeObject + + // ... + 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(); + } + // 当前 oparg 是 1,和 0x01 按位与的结果为真,所以知道函数有默认值 + // 于是将其从栈顶弹出,保存在函数的 func_defaults 字段中 + if (oparg & 0x01) { + assert(PyTuple_CheckExact(TOP())); + func->func_defaults = POP(); + } + + PUSH((PyObject *)func); + DISPATCH(); +} +~~~ + +通过以上命令可以很容易看出,该指令创建函数对象时,还会处理参数的默认值、以及类型注解等。另外 MAKE_FUNCTION 的指令参数只能表示要构建的函数存在默认值,但具体有多少个是看不到的,因为所有的默认值会按照顺序塞到一个 PyTupleObject 对象里面。 + +然后将默认值组成的元组用 func_defaults 字段保存,在 Python 层面可以使用 \_\_defaults\_\_ 访问。如此一来,默认值也成为了 PyFunctionObject 对象的一部分,它和 PyCodeObject 对象、global 名字空间一样,也被塞进了 PyFunctionObject 这个大包袱。 + +> 所以说 PyFunctionObject 这个嫁衣做的是很彻底的,工具人 PyFunctionObject,给个赞。 + +~~~python +def foo(a=1, b=2): + print(a + b) +~~~ + +然后我们还是以这个 foo 函数为例,看看不同的调用方式对应的底层实现。 + +## 执行 foo() + +由于函数参数都有默认值,此时可以不传参,看看这种方式在底层是如何处理的? + +~~~C +// Objects/call.c + +PyObject * +_PyFunction_Vectorcall(PyObject *func, PyObject* const* stack, + size_t nargsf, PyObject *kwnames) +{ + // ... + + // 判断能否进入快速通道,首先要满足函数定义时,参数不可以出现 / 和 *,并且内部不能出现闭包变量 + // 然后调用时不能使用关键字参数 + if (co->co_kwonlyargcount == 0 && nkwargs == 0 && + (co->co_flags & ~PyCF_MASK) == (CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE)) + { + // 上面的 if 虽然满足了,但是还不够,还要保证函数参数不能有默认值 + 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)) { + /* function called with no arguments, but all parameters have + a default value: use default values as arguments .*/ + stack = _PyTuple_ITEMS(argdefs); + return function_code_fastcall(co, stack, PyTuple_GET_SIZE(argdefs), + globals); + } + // 总的来说,以上两个条件都挺苛刻的 + } + + // ... + // 否则进入通用通道 + return _PyEval_EvalCodeWithName((PyObject*)co, globals, (PyObject *)NULL, + stack, nargs, + nkwargs ? _PyTuple_ITEMS(kwnames) : NULL, + stack + nargs, + nkwargs, 1, + d, (int)nd, kwdefs, + closure, name, qualname); +} +~~~ + +对于当前执行的 foo() 来说,由于参数都有默认值,并且此时也没有传参,因此会进入快速通道。而快速通道之前已经介绍过了,这里就不再说了,总之想要进入快速通道,条件还是蛮苛刻的。 + +## 执行 foo(1) + +显然此时就走不了快速通道了,会进入通用通道。此时重点就落在了 _PyEval_EvalCodeWithName 函数中,我们看一下它的逻辑。注意:该函数的逻辑较为复杂,理解起来会比较累,可能需要多读几遍。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +