可視化數(shù)據(jù)結(jié)構(gòu)以及算法演示工具
今天筆者將向大家分享Python函數(shù)中應(yīng)該避免的一個(gè)小細(xì)節(jié)。
為了實(shí)現(xiàn)代碼的重用,避免重復(fù)造輪子,通常我們會(huì)將一段常用的代碼邏輯封裝為函數(shù),這樣就可以實(shí)現(xiàn)代碼的一處編寫,多處調(diào)用。
在函數(shù)設(shè)計(jì)和編寫中,常常會(huì)用到默認(rèn)值參數(shù),即在函數(shù)定義的時(shí)候就給定默認(rèn)值。調(diào)用函數(shù)的時(shí)候,如果不給默認(rèn)值參數(shù)傳遞值,則它將使用函數(shù)定義時(shí)設(shè)定的默認(rèn)值。
本文要提醒大家的是,參數(shù)的默認(rèn)值最好不要為可變類型,比如,x=[]。雖然這在Python的語法上是合法的,但合法的東西并不一定是好東西。比如,生活中你無故對(duì)陌生人作出無禮的行為,雖然不違法,但可能帶來意想不到的后果。
def my_func(lst: List[str] = []):
# do something
在程序的世界中也是類似的道理。在Python函數(shù)中可變默認(rèn)參數(shù)是合法的——我們可以運(yùn)行上面這樣的代碼,并且Python也是允許的。然而,這并不是一個(gè)好的實(shí)踐,也并不推薦這樣做。
1. 可變性和不可變性的含義
可變性(Mutability):
如果一個(gè)數(shù)據(jù)結(jié)構(gòu)在創(chuàng)建后可以修改,那么它就是可變的。在Python中,像列表(List)、字典(Dict)和集合(Set)這樣的數(shù)據(jù)類型都是可變數(shù)據(jù)結(jié)構(gòu)。
不可變性(Immutability):
與可變性相反,如果一個(gè)數(shù)據(jù)結(jié)構(gòu)在創(chuàng)建之后不可更改,那么它就是不可變的。在Python中,像整數(shù)、浮點(diǎn)數(shù)、布爾型、字符串、None、元組(tuple)和凍結(jié)集合(fozensets)這樣的數(shù)據(jù)類型都是不可變的。
2. 為什么要使用默認(rèn)參數(shù)?
def say_hello(obj: str, greeting: str='Hello') -> None:
print(f'{greeting}, {obj}!')
if __name__ == '__main__':
say_hello(obj='World') # Hello, World!
say_hello(obj='World', greeting='Honey') # Honey, World!
在上面的函數(shù)中,greeting 就是一個(gè)默認(rèn)參數(shù)。在調(diào)用函數(shù)時(shí),如果我們不給 greeting 傳值,那么它將采用默認(rèn)值 Hello。如果我們給它傳遞了值,那它就會(huì)接受我們傳遞的值。
因此,如果我們可以接受默認(rèn)參數(shù)的默認(rèn)值,在函數(shù)調(diào)用時(shí)就可以選擇不傳遞默認(rèn)參數(shù),例如將上面示例中的 greeting 參數(shù)值設(shè)為 Hello。當(dāng)函數(shù)中某個(gè)參數(shù)的值多數(shù)情況下不變時(shí),就可以采用默認(rèn)參數(shù),比如一個(gè)讀取文件的函數(shù),如果文件路徑一般不會(huì)改變,那就可以將其設(shè)置為默認(rèn)參數(shù)。
3. 為什么不推薦使用可變默認(rèn)參數(shù)呢?
def my_func(fruits: List[str] = []):
fruits.append('apple')
return fruits
這里,我們有一個(gè)接受 fruits 作為參數(shù)的函數(shù) my_func,該函數(shù)會(huì)將 apple 追加到列表中,然后返回列表。
- fruits 是一個(gè)默認(rèn)參數(shù)。
- 如果我們給 fruits 傳遞東西,它就會(huì)接受該值。
- 如果我們不 fruits 傳遞東西,它就會(huì)使用默認(rèn)值 []。
a = my_func(['banana'])
print(a) # ['banana', 'apple']
這里,我們給 fruits 傳遞了內(nèi)容,調(diào)用函數(shù)時(shí),它將取值 banana,然后返回 ['banana', 'apple']。
a = my_func()
print(a) # ['apple']
如果我們不向 fruits 傳遞任何內(nèi)容,那么 fruits 將使用默認(rèn)值 [],函數(shù)將返回 ['apple']。
4. 那么問題來了
print(my_func()) # ['apple']
print(my_func()) # ['apple', 'apple']
print(my_func()) # ['apple', 'apple', 'apple']
如果我們?cè)诓唤o fruits 傳遞任何內(nèi)容的前提下,多次調(diào)用函數(shù),就會(huì)發(fā)生很奇怪的事。
- 第一次調(diào)用 my_func(),fruits 被賦值給 [],而函數(shù)體中的 fruits.append('apple') 則會(huì)使它變成 ['apple']。
- 第二次調(diào)用 my_func(),此時(shí) fruits 的值為 ['apple']。我們?cè)俅螆?zhí)行 fruits.append('apple'),fruits 的值就變成了 ['apple', 'apple']。
- 第三次調(diào)用 my_func(),此時(shí) fruits 的值為 ['apple', 'apple']。再次執(zhí)行 fruits.append('apple') 后,fruits 的值就變成了 ['apple', 'apple', 'apple']。
5. 為什么會(huì)發(fā)生這種情況呢?
from typing import List
def my_func(fruits: List[str] = []) -> List[str]:
fruits.append('apple')
return fruits
if __name__ == '__main__':
print(my_func()) # ['apple']
print(my_func()) # ['apple', 'apple']
print(my_func()) # ['apple', 'apple', 'apple']
原因是:當(dāng)我們定義函數(shù) my_func() 時(shí),Python解釋器只會(huì)讀取 fruits: List[str] = [] 一次。
如果我們執(zhí)行 fruits.append('apple') 或其他行為,對(duì) fruits 的這一改變將會(huì)保留在函數(shù)中,因?yàn)?nbsp;fruits 不會(huì)再被賦值給 []。
6. 那么應(yīng)該如何避免這種情況呢?
只需要將 fruits: List[str] = [] 的默認(rèn)值修改為 None(不可變數(shù)據(jù)類型),并且在函數(shù)體中對(duì) fruits 做一個(gè)是否為 None 的判斷即可。
from typing import List
def my_func(fruits: List[str] = None) -> List[str]:
if fruits is None:
fruits = []
fruits.append('apple')
return fruits
if __name__ == '__main__':
print(my_func()) # ['apple']
print(my_func()) # ['apple']
print(my_func()) # ['apple']
- 在函數(shù)定義中,我們將默認(rèn)參數(shù) fruits 的默認(rèn)值設(shè)為不可變值 None。
- 然后,我們判斷 fruits 是否為 None, 即 if fruits is None:,如果是則將其賦值給 []。
通過這種方式,就不會(huì)像使用可變默認(rèn)參數(shù)那樣產(chǎn)生奇怪的副作用(不期望的結(jié)果)。雖然這樣使得代碼變得更長,但為了確保邏輯的正確性,這是必須要做的事。
7. 結(jié)論
今天的分享到此結(jié)束,感謝你的閱讀,希望這些淺顯易懂的內(nèi)容對(duì)你有所幫助!