1. 程式人生 > >Python陷阱:為什麼不能用可變物件作為預設引數的值

Python陷阱:為什麼不能用可變物件作為預設引數的值

上次分享過一篇關於圖解Python變數與賦值的文章,今天接著這個話題繼續聊一聊關於賦值的一些坑。先來看一道題目:

>>> def func(numbers=[], num=1):
...     numbers.append(num)
...     return numbers

>>> func()
[1]
>>> func()
[1, 1]
>>> func()
[1, 1, 1]

我們似乎發現了一個Bug,每次用相同的方式呼叫函式 func() 時,返回結果竟然不一樣,而且每次返回的列表在不斷地變長。

>>> id(func())
4330472840
>>> id(func())
4330472840

從上面可以看出,函式的返回值其實是同一個列表物件,因為他們的id值是一樣的,只不過是列表中的元素在變化。為什麼會這樣呢?

這要從函式的特性說起,在 Python 中,函式是第一類物件(function is the first class object),換而言之,函式也是物件,跟整數、字串一樣可以賦值給變數、當做引數傳遞、還可以作為返回值。函式也有自己的屬性,比如函式的名字、函式的預設引數列表。

# 函式的名字
>>> func.__name__
'func' # 函式的預設引數列表 >>> func.__defaults__ ([1, 1, 1, 1, 1], 1)

def是一條可執行語句,Python 直譯器執行 def 語句時,就會在記憶體中就建立了一個函式物件(此時,函式裡面的程式碼邏輯並不會執行,因為還沒呼叫嘛),在全域性名稱空間,有一個函式名(變數叫 func)會指向該函式物件,記住,至始至終,不管該函式呼叫多少次,函式物件只有一個,就是function object,不會因為呼叫多次而出現多個函式物件。

function_default_args1.jpg

函式物件生成之後,它的屬性:名字和預設引數列表都將初始化完成。

function_default_args2.jpg

初始化完成時,屬性 __default__

中的第一個預設引數 numbers 指向一個空列表。

當函式第一次被呼叫時,就是第一次執行 func()時,開始執行函式裡面的邏輯程式碼(此時函式不再需要初始化了),程式碼邏輯就是往numbers中新增一個值為1的元素

function_default_args3.jpg

第二次呼叫 func(),繼續往numbers中新增一個元素

function_default_args4.jpg

第三次、四次依此類推。

所以現在你應該明白為什麼呼叫同一個函式,返回值確每次都不一樣了吧。因為他們共享的是同一個列表(numbers)物件,只是每呼叫一次就往該列表中增加了一個元素

如果我們顯示地指定 numbers 引數,結果截然不同。

>>> func(numbers=[10, 11])
[10, 11, 1]

function_default_args5.jpg

因為numbers被重新賦值了,它不再指向原來初始化時的那個列表了,而是指向了我們傳遞過去的那個新列表物件,因此返回值變成了 [10, 11, 1]

那麼我們應該如何避免前面那種情況發生呢?就是不要用可變物件作為引數的預設值。

正確方式:

>>> def func(numbers=None, num=1):
...     if numbers is None:
...         numbers = [num]
...     else:
...         numbers.append(num)
...     return numbers
...
>>> func()
[1]
>>> func()
[1]
>>> func()
[1]

如果呼叫時沒有指定引數,那麼呼叫方法時,預設引數 numbers 每次都被重新賦值了,所以,每次呼叫的時候numbers都將指向一個新的物件。這就是與前者的區別所在。

那麼,是不是說我們永遠都不應該用可變物件來作為引數的預設值了嗎?並不是,既然Python有這樣的語法,就一定有他的應用場景,就像 for ... else 語法一樣。我們可以用可變物件來做快取功能。

例如:計算一個數的階乘時可以用一個可變物件的字典當作快取值來實現快取,快取中儲存計算好的值,第二次呼叫的時候就無需重複計算,直接從快取中拿。

def factorial(num, cache={}):
    if num == 0:
        return 1
    if num not in cache:
        print('xxx')
        cache[num] = factorial(num - 1) * num
    return cache[num]


print(factorial(4))
print("-------")
print(factorial(4))

輸出:

---第一次呼叫---
xxx
xxx
xxx
xxx
24
---第二次呼叫---
24

第二次呼叫的時候,直接從 cache 中拿了值,所以,你說用可變物件作為預設值是 Python 的缺陷嗎?也並不是,對吧!你還是當作一種特性來使用。

參考文件:https://docs.python.org/3/reference/compound_stmts.html#function-definitions


關注公眾號「Python之禪」(id:vttalk)獲取最新文章 python之禪