摘要:所有默認參數值則存儲在函數對象的屬性中,它的值為一個列表,列表中每一個元素均為一個默認參數的值。你可以定義一個對象作為占位符,如下面例子雖然應該避免默認參數值為可變對象,不過有時候使用可變對象作為默認值會收到不錯的效果。
原文地址
在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次調用,下面簡單分析一下:
第一次不提供默認參數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),這里也可以用裝飾器來實現計數和緩存功能。
參考
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://specialneedsforspecialkids.com/yun/37439.html
摘要:下面我們看看使用可變默認參數時會出現什么莫名其妙的狀況。這就意味著如果你使用可變默認參數并改變它,所有調用該函數的結果都是這個可變對象。同時也有注意,該函數要傳遞一個對象作為第二個參數,因為函數中對它進行了操作。 showImg(https://segmentfault.com/img/bVbrFS3?w=762&h=505); 絕大多數情況下,Python是一個干凈具有一致性的語言。...
摘要:編程規范筆記上寫在前面從語言開始,自己陸續學習了,但是自從研究生做畢設接觸以來,就愛不釋手,再也沒有動力嘗試其他語言。一與的一大優勢就是具備優秀的可讀性,而這基于一套較為完整的公認編程規范。如原本希望的結果是,結果卻完全一樣。 Python編程規范筆記(上) 寫在前面: 從C語言開始,自己陸續學習了C++/Java,但是自從研究生做畢設接觸Python以來,就愛不釋手,再也沒有動力嘗試...
摘要:一般使用或者調用外部腳本需要注意的是,這里的方向是相對于主程序的,所以就是子進程的輸出,而是子進程的輸入。基于同樣的原因,假如調用了方法等待子進程執行完畢而沒有及時處理輸出的話,就會造成死鎖。 最近有一項需求,要定時判斷任務執行條件是否滿足并觸發 Spark 任務,平時編寫 Spark 任務時都是封裝為一個 Jar 包,然后采用 Shell 腳本形式傳入所需參數執行,考慮到本次判斷條件...
閱讀 2837·2021-09-28 09:45
閱讀 1510·2021-09-26 10:13
閱讀 905·2021-09-04 16:45
閱讀 3666·2021-08-18 10:21
閱讀 1092·2019-08-29 15:07
閱讀 2637·2019-08-29 14:10
閱讀 3150·2019-08-29 13:02
閱讀 2465·2019-08-29 12:31