作者gmccntzx1 (o.O)
看板Python
标题Re: [问题] None在def中的变化
时间Sat Apr 4 16:28:44 2020
这个**问题**其实被蛮多人讨论过了,但比起争论它是否是 Python 的设计瑕疵,我认为
深入了解其背後的运作原理是更值得做的事情。
这边我先以原 PO 所问的这个段内容起头:
> ... 想请问为什麽 ex2 里引述预设值改为 None 时,不会发生印出的内容包含前一次
> 呼叫内容,第一次输出['a']後,result不是已经变成['a']了吗 ...
简单版的回答是:
在那个 function 内所用到的 `result` 一开始指向的是 `None`,如同 signature
里的 `result=None` 所表示。所以在执行 `if result is None:` 时,所得到的结
果是 True ,因此 `result` 就如下一行一样,被重新指向一个新的 empty list。
(延伸阅读: name binding,
https://nedbatchelder.com/text/names.html )
详细版的回答:
一开始, `result` 指向的物件是存在 local scope 里的。而因为该物件是 `None`
,所以 `result` 也就是指向 `None`。而要知道一开始 local scope 内有什麽东西
,你可以在 if 的上一行加上 `print(locals())` 来观察。
为求接下来撰文方便,我们用官方文件中的例子来说明,请参考下图:
https://i.imgur.com/yeMxEP9.png
程式执行到 `print(f(1))` 时,`print` 里的 function call `f(1)` 会先被执
行。而因为 `f` 第一个参数 `a` 所拿到的值是 1,而 `L` 没有被指定,所以进入
function 後,`locals()` 回传的内容会是 `{'a': 1, 'L': []}`。
在执行 `L.append(a)` 之後,`L` 这个 list 的内容变成了 `[1]`。但是,记得
前面提到的 name binding 吗?由於 `L` 指向的正是 local scope 的那个 `L`,
所以如果接着再呼叫一次 `locals()`,回传的内容会是 `{'a': 1, 'L': [1]}`。
因此执行到 `print(f(2))` 时,由於稍早在 `f` 的 local scope 内的 `L` 已经
被改变了,所以这时候 `print(locals())` 里看到的 `L` 就是已经被改变的状态。
(不过使用 `locals()` 来观察一个 function 被执行时其 local scope 的内容并
不完全适合,**详细的原因後续会再说明**。)
但是这跟 mutable/immutable object 有关系吗?以这个例子来说其实不太适合,
让我们将它稍微改写成以下两个版本:
- mutable default argument
https://i.imgur.com/ole5dma.png
- immutable default argument
https://i.imgur.com/f13zzlx.png
这样一来,就可以很明显地了解这个问题跟使用 mutable/immutable object 作为
预设值的差别了。
然而,我们知道了 `locals()` 的用处,那是否可以用它来做些有趣的事情呢?
譬如,直接使用 `locals()` 去修改预设值(暂不考虑有传入 `L` 的情况)?
https://i.imgur.com/Wozmmqy.png
很抱歉,失败了。原因有点复杂,恕我在此省略。但其实这点在官方文件也有提到
> Note: The contents of this dictionary should not be modified;
> changes may not affect the values of local and free variables used
> by the interpreter.
https://docs.python.org/3/library/functions.html#locals
但有没有其他办法去达到这个目的呢?其实还是有的,方法如下
https://i.imgur.com/PT83bOF.png
不过很明显地,比起这麽做,倒不如用常见的方法:将预设值设为 `None`,然後在函
数内用 if 判断以重新设定。
稍微扯远了。这个问题的根本还是需要回到 "参数初始化的方式" 来讨论。原因也如同
官方文件所提到的
> ... The default value is evaluated only once ...
https://docs.python.org/3/tutorial/controlflow.html#more-on-defining-functions
还有,如果 `result` 从一开始就没有被定义在 function signature 里的话,在
local scope 内就不会 `result`。在这种情况下,便会循着所谓的 LEGB rule (
local, enclosed, global, built-in) 去做 name resolution 。
(延伸阅读: LEGB rule,
https://sebastianraschka.com/Articles/2014_python_scope_and_namespaces.html )
----------------------------------------------------------------------------
但光是这样的回答,相信还无法满足热血的版众。其中官方文件的那句
> ... The default value is evaluated only once ...
没有被解答的话,一定让人觉得心痒痒的。
## 深入又不太深入地检视 CPython 的执行过程
要了解官方文件的那句话,我们得先了解 Python 是如何执行一个脚本的。为求单纯,
接下来皆以 CPython 3.7 为 runtime 来说明。
在我们下指令 `$ python your_script.py` 以执行脚本时,其实在背後会先透过一个
compiler 将脚本 compile 成 .pyc 档,之後再交由 interpreter 去执行。这部分
我描述地非常简略,有兴趣的人可以参考 Louie Lu 所写的
"Python 底层运作 01 – 虚拟机器与 Byte Code"
这篇文章。而我要谈到的部分为该文章所附图中的 virtual machine 这块,恕我偷懒
直接贴上该图
https://blog.louie.lu/wp-content/uploads/2017/04/python_flow.png
其中, code object 可以约略地视为一个个程式码区块 (e.g. module, function)
被编译後产生的物件,其带有执行时需要的资料;而 bytecode 则是让 interpreter
执行的步骤表。因此,要了解 CPython 的执行过程,我们可以从 bytecode 下手。
以前面提到的例子来说,我们可以使用 `dis` 这个 module 来分析:
https://i.imgur.com/VNWYXIx.png
上图中左侧两种颜色区块分别对应到右侧由 `dis.dis()` 印出的 bytecode 。而
bytecode table 中各个栏位代表的东西如下:
https://i.imgur.com/disnxl2.png
由於 CPython 的 interpreter 是一个 stack-based virtual machine,所以上面
看到的 opname 都是用来操作 stack 的指令。不过这边就先暂时不一一介绍各个
opname 所代表的意思,我们直接跟着左侧原始码来看:
1. 进入 `main()` 後,开头的第 4 行就是一个函数的定义。而对照到 bytecode
table ,可以看到有着一连串的指令等着要执行。其中 `4 LOAD_CONST` 和
`6 LOAD_CONST` 分别是载入一个 code object 和函数 `f` 的名称。
而接着的 `8 MAKE_FUNCTION` 正是一个用来建立 function object 的指令。
最後 `10 STORE_FAST` 则是将上一步骤所产生的 function object 以 `f` 为
名称储存在某处。
2. 接着执行脚本的第 8 行,就是很单纯的载入两个函数 `print`, `f` 和常数参数
`1`,然後再先後呼叫 `f` 和 `print` 两个函数。
3. 执行脚本的第 9 行,同上。不过因为这行是 `main()` 的最後一行,所以在最後
还会看到两行指令 `36 LOAD_CONST`, `38 RETURN_VALUE`,这也就是我们熟悉的
"没有明确透过 `return` 回传结果的话,预设会回传 `None`"的设计。
而根据上述第 1 点,我们可以知道一个 function object 的建立时间点就是执行到
`def func():` 的时候。但是 `MAKE_FUNCTION` 到底帮我们处理了什麽,我们得从
CPython 的原始码来了解。
在 Python 3.7 中, bytecode 里的各种 instruction 是由 ceval.c 这个档案里的
`_PyEval_EvalFrameDefault` 这个函数来处理的,其可以简单视为由 for + switch
所构成的 bytecode dispatcher 。
话题回到我们所感兴趣的 `MAKE_FUNCTION`,其中前三行为:
```c
// ceval.c::_PyEval_EvalFrameDefault
//
https://github.com/python/cpython/blob/cb75801/Python/ceval.c#L3201-L3207
PyObject *qualname = POP(); // 1
PyObject *codeobj = POP(); // 2
PyFunctionObject *func = (PyFunctionObject *)
PyFunction_NewWithQualName(codeobj, f->f_globals, qualname); // 3
```
1. 取出 stack 最上层的物件,其为稍後建立的 func 的 `__qualname__`
2. 取出 stack 最上层的物件,其为稍後建立的 func 的 `__code__`
3. 透过 `PyFunction_NewWithQualName` 建立 function object `func`
到这边为止,我们可以知道这就是单纯地在建立一个 function object,尚未处理其他
如 keyword argument, closure ... 等。所以对於一个简单的函数如:
```python
def foo(): return
```
藉着这三行就已经处理完了。
而同样在 `MAKE_FUNCTION` 这个区块里,後半部还有以下的处理:
```c
// ceval.c::_PyEval_EvalFrameDefault
//
https://github.com/python/cpython/blob/cb75801/Python/ceval.c#L3215-L3230
if (oparg & 0x08) {
assert(PyTuple_CheckExact(TOP()));
func ->func_closure = POP(); // 1
}
if (oparg & 0x04) {
assert(PyDict_CheckExact(TOP()));
func->func_annotations = POP(); // 2
}
if (oparg & 0x02) {
assert(PyDict_CheckExact(TOP()));
func->func_kwdefaults = POP(); // 3
}
if (oparg & 0x01) {
assert(PyTuple_CheckExact(TOP()));
func->func_defaults = POP(); // 4
}
```
1. 设定 `func_closure`,也就是在 Python 中的 `func.__closure__`
2. 设定 `func_annotations`,也就是在 Python 中的 `func.__annotations__`
3. 设定 `func_kwdefaults`,也就是在 Python 中的 `func.__kwdefaults__`
4. 设定 `func_defaults`,也就是在 Python 中的 `func.__defaults__`
好的,看到这边是否有想起我们在前面有做了一件调皮的事情?我们那时直接修改了
`func.__defaults__` 来重设 `L` 的预设值以模拟每次呼叫 `f(x)` 时使用的 `L`
都是一个 empty list 。但是这边明明只看到
```c
func->func_defaults = POP();
```
这样的一行,很明显地只是在赋值而已,跟文件上说的 "The default value is
evaluated only once" 应该不是同一件事情。
没错,因为 evaluation 在执行 `MAKE_FUNCTION` 前就已经发生了。我们回想一下
那一段 bytecode
https://i.imgur.com/7IoAHER.png
可以发现,其实 `L` 的预设值早在 `0 BUILD_LIST 0` 就已经处理掉了。
(`2 BUILD_TUPLE 1` 在做的事情则是将上一步建立出的 list 包成 tuple ,因为
`func.__defaults__` 接受的型别是 tuple)
我们将那个范例稍微改写一下,应该就可以更容易理解:
https://i.imgur.com/u49LufM.png
这里我们将 `L` 的预设值改为 `make_empty_list()`,在右侧 bytecode table 中
也可以看到 `make_empty_list()` 确实也只有在 `MAKE_FUNCTION` 前被执行一次,
因此这个版本我们预期的结果也如同 `def f(a, L=[]):` 一样,如下图
https://i.imgur.com/E5bpw3y.png
以上,这就是文件中 "The default value is evaluated only once" 的意思。
如果看到这边还觉得不过瘾,想要更深入理解每个 bytecode instruction 在做什麽
事情的话,可以试着玩玩看我最近在做的一个专案 `bytefall`。这个专案主要是延伸
自 nedbat/byterun 和 darius/tailbiter ,而我将它改写成支援 Python 3.4 ~
3.8 并加入一些特殊的功能,如下图所示的 opcode tracer
https://i.imgur.com/CB0ytfr.png
repo:
https://github.com/naleraphael/bytefall
repl.it (线上试玩):
https://repl.it/@naleraphael/pymutabledefaults
## 补充
在对 CPython virtual machine 有了稍微地了解後,我们来谈谈为何前面提到 "用
`locals()` 来观察一个 function 被执行时其 local scope 的内容并不完全适合"
这件事得先从 `locals()` 的实作讲起:
```c
// bltinmodule.c::builtin_locals_impl
//
https://github.com/python/cpython/blob/681044a/Python/bltinmodule.c#L1604-L1613
static PyObject *
builtin_locals_impl(PyObject *module)
{
// ... omitted
d = PyEval_GetLocals();
// ... omitted
}
```
这边可以看到,当我们呼叫 `locals()` 时,实际上会呼叫到 `PyEval_GetLocals()`
。而 `PyEval_GetLocals()` 的实作如下
```c
// ceval.c::PyEval_GetLocals
//
https://github.com/python/cpython/blob/3.7/Python/ceval.c#L4436-L4450
PyObject *
PyEval_GetLocals(void)
{
// ... omitted
return current_frame->f_locals;
}
```
它回传的其实只是当前 frame 的 `f_locals`。
你可以先把 frame 当作是一个带有 scope 中各种资讯的物件,也就是说,当你进入了
一个新的 scope ,也就意味着 interpreter 正在处理那个 frame 中的资讯。这是否
让你想起前面提到的 code object?没错,每个 frame 都带有一个正要被执行的 code
object (`f_code`)。而 `f_locals` 就是那个 scope 里的 local objects 。
但这还没解释我们的疑问。我们再回想一下前面提到的,执行 `print(f(1))` 时的
bytecode instruction 也就是下图中右侧黄色区块
https://i.imgur.com/eteVP6r.png
其中的 `14 LOAD_FAST 0 (f)` 看起来是要从目前这个 frame 取得 `f` 这个
函数,但是它背後是如何处理的呢?
```c
// ceval.c::_PyEval_EvalFrameDefault
//
https://github.com/python/cpython/blob/681044a/Python/ceval.c#L1074-L1085
{
PyObject *value = GETLOCAL(oparg);
// ... omitted
}
```
这个 macro `GETLOCAL` 的定义为
```c
// ceval.c::_PyEval_EvalFrameDefault
//
https://github.com/python/cpython/blob/681044a/Python/ceval.c#L792
#define GETLOCAL(i) (fastlocals[i])
// ceval.c::_PyEval_EvalFrameDefault
//
https://github.com/python/cpython/blob/681044a/Python/ceval.c#L880
fastlocals = f->f_localsplus;
```
这边我们发现实际上 `LOAD_FAST` 是从 `f->f_localsplus` 寻找资料,而非
`f->f_locals`。而 `f_localsplus` 的定义如下:
```c
// frameobject.h
//
https://github.com/python/cpython/blob/681044a/Include/frameobject.h#L46
PyObject *f_localsplus[1]; /* locals+stack, dynamically sized */
```
由此可知,`locals()` 回传的东西并不包含当时 interpreter stack 里的资料,
但是这又和进入一个 function scope 有什麽关系呢?以下我直接列出执行顺序
```
// starts from `CALL_FUNCTION`
//
https://github.com/python/cpython/blob/681044a0/Python/ceval.c#L3121-L3131
ceval.c::call_function =>
call.c::_PyFunction_FastCallKeywords =>
(1) call.c::function_code_fastcall =>
ceval.c::PyEval_EvalFrameEx => (tstate->interp->eval_frame =>)
_PyEval_EvalFrameDefault
(2) ceval.c::_PyEval_EvalCodeWithName =>
ceval.c::PyEval_EvalFrameEx =>
_PyEval_EvalFrameDefault
```
我们可以发现在执行 `CALL_FUNCTION` 之後,会再透过 interpreter 去处理一个
新的 frame 。这也呼应到前面讲的 "当你进入了一个新的 scope ,也就意味着
interpreter 正在处理那个 frame 中的资讯"。
## 回到原主题
关於 mutable default argument ,除了去探讨为何 Python 为何会这样设计,如:
https://stackoverflow.com/questions/1132941/
https://softwareengineering.stackexchange.com/questions/157373
也可以在深入了解这样的设计後,想想看可以透过它达成哪些功能,例如下面文章说的
caching, local rebinding
http://effbot.org/zone/default-values.htm#valid-uses-for-mutable-defaults
类似的技巧其实在官方文件也有提到:
https://docs.python.org/3/faq/programming.html#why-are-default-values-shared-between-objects
要评论这个特性的好坏,个人认为一切取决於你是如何使用它的。如果你是一个负责
管理一个团队的开发,又觉得这个特性很容易造成问题,你也可以透过 pre-commit
hook + pylint 来处理 (dangerous-default-value (W0102))。
当然,甚至可以投入 Python 社群为 source code 贡献:
https://mail.python.org/pipermail/python-ideas/2007-January/000121.html
最後再推荐一篇由 Anthony Shaw 所写的好文:
https://realpython.com/cpython-source-code-guide/
里面也提到了(在说明 symbol tables 那部分的下方)
> If you’ve ever wondered why Python’s default arguments are mutable,
> the reason is in this function. You can see they are a pointer to the
> variable in the symtable. No extra work is done to copy any values to
> an immutable type.
## Bonus
也许你没料到 Guido 对这个特性的想法
https://twitter.com/gvanrossum/status/1014524798850875393
--
※ 发信站: 批踢踢实业坊(ptt.cc), 来自: 220.136.19.245 (台湾)
※ 文章网址: https://webptt.com/cn.aspx?n=bbs/Python/M.1585988949.A.A54.html
1F:→ s860134: 这是 markdown 吧? 04/04 17:47
2F:推 pmove: 从底层的byte code讲,当然这是真正的因 04/04 17:57
3F:→ pmove: 就像讲微分题目,从Lim 讲起,这当然可以。只是我有点吓到 04/04 18:07
4F:推 jiyu520: 学习了 谢谢 04/04 18:10
5F:推 cuteSquirrel: 04/04 20:34
6F:推 shezion: 好专业的说明,感谢 04/09 00:38