7.1 使用可變物件作為默認參數值的錯誤
你已經學了很多東西,讓我們來看看新手最常見的錯誤之一——最好從別人的錯誤中學到教訓,對吧?
Python允許你給函數指定可選參數,透過為它們設置默認值來實現。這當然是Python語言的一個非常方便的特性,但如果默認值的類型是可變的,可能會導致不愉快的後果。例如,看看下面的函數定義:
def foo(bar=[]): # bar - 是可選參數
# 默認為空列表。
bar.append("baz") # 這行代碼可能會出問題...
return bar
這種情況下的一個常見錯誤是認為每次在沒有提供這個參數值的情況下調用函數時,可選參數將被設置為默認值。
在上面的代碼中,例如可以假設每次調用函數 foo()
(也就是不指定參數 bar
的情況下),它總是會返回 ["baz"]
,因為預設是每次調用 foo()
時(不指定參數 bar
),會將 bar
設置為 []
(即空列表)。
但我們來看看實際會發生什麼:
result = foo()
print(result) # ["baz"]
result = foo()
print(result) # ["baz", "baz"]
result = foo()
print(result) # ["baz", "baz", "baz"]
為什麼函數每次調用 foo()
時都會繼續將值 baz
添加到現有列表中,而不是每次創建一個新列表?
答案在於對Python「底層運作機制」的更深入理解。具體來說: 函數的默認值僅在函數定義時初始化一次。
因此,參數 bar
僅在首次定義 foo()
時被初始化為默認值(即空列表),但後續的調用(即不提供參數 bar
的情況下)將繼續使用首次定義時創建的相同列表。
為了解決這個錯誤,常見的「解決方法」如下:
def foo(bar=None):
if bar is None:
bar = []
bar.append("baz")
return bar
result = foo()
print(result) # ["baz"]
result = foo()
print(result) # ["baz"]
result = foo()
print(result) # ["baz"]
7.2 錯誤使用類變數
讓我們看一個例子:
class A(object):
x = 1
class B(A):
pass
class C(A):
pass
print(A.x, B.x, C.x)
# 1 1 1
看起來一切正常。
現在讓我們給類 B
的字段 x
賦值:
B.x = 2
print(A.x, B.x, C.x)
# 1 2 1
一切如預期。
那麼現在更改類 A
的變數:
A.x = 3
print(A.x, B.x, C.x)
# 3 2 3
奇怪,我們只改變了 A.x
。為什麼 C.x
也改變了?
在Python中,類變數被處理為字典,並遵循通常所說的方法解析順序(MRO)。因此,在上面的代碼中,因為
屬性 x
沒有在 類 C
中找到,所以會在它的基類中尋找(在上面的例子中只包括 A
,儘管Python支持多重繼承)。
換句話說,C
沒有自己的屬性 x
,獨立於 A
。因此,對 C.x
的引用實際上是對 A.x
的引用。如果沒有正確處理,這可能會引起問題。在學習Python時,特別注意類屬性及其操作。
7.3 錯誤地指定異常塊的參數
假設你有以下代碼塊:
try:
l = ["a", "b"]
int(l[2])
except ValueError, IndexError: # 想捕捉這兩個異常,對吧?
pass
Traceback (most recent call last):
File "<stdin>", line 3, in <module>
IndexError: list index out of range
這裡的問題在於 except
表達式不接受這樣指定的異常列表。因此,在上面的代碼中,IndexError
異常不會被 except
表達式捕捉到;相反,異常最終會綁定到名為 IndexError
的參數。
重要!
你可能會在互聯網上的例子中看到此類代碼,因為它在 Python 2.x 中使用。
使用 except
表達式捕捉多個異常的正確方法是指定第一個參數為包含要捕捉異常的元組。此外,為了更好的可讀性,你可以使用關鍵字 as
,例如這樣:
try:
l = ["a", "b"]
int(l[2])
except (ValueError, IndexError) as e:
pass
GO TO FULL VERSION