陷阱!python參數默認值

原文地址html

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

#! /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)

輸出結果是git

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

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

<!-- more -->緩存

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

其實想要獲得上面的輸出,只須要將obj = intlist()替換爲obj = intlist(l=[])函數

默認參數工做機制

上面怪異的輸出簡單來講是由於:this

Default values are computed once, then re-used.lua

所以每次調用__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

相關文章
相關標籤/搜索