在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]
有點奇怪,難道輸出不應該是像下面這樣嗎?
<!-- more -->
[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次調用,下面簡單分析一下:
-
第一次不提供默認參數x的值進行調用,此時使用函數b定義時計算出來的值作爲x的值。所以id(x)和id(b.__defaults__[0])相等,x追加數字後,函數屬性中的默認參數值也變爲[5];
-
第二次仍然沒有提供參數值,x的值爲經過第一次調用後的默認參數值[5],然後對x進行追加,同時也對函數屬性中的默認參數值追加;
-
傳遞參數list()來調用b,此時新建一個列表作爲x的值,所以id(x)不同於函數屬性中默認參數的id值,追加5後x的值爲[5];
-
再一次傳遞參數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),這裏也可以用裝飾器來實現計數和緩存功能。