-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
80d2896
commit dfd1016
Showing
1 changed file
with
214 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,214 @@ | ||
## 楔子 | ||
|
||
上一篇文章介绍了位置参数,下面来看一看关键字参数。另外函数还支持默认值,我们就放在一起介绍吧。 | ||
|
||
## 函数的默认值 | ||
|
||
简单看一个函数: | ||
|
||
~~~python | ||
import dis | ||
|
||
code = """ | ||
def foo(a=1, b=2): | ||
print(a + b) | ||
foo() | ||
""" | ||
|
||
dis.dis(compile(code, "<func>", "exec")) | ||
~~~ | ||
|
||
字节码指令如下: | ||
|
||
~~~C | ||
// 构造函数的时候,默认值会被提前压入运行时栈 | ||
0 LOAD_CONST 5 ((1, 2)) | ||
2 LOAD_CONST 2 (<code object foo at 0x7f3...>) | ||
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 函数中,我们看一下它的逻辑。注意:该函数的逻辑较为复杂,理解起来会比较累,可能需要多读几遍。 | ||