坑!當(dāng)Python默認參數(shù)是可變對象時

今天,看到一道python題,初始覺得很簡單,但是看到最終答案的我一臉懵逼,先把題列出來看看:

def func(n, li = []):
    for i in range(n):
        li.append(i)
    print(l)

func(2)
func(3,l=[1,2])
func(2)

看到這里的小伙伴可以先默算一下結(jié)果。
我想的答案是三行分別打?。篬0,1]、[1,2,0,1,2]、[0,1]
但是:

# 輸出:
[0, 1]
[1, 2, 0, 1, 2]
[0, 1, 0, 1]

???
???????
最后一個什么情況?為什么是在第一次調(diào)用函數(shù)后的li列表中添加的?

基于自己對內(nèi)存地址的理解,我知道當(dāng)前情況說明的是:第一次和第三次函數(shù)調(diào)用時,函數(shù)內(nèi)部的li列表都是同一個內(nèi)存地址,并未引用新的內(nèi)存空間;而第二次調(diào)用函數(shù)時,指定了列表參數(shù),此時li引用了新的內(nèi)存空間。
所以我在函數(shù)內(nèi)打印了列表參數(shù)li的內(nèi)存地址:

def func(n, li = []):
    print(id(li))
    for i in range(n):
        li.append(i)
    print(li)

func(2)
func(3,li=[1,2])
func(2)

輸出:

2017077218568
[0, 1]
2017076682696
[1, 2, 0, 1, 2]
2017077218568
[0, 1, 0, 1]

果然,第一次調(diào)用和第三次調(diào)用時其操作的是同一個內(nèi)存地址數(shù)據(jù),因此造成了這樣的追加結(jié)果。

最可疑的原因是默認參數(shù)引起的,不過已經(jīng)超過我的認知,為什么內(nèi)部有這種現(xiàn)象,自己真想不明白,求助google找到了答案。

原因解析

源地址答案:Python函數(shù)參數(shù)默認值的陷阱和原理深究:http://cenalulu.github.io/python/default-mutable-arguments/
這里首先需要理解python中變量的實質(zhì),了解其引用和內(nèi)存地址的含義。

Python’s default arguments are evaluated once when the function is defined, not each time the function is called (like it is in say, Ruby). This means that if you use a mutable default argument and mutate it, you will and have mutated that object for all future calls to the function as well. ——Python Common Gotchas

其大意是說,Python的默認參數(shù)只在函數(shù)定義時被賦值一次,而不會每次調(diào)用函數(shù)時又創(chuàng)建新的引用。
這意味著,函數(shù)定義完成后,默認參數(shù)已經(jīng)存在固定的內(nèi)存地址了,如果你使用一個可變的默認參數(shù)并對其進行改變,那么以后對該函數(shù)的調(diào)用都會改變這個可變對象。

原文作者解釋如下:

可見如果參數(shù)默認值是在函數(shù)編譯compile階段就已經(jīng)被確定。之后所有的函數(shù)調(diào)用時,如果參數(shù)不顯示的給予賦值,那么所謂的參數(shù)默認值不過是一個指向那個在compile階段就已經(jīng)存在的對象的指針。如果調(diào)用函數(shù)時,沒有顯示指定傳入?yún)?shù)值得話。那么所有這種情況下的該參數(shù)都會作為編譯時創(chuàng)建的那個對象的一種別名存在。如果參數(shù)的默認值是一個不可變(Imuttable)數(shù)值,那么在函數(shù)體內(nèi)如果修改了該參數(shù),那么參數(shù)就會重新指向另一個新的不可變值。而如果參數(shù)默認值是和本文最開始的舉例一樣,是一個可變對象(Muttable),那么情況就比較糟糕了。所有函數(shù)體內(nèi)對于該參數(shù)的修改,實際上都是對compile階段就已經(jīng)確定的那個對象的修改。

Python官方文檔中也有特別提示:Important warning: The default value is evaluated only once. This makes a difference when the default is a mutable object such as a list, dictionary, or instances of most classes. For example, the following function accumulates the arguments passed to it on subsequent calls:

如何避免這個缺陷

當(dāng)然最好的方式是不要使用可變對象作為函數(shù)默認值。如果非要這么用的話,下面是一種解決方案。還是以文章開頭的需求為例:

def func(n, li = []):
    # 這里使用 is None判斷不行
    if not li:
        li = []
    print(id(li))
    for i in range(n):
        li.append(i)
    print(li)

func(2)
func(3,li=[1,2])
func(2)

輸出結(jié)果,能按照正常邏輯結(jié)果輸出:

2017078756808
[0, 1]
2017079467976
[1, 2, 0, 1, 2]
2017078756808
[0, 1]

結(jié)語

這是設(shè)計python語言的時候就定義好的奇異之處,雖說有點違背編程邏輯,但記得避免就好,不用過多深究。

2019.07.07更新:遇到類似的問題一并總結(jié)在這篇了

函數(shù)可能會修改接收到的任何可變對象

def f(a,b):

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容