Re: [問題] None在def中的變化

看板Python作者 (o.O)時間4年前 (2020/04/04 16:29), 編輯推噓4(402)
留言6則, 5人參與, 4年前最新討論串2/3 (看更多)
這個**問題**其實被蠻多人討論過了,但比起爭論它是否是 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
這是 markdown 吧?
04/04 17:47, 1F

04/04 17:57, 4年前 , 2F
從底層的byte code講,當然這是真正的因
04/04 17:57, 2F

04/04 18:07, 4年前 , 3F
就像講微分題目,從Lim 講起,這當然可以。只是我有點嚇到
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
文章代碼(AID): #1UY4LLfK (Python)
文章代碼(AID): #1UY4LLfK (Python)