Python 板


LINE

这个**问题**其实被蛮多人讨论过了,但比起争论它是否是 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







like.gif 您可能会有兴趣的文章
icon.png[问题/行为] 猫晚上进房间会不会有憋尿问题
icon.pngRe: [闲聊] 选了错误的女孩成为魔法少女 XDDDDDDDDDD
icon.png[正妹] 瑞典 一张
icon.png[心得] EMS高领长版毛衣.墨小楼MC1002
icon.png[分享] 丹龙隔热纸GE55+33+22
icon.png[问题] 清洗洗衣机
icon.png[寻物] 窗台下的空间
icon.png[闲聊] 双极の女神1 木魔爵
icon.png[售车] 新竹 1997 march 1297cc 白色 四门
icon.png[讨论] 能从照片感受到摄影者心情吗
icon.png[狂贺] 贺贺贺贺 贺!岛村卯月!总选举NO.1
icon.png[难过] 羡慕白皮肤的女生
icon.png阅读文章
icon.png[黑特]
icon.png[问题] SBK S1安装於安全帽位置
icon.png[分享] 旧woo100绝版开箱!!
icon.pngRe: [无言] 关於小包卫生纸
icon.png[开箱] E5-2683V3 RX480Strix 快睿C1 简单测试
icon.png[心得] 苍の海贼龙 地狱 执行者16PT
icon.png[售车] 1999年Virage iO 1.8EXi
icon.png[心得] 挑战33 LV10 狮子座pt solo
icon.png[闲聊] 手把手教你不被桶之新手主购教学
icon.png[分享] Civic Type R 量产版官方照无预警流出
icon.png[售车] Golf 4 2.0 银色 自排
icon.png[出售] Graco提篮汽座(有底座)2000元诚可议
icon.png[问题] 请问补牙材质掉了还能再补吗?(台中半年内
icon.png[问题] 44th 单曲 生写竟然都给重复的啊啊!
icon.png[心得] 华南红卡/icash 核卡
icon.png[问题] 拔牙矫正这样正常吗
icon.png[赠送] 老莫高业 初业 102年版
icon.png[情报] 三大行动支付 本季掀战火
icon.png[宝宝] 博客来Amos水蜡笔5/1特价五折
icon.pngRe: [心得] 新鲜人一些面试分享
icon.png[心得] 苍の海贼龙 地狱 麒麟25PT
icon.pngRe: [闲聊] (君の名は。雷慎入) 君名二创漫画翻译
icon.pngRe: [闲聊] OGN中场影片:失踪人口局 (英文字幕)
icon.png[问题] 台湾大哥大4G讯号差
icon.png[出售] [全国]全新千寻侘草LED灯, 水草

请输入看板名称,例如:e-shopping站内搜寻

TOP