2.34. 動態程式碼¶
有三個內建函式接受一段 Python 原始碼字串並執行它:eval()、exec() 與 compile()。它們合在一起,讓程式碼能在執行階段建構並執行更多程式碼 -- 這偶爾正是最合適的工具,但更常成為錯誤與安全漏洞的來源。
警告
這些函式會執行任意的 Python。將使用者輸入傳給 eval 或 exec -- 例如使用者可編輯之檔案中的字串、透過網路接收的酬載、在 REPL 提示字元下輸入的值 -- 等於讓那段輸入能做任何呼叫端指令碼所能做的事,甚至包括刪除裝置上的每一個檔案。請審慎使用它們,絕不要放在會自動執行的程式碼中,也絕不要對你無法掌控的資料使用。
2.34.1. eval¶
eval() 執行單一運算式並傳回其值:
>>> eval("3 * 7")
21
>>> name = "OpenMV"
>>> eval("name.lower()")
'openmv'
該運算式預設會看到呼叫端的全域與區域命名空間,這就是為什麼在第二個範例中 name 能夠解析。傳入明確的字典可以讓你對評估過程進行沙箱化:
eval("a + b", {"__builtins__": None}, {"a": 1, "b": 2})
即使已沙箱化,eval 依然危險。逃脫這類沙箱有眾所周知的技巧;對於不受信任的輸入,切勿單獨依賴清空 __builtins__ 的這個小技巧。
2.34.2. exec¶
exec() 執行一區塊程式碼,而非單一運算式 -- 包括陳述式、函式定義、迴圈 -- 並傳回 None:
exec("for i in range(3): print(i)")
輸出:
0
1
2
該區塊可以定義之後可供使用的名稱,不過在區域與全域範圍方面有一些注意事項。在函式內部使用 exec 很少會如撰寫者預期般運作;如果你需要用到它,請在模組層級執行。
2.34.3. compile¶
compile() 將一段原始碼字串轉換成一個程式碼物件,之後可以傳給 eval() 或 exec()。當同一段原始碼會執行多次時請使用它 -- 剖析只發生一次,執行起來更快:
expr = compile("x * x", "<expr>", "eval")
for x in range(5):
print(eval(expr))
輸出:
0
1
4
9
16
中間的引數是一個標籤,當該程式碼引發例外時會出現在追溯訊息(traceback)中。第三個引數對單一運算式為 "eval"、對一個區塊為 "exec",或對會印出其結果的互動式陳述式為 "single"。
2.34.4. 何時該動用這些工具¶
幾乎永遠不需要。初學者想像的大多數使用情境都有更安全的替代方案:
讀取設定檔。 使用
json-- 結構化資料,不會執行程式碼。評估使用者輸入的數值。 使用
int()/float()來剖析,然後進行算術運算。如果使用者真的需要輸入一個公式,請使用小型的運算式剖析器,而不是eval。
當你確實需要 eval / exec / compile 時,請隔離呼叫點、記錄即將被執行的確切字串,並將該原始碼視為你程式碼中最可疑的東西。