文章的主題
不要使用可變對象作為函數的默認參數例如 list,dict,因為def
是一個可執行語句,只有def
執行的時候才會計算默認默認參數的值,所以使用默認參數會造成函數執行的時候一直在使用同一個對象,引起bug。
基本原理
在 Python 源碼中,我們使用def
來定義函數或者方法。在其他語言中,類似的東西往往只是一一個語法聲明關鍵字,但def
卻是一個可執行的指令。Python代碼執行的時候先會使用 compile 將其編譯成 PyCodeObject.
PyCodeObject 本質上依然是一種靜態源代碼,只不過以字節碼方式存儲,因為它面向虛擬機。因此 Code 關注的是如何執行這些字節碼,比如棧空間大小,各種常量變量符號列表,以及字節碼與源碼行號的對應關系等等。
PyFunctionObject 是運行期產生的。它提供一個動態環境,讓 PyCodeObject 與運行環境關聯起來。同時為函數調用提供一系列的上下文屬性,諸如所在模塊、全局名字空間、參數默認值等等。這是def
語句執行的時候干的活。
PyFunctionObject 讓函數面向邏輯,而不僅僅是虛擬機。PyFunctionObject 和 PyCodeObject 組合起來才是一個完整的函數。
下文翻譯了一篇文章,有一些很好的例子。但是由于水平有限,有些不會翻譯或者有些翻譯有誤,敬請諒解。如果有任何問題請發郵件到 acmerfight圈gmail.com,感激不盡
主要參考資料 書籍:《深入Python編程》 大牛:shell 和 Topsky
Python對于函數中默認參數的處理往往會給新手造成困擾(但是通常只有一次)。
當你使用“可變”的對象作為函數中作為默認參數時會往往引起問題。因為在這種情況下參數可以在不創建新對象的情況下進行修改,例如 list dict。
>>> def function(data=[]):
... data.append(1)
... return data
...
>>> function()
[1]
>>> function()
[1, 1]
>>> function()
[1, 1, 1]
像你所看到的那樣,list變得越來越長。如果你仔細地查看這個list。你會發現list一直是同一個對象。
>>> id(function())
12516768
>>> id(function())
12516768
>>> id(function())
12516768
原因很簡單: 在每次函數調用的時候,函數一直再使用同一個list對象。這么使用引起的變化,非常“sticky”。
為什么會發生這種情況?
當且僅當默認參數所在的“def”語句執行的時候,默認參數才會進行計算。請看文檔描述
http://docs.python.org/ref/function.html
的相關部分。
"def"是Python中的可執行語句,默認參數在"def"的語句環境里被計算。如果你執行了"def"語句多次,每次它都將會創建一個新的函數對象。接下來我們將看到例子。
用什么來代替?
像其他人所提到的那樣,用一個占位符來替代可以修改的默認值。None
def myfunc(value=None):
if value is None:
value = []
# modify value here
如果你想要處理任意類型的對象,可以使用sentinel
sentinel = object()
def myfunc(value=sentinel):
if value is sentinel:
value = expression
# use/modify value here
在比較老的代碼中,written before “object” was introduced,你有時會看到
sentinel = ['placeholder']
譯者注:太水,真的不知道怎么翻譯了。我說下我的理解 有時邏輯上可能需要傳遞一個None,而你的默認值可能又不是None,而且還剛好是個列表,列表不
可以寫在默認值位置,所以你需要占位符,但是用None,你又不知道是不是調用者傳遞過來的那個
正確地使用可變參數
最后需要注意的是一些高深的Python代碼經常會利用這個機制的優勢;舉個例子,如果在一個循環里創建一些UI上的按鈕,你可能會嘗試這樣去做:
for i in range(10):
def callback():
print "clicked button", i
UI.Button("button %s" % i, callback)
但是你卻發現callback
打印出相同的數字(在這個情況下很可能是9)。原因是Python的嵌套作用域只是綁定變量,而不是綁定數值的,所以callback
只看到了變量i
綁定的最后一個數值。為了避免這種情況,使用顯示綁定。
for i in range(10):
def callback(i=i):
print "clicked button", i
UI.Button("button %s" % i, callback)
i=i
把callback的參數i
(一個局部變量)綁定到了當前外部的i
變量的數值上。(譯者注:如果不理解這個例子,請看http://stackoverflow.com/questions/233673/lexical-closures-in-python)
另外的兩個用途local caches/memoization
def calculate(a, b, c, memo={}):
try:
value = memo[a, b, c] # return already calculated value
except KeyError:
value = heavy_calculation(a, b, c)
memo[a, b, c] = value # update the memo dictionary
return value
(對一些遞歸算法非常好用)
對高度優化的代碼而言, 會使用局部變量綁全局的變量:
import math
def this_one_must_be_fast(x, sin=math.sin, cos=math.cos):
...
這是如何工作的?
當Python執行一條def
語句時, 它會使用已經準備好的東西(包括函數的代碼對象和函數的上下文屬性),創建了一個新的函數對象。同時,計算了函數的默認參數
值。
不同的組件像函數對象的屬性一樣可以使用。上文用到的'function'
>>> function.func_name
'function'
>>> function.func_code
<code object function at 00BEC770, file "<stdin>", line 1>
>>> function.func_defaults
([1, 1, 1],)
>>> function.func_globals
{'function': <function function at 0x00BF1C30>,
'__builtins__': <module '__builtin__' (built-in)>,
'__name__': '__main__', '__doc__': None}
這樣你可以訪問默認參數,你甚至可以修改它。
>>> function.func_defaults[0][:] = []
>>> function()
[1]
>>> function.func_defaults
([1],)
然而我不推薦你平時這么使用。
另一個重置默認參數的方法是重新執行相同的def
語句,Python將會和代碼對象創建一個新的函數對象,并計算默認參數,并且把新創建的函數對象賦值給了和上次相同的變量。但是再次強調,只有你清晰地知道在做什么的情況下你才能這么做。
And yes, if you happen to have the pieces but not the function, you can use the function class in the new module to create your own function object.