1. 程式人生 > >Python中的預設引數詳解

Python中的預設引數詳解

文章的主題

不要使用可變物件作為函式的預設引數例如 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”語句執行的時候,預設引數才會進行計算。請看文件描述

https://docs.python.org/2/reference/compound_stmts.html#function-definitions
其中有下面一段

"Default parameter values are evaluated when the function definition is executed. This means that the expression is evaluated once, when the function is defined, and that the same “pre-computed” value is used for each call. This is especially important to understand when a default parameter is a mutable object, such as a list or a dictionary: if the function modifies the object (e.g. by appending an item to a list), the default value is in effect modified. This is generally not what was intended. A way around this is to use None as the default, and explicitly test for it in the body of the function,e.g.:


複製程式碼 程式碼如下:
def whats_on_the_telly(penguin=None):
    if penguin is None:
        penguin = []
    penguin.append("property of the zoo")
    return penguin
"

"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.