先看一個例子
class HauntedBus:
"""備受幽靈乘客折磨的校車"""
def __init__(self, passengers=[]):
self.passengers = passengers
def pick(self, name):
self.passengers.append(name)
def drop(self, name):
self.passengers.remove(name)
測試
>>> bus1 = HauntedBus(['Alice', 'Bill'])
>>> bus1.passengers
['Alice', 'Bill']
>>> bus1.pick('Charlie')
>>> bus1.drop('Alice')
>>> bus1.passengers
['Bill', 'Charlie']
>>> bus2 = HauntedBus()
>>> bus2.pick('Carrie')
>>> bus2.passengers
['Carrie']
>>> bus3 = HauntedBus()
>>> bus3.passengers
['Carrie']
>>> bus3.pick('Dave')
>>> bus2.passengers
['Carrie', 'Dave']
>>> bus2.passengers is bus3.passengers
True
>>> bus1.passengers
['Bill', 'Charlie']
出現(xiàn)這個問題的根源是,默認值在定義函數(shù)時計算(通常在加載模塊時),因此默認值變成了函數(shù)對象的屬性。因此,如果默認值是可變對象,而且修改了它的值,那么后續(xù)的函數(shù)調(diào)用都會受到影響.
可變默認值導致的這個問題說明了為什么通常使用 None 作為接收可變值的參數(shù)的默認值.
對上面的類進行改進,規(guī)避上述的問題:
class TwilightBus:
"""讓乘客銷聲匿跡的校車"""
def __init__(self, passengers=None):
if passengers is None:
self.passengers = []
else:
self.passengers = passengers
def pick(self, name):
self.passengers.append(name)
def drop(self, name):
self.passengers.remove(name)
測試
>>> basketball_team = ['Sue', 'Tina', 'Maya', 'Diana', 'Pat']
>>> bus = TwilightBus(basketball_team)
>>> bus.drop('Tina')
>>> bus.drop('Pat')
>>> basketball_team
['Sue', 'Maya', 'Diana']
TwilightBus 違反了設(shè)計接口的最佳實踐,即“最少驚訝原則”。學生從校車中下車后,她的名字就從籃球隊的名單中消失了,這確實讓人驚訝。
這里的問題是,校車為傳給構(gòu)造方法的列表創(chuàng)建了別名。正確的做法是,校車自己維護乘客列表。修正的方法很簡單:在 init 中,傳入 passengers 參數(shù)時,應(yīng)該把參數(shù)值的副本賦值給self.passengers
修改初始化函數(shù)
def __init__(self, passengers=None):
if passengers is None:
self.passengers = []
else:
self.passengers = list(passengers)
在內(nèi)部像這樣處理乘客列表,就不會影響初始化校車時傳入的參數(shù)了。此外,這種處理方式還更靈活:現(xiàn)在,傳給 passengers 參數(shù)的值可以是元組或任何其他可迭代對象,例如set 對象,甚至數(shù)據(jù)庫查詢結(jié)果,因為 list 構(gòu)造方法接受任何可迭代對象。自己創(chuàng)建并管理列表可以確保支持所需的.remove() 和 .append() 操作,這樣 .pick() 和.drop() 方法才能正常運作
除非這個方法確實想修改通過參數(shù)傳入的對象,否則在類中直接把參數(shù)賦值給實例變量之前一定要三思,因為這樣會為參數(shù)對象創(chuàng)建別名。如果不確定,那就創(chuàng)建副本