Python函數的默認參數的設計【原創】

 

在Python教程裏,針對默認參數,給了一個「重要警告」的例子:javascript

def f(a, L=[]):
    L.append(a)
    return L

print(f(1))
print(f(2))
print(f(3))

默認值只會執行一次,也沒說緣由。會打印出結果:java

[1]
[1, 2]
[1, 2, 3]

由於學的第一門語言是Ruby,因此感受有些奇怪。 但確定的是方法f必定儲存了變量L。python

 

準備知識:指針

p指向不可變對象,好比數字。則至關於p指針指向了不一樣的內存地址。git

p指向的是可變對象,好比list。list自身的改變,並不會改變list對象自身所在的內存地址。因此p指向的內存地址不變。github

>>> p = 1
>>> id(p)
4523801232
>>> p = p + 1
>>> id(p)
4523801264
>>> p = 11
>>> id(p)
4523801552

>>> p = []
>>> id(p)
4527080640
>>> p.append(11)
>>> id(p)
4527080640

 

 

根本緣由

Python函數的參數默認值,是在編譯階段就綁定了。(寫代碼時就定義了。)app

 

下面是一段從Python Common Gotchas中摘錄的緣由解釋:ide

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.函數

由此可知:ui

  1. 在運行代碼時,運行到函數定義時,默認參數的表達式就被執行了。 
  2. 函數調用時,不會再次運行默認參數的表達式。⚠️ 這點和Ruby徹底不一樣。
  3. 由此可知,若是默認參數,指向一個不變對象,例如L = 1。那麼在函數調用時,在函數體內對L從新賦值,L實際上是一個新的指針, 指向的是一個新的內存地址。而原來默認參數L自己及指向的內存地址,已經儲存在最開始編譯時的函數定義中。能夠用__default__查看。
  4. 若是默認參數指向的是一個可變對象,如list, 那麼L.append(a)是對可變對象自身的修改,L指向的內存地址不變。因此每次調用函數,默認參數取出的都是這個內存地址的對象。

第三條,修改上面的例子:lua

 

def f(a, L = 1):
    L = a
    print(id(L))
    return L

print("self",id(f.__defaults__[0]))
print(f(1))
print("self",id(f.__defaults__[0]))
print(f(33))
print("self",id(f.__defaults__[0]))

#運行結果:
self 4353170064
4353170064
1
self 4353170064
4353171088
33
self 4353170064

默認參數L,在編譯階段就綁定了,儲存在__default__內。函數體內的L = a表達式,生成的是新的變量。返回的L是新的變量,和默認參數無關。

 

第四條,仍是上面的例子, 改一下默認參數的類型爲可變對象list:

def f(a, L = []):
    L.append(a) print(id(L))
    return L
# L = f(1)
print("self",id(f.__defaults__[0]))
print(f(1))
print("self",id(f.__defaults__[0]))
print(f(33))
print("self",id(f.__defaults__[0]))
#返回結果
self 4337586048
4337586048
[1]
self 4337586048
4337586048
[1, 33]
self 4337586048

由id號可知,返回的是默認參數自身。

 

如何避免這個陷阱帶來沒必要要麻煩

def f(a, L = None):
    if L is None:
        L = []
    L.append(a)
    return L

 

爲何Python要這麼設計

 StackOverflow 上爭論不少。

 

Ruby之因此沒有這個問題,我想是由於Ruby的def關鍵字定義了一個封閉做用域,任何數據都必須經過參數傳入到方法內,才能用。

和Ruby比,Python參數的做用被大大消弱了。Python的出現晚於Ruby,其創始人確定參考了Ruby的設計。拋棄了這個設計,選擇了相似javascript的函數方式。def定義的函數,執行時是能夠接收外部做用域的變量的。

有觀點認爲:

出於Python編譯器的實現方式考慮,函數是一個內部一級對象。而參數默認值是這個對象的屬性。在其餘任何語言中,對象屬性都是在對象建立時作綁定的。所以,函數參數默認值在編譯時綁定也就不足爲奇了。

 

本文參考了:http://cenalulu.github.io/python/default-mutable-arguments/#toc1 ,並加入了本身的理解。歡迎轉載!

相關文章
相關標籤/搜索