Re: [問題] None在def中的變化
這個**問題**其實被蠻多人討論過了,但比起爭論它是否是 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://www.ptt.cc/bbs/Python/M.1585988949.A.A54.html
→
04/04 17:47,
4年前
, 1F
04/04 17:47, 1F
推
04/04 17:57,
4年前
, 2F
04/04 17:57, 2F
→
04/04 18:07,
4年前
, 3F
04/04 18:07, 3F
推
04/04 18:10,
4年前
, 4F
04/04 18:10, 4F
推
04/04 20:34,
4年前
, 5F
04/04 20:34, 5F
推
04/09 00:38,
4年前
, 6F
04/09 00:38, 6F
討論串 (同標題文章)
完整討論串 (本文為第 2 之 3 篇):
15
59
Python 近期熱門文章
PTT數位生活區 即時熱門文章