伯樂線上注:本文來自文章作者@acmerfight 的投稿(原文)。如果其他朋友也有不錯的原創或譯文,可以嘗試推薦給我們。
—————————–
文章的主題
不要使用可變物件作為函式的預設引數例如 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。
1 2 3 4 5 6 7 8 9 10 |
>>> def function(data=[]): ... data.append(1) ... return data ... >>> function() [1] >>> function() [1, 1] >>> function() [1, 1, 1] |
像你所看到的那樣,list變得越來越長。如果你仔細地檢視這個list。你會發現list一直是同一個物件。
1 2 3 4 5 6 7 |
<code>&gt;&gt;&gt; id(function()) 12516768 &gt;&gt;&gt; id(function()) 12516768 &gt;&gt;&gt; id(function()) 12516768 </code> |
原因很簡單: 在每次函式呼叫的時候,函式一直再使用同一個list物件。這麼使用引起的變化,非常“sticky”。
為什麼會發生這種情況?
當且僅當預設引數所在的“def”語句執行的時候,預設引數才會進行計算。請看文件描述
http://docs.python.org/ref/function.html
的相關部分。
“def”是Python中的可執行語句,預設引數在”def”的語句環境裡被計算。如果你執行了”def”語句多次,每次它都將會建立一個新的函式物件。接下來我們將看到例子。
用什麼來代替?
像其他人所提到的那樣,用一個佔位符來替代可以修改的預設值。None
1 2 3 4 |
def myfunc(value=None): if value is None: value = [] # modify value here |
如果你想要處理任意型別的物件,可以使用sentinel
1 2 3 4 5 6 |
sentinel = object() def myfunc(value=sentinel): if value is sentinel: value = expression # use/modify value here |
在比較老的程式碼中,written before “object” was introduced,你有時會看到
1 2 3 4 |
sentinel = ['placeholder'] 譯者注:太水,真的不知道怎麼翻譯了。我說下我的理解 有時邏輯上可能需要傳遞一個None,而你的預設值可能又不是None,而且還剛好是個列表,列表不 可以寫在預設值位置,所以你需要佔位符,但是用None,你又不知道是不是呼叫者傳遞過來的那個 |
正確地使用可變引數
最後需要注意的是一些高深的Python程式碼經常會利用這個機制的優勢;舉個例子,如果在一個迴圈裡建立一些UI上的按鈕,你可能會嘗試這樣去做:
1 2 3 4 |
for i in range(10): def callback(): print "clicked button", i UI.Button("button %s" % i, callback) |
但是你卻發現callback
列印出相同的數字(在這個情況下很可能是9)。原因是Python的巢狀作用域只是繫結變數,而不是繫結數值的,所以callback
只看到了變數i
繫結的最後一個數值。為了避免這種情況,使用顯示繫結。
1 2 3 4 |
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
1 2 3 4 5 6 7 |
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 |
(對一些遞迴演算法非常好用)
對高度優化的程式碼而言, 會使用區域性變數綁全域性的變數:
1 2 3 4 |
import math def this_one_must_be_fast(x, sin=math.sin, cos=math.cos): ... |
這是如何工作的?
當Python執行一條def
語句時, 它會使用已經準備好的東西(包括函式的程式碼物件和函式的上下文屬性),建立了一個新的函式物件。同時,計算了函式的預設引數值。
不同的元件像函式物件的屬性一樣可以使用。上文用到的’function’
1 2 3 4 5 6 7 8 9 10 |
>>> 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} |
這樣你可以訪問預設引數,你甚至可以修改它。
1 2 3 4 5 |
>>> 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.