陷阱!python參數默認值

925503490 11年前發布 | 13K 次閱讀 Python Python開發

在stackoverflow上看到這樣一個程序:

#! /usr/bin/env python
# -*- coding: utf-8 -*-

class demo_list:
    def __init__(self, l=[]):
        self.l = l

    def add(self, ele):
        self.l.append(ele)

def appender(ele):
    obj = demo_list()
    obj.add(ele)
    print obj.l

if __name__ == "__main__":
    for i in range(5):
        appender(i)

輸出結果是

[0]
[0, 1]
[0, 1, 2]
[0, 1, 2, 3]
[0, 1, 2, 3, 4] 

有點奇怪,難道輸出不應該是像下面這樣嗎?

[0]
[1]
[2]
[3]
[4] 

其實想要得到上面的輸出,只需要將obj = intlist()替換為obj = intlist(l=[])

默認參數工作機制

上面怪異的輸出簡單來說是因為:

Default values are computed once, then re-used.

因此每次調用__init__(),返回的是同一個list。為了驗證這一點,下面在__init__函數中添加一條語句,如下:

def __init__(self, l=[]):
    print id(l),
    self.l = l

輸出結果為:

4346933688 [0]
4346933688 [0, 1]
4346933688 [0, 1, 2]
4346933688 [0, 1, 2, 3]
4346933688 [0, 1, 2, 3, 4] 

可以清晰看出每次調用__init__函數時,默認參數l都是同一個對象,其id為4346933688。

關于默認參數,文檔中是這樣說的:

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.

為了能夠更好地理解文檔內容,再來看一個例子:

def a():
    print "a executed"
    return []

def b(x=a()):
    print "id(x): ", id(x)
    x.append(5)
    print "x: ", x

for i in range(2):
    print "-" * 15, "Call b()", "-" * 15
    b()
    print b.__defaults__
    print "id(b.__defaults__[0]): ", id(b.__defaults__[0])

for i in range(2):
    print "-" * 15, "Call b(list())", "-" * 15
    b(list())
    print b.__defaults__
    print "id(b.__defaults__[0]): ", id(b.__defaults__[0])

注意,當python執行def語句時,它會根據編譯好的函數體字節碼和命名空間等信息新建一個函數對象,并且會計算默認參數的值。函數的所有構成要素均可通過它的屬性來訪問,比如可以用func_name屬性來查看函數的名稱。所有默認參數值則存儲在函數對象的__defaults__屬性中,它的值為一個列表,列表中每一個元素均為一個默認參數的值。

好了,你應該已經知道上面程序的輸出內容了吧,一個可能的輸出如下(id值可能為不同):

a executed
————— Call b() —————
id(x): 4316528512
x: [5]
([5],)
id(b.__defaults__[0]): 4316528512
————— Call b() —————
id(x): 4316528512
x: [5, 5]
([5, 5],)
id(b.__defaults__[0]): 4316528512
————— Call b(list()) —————
id(x): 4316684872
x: [5]
([5, 5],)
id(b.__defaults__[0]): 4316528512
————— Call b(list()) —————
id(x): 4316684944
x: [5]
([5, 5],)
id(b.__defaults__[0]): 4316528512 

我們看到,在定義函數b(也就是執行def語句)時,已經計算出默認參數x的值,也就是執行了a函數,因此才會打印出a executed。之后,對b進行了4次調用,下面簡單分析一下:

  1. 第一次不提供默認參數x的值進行調用,此時使用函數b定義時計算出來的值作為x的值。所以id(x)和id(b.defaults[0])相等,x追加數字后,函數屬性中的默認參數值也變為[5];
  2. 第二次仍然沒有提供參數值,x的值為經過第一次調用后的默認參數值[5],然后對x進行追加,同時也對函數屬性中的默認參數值追加;
  3. 傳遞參數list()來調用b,此時新建一個列表作為x的值,所以id(x)不同于函數屬性中默認參數的id值,追加5后x的值為[5];
  4. 再一次傳遞參數list()來調用b,仍然是新建列表作為x的值。

如果上面的內容你已經搞明白了,那么你可能會覺得默認參數值的這種設計是python的設計缺陷,畢竟這也太不符合我們對默認參數的認知了。然而事實可能并非如此,更可能是因為:

Functions in Python are first-class objects, and not only a piece of code.

我們可以這樣解讀:函數也是對象,因此定義的時候就被執行,默認參數是函數的屬性,它的值可能會隨著函數被調用而改變。其他對象不都是如此嗎?

可變對象作為參數默認值?

參數的默認值為可變對象時,多次調用將返回同一個可變對象,更改對象值可能會造成意外結果。參數的默認值為不可變對象時,雖然多次調用返回同一個對象,但更改對象值并不會造成意外結果。

因此,在代碼中我們應該避免將參數的默認值設為可變對象,上面例子中的初始化函數可以更改如下:

def __init__(self, l=None):
       if not l:
            self.l = []
       else:
            self.l = l

在這里將None用作占位符來控制參數l的默認值。不過,有時候參數值可能是任意對象(包括None),這時候就不能將None作為占位符。你可以定義一個object對象作為占位符,如下面例子:

sentinel = object()

def func(var=sentinel):
   if var is sentinel:
        pass
   else:
        print var

雖然應該避免默認參數值為可變對象,不過有時候使用可變對象作為默認值會收到不錯的效果。比如我們可以用可變對象作為參數默認值來統計函數調用次數,下面例子中使用collections.Counter()作為參數的默認值來統計斐波那契數列中每一個值計算的次數。

def fib_direct(n, count=collections.Counter()):
    assert n > 0, 'invalid n'
    count[n] += 1
    if n < 3:
        return n
    else:
        return fib_direct(n - 1) + fib_direct(n - 2)

print fib_direct(10)
print fib_direct.__defaults__[0]

運行結果如下:

89
Counter({2: 34, 1: 21, 3: 21, 4: 13, 5: 8, 6: 5, 7: 3, 8: 2, 9: 1, 10: 1})

我們還可以用默認參數來做簡單的緩存,仍然以斐波那契數列作為例子,如下:

def fib_direct(n, count=collections.Counter(), cache={}):
    assert n > 0, 'invalid n'
    count[n] += 1
    if n in cache:
        return cache[n]
    if n < 3:
        value = n
    else:
        value = fib_direct(n - 1) + fib_direct(n - 2)
    cache[n] = value
    return value

print fib_direct(10)
print fib_direct.__defaults__[0]

結果為:

89
Counter({2: 2, 3: 2, 4: 2, 5: 2, 6: 2, 7: 2, 8: 2, 1: 1, 9: 1, 10: 1})

這樣就快了太多了,fib_direct(n)調用次數為o(n),這里也可以用裝飾器來實現計數和緩存功能。

參考
Python instances and attributes: is this a bug or i got it totally wrong?
Default Parameter Values in Python
“Least Astonishment” in Python: The Mutable Default Argument
A few things to remember while coding in Python
Using Python’s mutable default arguments for fun and profit

 

來自:http://selfboot.cn/2014/10/27/python_default_values/

 

 本文由用戶 925503490 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!