1. 程式人生 > >Python函式的預設引數的設計【原創】

Python函式的預設引數的設計【原創】

 

在Python教程裡,針對預設引數,給了一個“重要警告”的例子:

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

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

預設值只會執行一次,也沒說原因。會打印出結果:

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

因為學的第一門語言是Ruby,所以感覺有些奇怪。 但肯定的是方法f一定儲存了變數L。

 

準備知識:指標

p指向不可變物件,比如數字。則相當於p指標指向了不同的記憶體地址。

p指向的是可變物件,比如list。list自身的改變,並不會改變list物件自身所在的記憶體地址。所以p指向的記憶體地址不變。

>>> 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函式的引數預設值,是在編譯階段就綁定了。(寫程式碼時就定義了。)

 

下面是一段從Python Common Gotchas中摘錄的原因解釋:

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.

由此可知:

  1. 在執行程式碼時,執行到函式定義時,預設引數的表示式就被執行了。 
  2. 函式呼叫時,不會再次執行預設引數的表示式。⚠️ 這點和Ruby完全不同。
  3. 由此可知,如果預設引數,指向一個不變物件,例如L = 1。那麼在函式呼叫時,在函式體內對L重新賦值,L其實是一個新的指標, 指向的是一個新的記憶體地址。而原來預設引數L本身及指向的記憶體地址,已經儲存在最開始編譯時的函式定義中。可以用__default__檢視。
  4. 如果預設引數指向的是一個可變物件,如list, 那麼L.append(a)是對可變物件自身的修改,L指向的記憶體地址不變。所以每次呼叫函式,預設引數取出的都是這個記憶體地址的物件。

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

 

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 ,並加入了自己的理解。歡迎轉載!

&n