本文為《爬著學Python》系列第十篇文章。
在實際操作中,可能函數是我們幾乎唯一的實現操作的方式,這是因為函數能夠構造一個高度集中的變量環境,在合理的設計下,它能使程序思路更加清晰的同時更利于調整與修改。幾乎沒有哪個程序設計語言會不涉及自定義函數的。
在上一篇文章中我們留了許多內容說要在本文中介紹,它們是一些和函數參數相關的問題。函數是我們的對操作方式的一種整合,因此我們會通過函數來進行運算或者完成某些功能,這些功能涉及到變量時,我們必須清楚到底發生了哪些事情。廢話少說吧。
創建自定義函數
Python的自定義函數格式中規中矩
def func_name(arg1):
pass
用def
引導自定義函數名,用括號給出該函數的參數,在冒號后換行通過縮進確定函數體。在格式上和條件判斷語句有些相似。
當然,我們從簡單的開始講起,這是Python自定義函數的簡單形式。一般能“動手腳”的地方只有三個,一個是def
前面可以用裝飾器(詳見我的另一篇文章Python精進-裝飾器與函數對象),一個是函數參數,一個是執行語句。
關于執行語句部分,主要是函數的嵌套以及控制結構的組合,這種內容作為知識講解沒什么意思。大多數人都知道可以這么做,但很多人做不好,是不是因為沒學好呢?我覺得不是的,是練少了,積累項目經驗以后就會逐漸強化這方面的能力。而裝飾器之前專門提前講過,因此本文的重點會放在函數參數上。之后也會在深入了解Python自定義函數參數設計的基礎上去認識如何正確設置函數返回值。
自定義函數的參數
首先我要聲明一點,我決定不講一般意義上的形參(形式參數)和實參(實際參數)的知識。按道理來說,即使Python不嚴格要求定義函數參數,但這方面的知識有助于理解自定義函數中參數操作的情況,還是應該說明一下的。但是我仔細想了一下在Python編程中不知道這兩個概念真的完全沒有任何關系,我們可以簡單地理解為在定義函數時括號中聲明的參數是我們在函數使用中會用到的參數,在調用函數時括號中的變量就是參加函數運算用到的變量。是的,換個名字,參數(用于定義)和變量(用于調用)就足以理解了。
可能完全沒有基礎的同學看上面一段話還是有些不明白,這很正常,我們還沒有講過函數的調用。沒關系再接下來的例子中我們會見到。不過這一節我們重點是看看函數定義時參數有哪些形式。
最普通的參數
最普通的自定義函數參數就是在括號中列出一系列我們要用的參數。
def print_times(_string, _time):
for i in range(_time):
print(_string)
print_times('Hello!', 3)
在這個例子中我們定義函數時定義了兩個變量,分別是_string
和_time
。這個函數的作用不用我過多說明,函數體只有一個循環語句,目的是重復輸出一個字符串。首先要注意的是為什么我要在"string"和“time”前加下劃線呢,這是為了防止和其他變量出現沖突。如“string”有可能和內置關鍵字沖突(其實不會,Python字符串內置關鍵字是str,這里是為了保險起見),“date”有可能在使用了標準庫datetime
與其中的方法沖突。為了減少歧義,在定義函數的時候給變量前面加上下劃線是比較穩妥的辦法。這個技巧是面向對象編程時類設計常用的手段,在自定義函數中一樣可以用。在后面的例子中,有時我會用比較長的不太可能沖突的變量就可以不這么做了。
接下來就是函數的作用的問題,我們需要重復輸出一個字符串,所以理所當然的情況下我們只需要簡單地涉及兩個操作對象,一個是要輸出的字符串,一個是這個字符串輸出的次數。這是比較好理解的。所以我們調用函數的時候,我們給出的字符串是Hello
,次數是3,這個函數就會輸出Hello
三次。
但是可能會有疑惑的是,我們有必要自定義一個函數這么做嗎?我們直接用一個循環語句不是一樣可以完成這樣的工作嗎?這也就是我們為什么需要自定義函數的問題。
在文章開頭我簡單講了一下自定義函數可以把操作進行整合,可以集中變量環境,這里我們仔細說明一下這些話是什么意思。
誠然我們可以通過一個循環語句來完成重復輸出字符串的工作。但是,如果我們在程序中需要多次用到這個功能呢,是不是我們每次都要再寫一個循環語句呢?情況更糟的是,如果代碼寫完了好幾天以后,我突然想要在每次輸出這個字符串以后再輸出一個另一個字符串呢?如果我們使用了函數,這時候我們可以把函數改成這樣
def print_times(_string, _time, fix_string=None):
if fix_string is None:
for i in range(_time):
print(_string)
else:
for i in range(_time):
print(_string)
print(fix_string)
或者這樣
def print_times(_string, _time, fix_string=None):
def print_times_former(_string, _time):
for i in range(_time):
print(_string)
if fix_string is not None:
_string += '\n' + fix_string
print_times_former(_string, _time)
或者我們可以寫一個裝飾器(功能會更局限,在此不演示了),總之方法有很多。
注意到我給新參數一個默認值并且使用了一個判斷語句,這樣原來調用print_times
函數的地方不會報錯,會像原來一樣完成工作(有默認值的參數會在下面介紹)。我們可以去調用了print_times
函數的地方加上我們需要使用的函數,它們就可以完成新功能了。
可能你還可以反駁,就算我寫了幾遍循環,我就去用了循環的地方添上不就行了嗎。那好,我的問題是,如果一個文件代碼量很大,那么多for語句,你要找出來是重復輸出字符串的地方恐怕也挺費勁吧,不小心改到別的循環運行有問題是不是還得回來找?如果用了函數,在任何編輯器中ctrl+F
查找print_times
結果就一目了然了(在編輯器如VS Code中你只要選中這個字段就能清楚看到,甚至不需要搜索,而且可以復選進行同步修改)。
而且試想一下,這只是一個簡單的重復輸出字符串的功能而已,如果是更復雜的功能,函數的優勢就更明顯了。這還是沒有返回值的函數,涉及到返回值時,函數的優勢非常大,下面我們會談到。函數還可以在別的文件中引用,而不是直接復制粘貼一大段代碼過來。
言歸正傳,我們來看看最開始的簡單的print_times
函數是怎么工作的。我們把_string
和_time
作為參數,在函數體的執行語句中定義了一些操作,但是如果我們不調用這個函數,那么什么都不會發生。其實自定義函數就像是一個模板,我們給出要操作的對象的典型(就是參數),在函數體中給出它的操作語句。定義自定義函數的時候它是不會真的對這些參數進行操作的,它只是用來規定我們操作參數的方法。我們定義了一些對這些參數的操作,然后把它打包成一個函數。意思就是,要是以后要對一些變量用這個函數,那么程序就請按這樣操作吧。
于是,當我們print_times('Hello!', 3)
這樣調用print_times
函數的時候,程序就會完成我們規定好了的工作。要注意的是,僅僅是print_times
的話一般代表這個函數本身,它有可能是函數變量,也有可能是函數對象。而如果函數后面加上括號,在括號里面給出作為參數的變量,print_times('Hello!', 3)
就是調用這個函數。這些知識還是參考Python精進-裝飾器與函數對象。
需要說明的是,函數調用的時候,變量的順序是要和函數參數定義的時候聲明參數的數量相等且順序一致的。除非我們在給定參數的時候指明參數名,如
print_times(_time=3, _string='Hello!',)
這樣即使順序和參數聲明的時候的順序不一致,解釋器也能完成正常完成功能。但是這個方法非常不推薦大家使用,原因在后面會再提。之所以要說函數參數的順序問題,因為這涉及到其他形式的函數參數,包括有默認值的參數和可選參數。
接下來我們先介紹有默認值的函數參數。
參數的初始值
其實參數有默認值的函數我們在上面就見過一個,但是在這里我們先不去管他。我們先來看看這個所謂的參數默認值是什么樣的。
def func_defualt(a=3)
print(a)
func()
func(2)
注意到形式其實很簡單,就是在聲明函數參數的時候用賦值語句給參數一個初始值。在這樣的情況下,我們本來調用函數是需要給出變量作為參數的,但是如果我們的參數有默認值,那么如果我們在調用這個函數時不實例化這個參數,那么程序就會用參數的默認值進行操作。上面的兩條調用語句,分別會輸出3
和2
。
接下來要說的,就是剛才我們所說過的參數順序的問題。直接先說結論,有默認值的參數要放在所有沒有默認值的參數后面。這個規定不像之前涉及過的編程習慣問題,這是默認Python解釋器規定的出錯類型。
>>> def func_default2(a=1,b):
... print(a, b)
...
File "<stdin>", line 1
SyntaxError: non-default argument follows default argument
>>>
Python之所以要這樣規定,是為了減少程序出錯的可能性,是出于safety的考慮。在程序中safety和security是不一樣的概念,security一般指程序抵御外部攻擊的能力,safety則一般指程序運行的穩定性。
試想一下,如果我們能夠用def func(a=1,b):
這樣的形式定義函數,那么調用這個函數的時候就可能會出現問題。首先,如果你按照順序給出了所有參數的值,或者雖然打亂順序但是對應好參數名用變量賦值了,那么你有什么必要給這個參數一個默認值呢?那到了想讓參數默認值發揮作用的場景,你也只能把除了有默認值的參數以外的其他參數都對應好參數名用變量賦值,這不僅麻煩而且容易出現紕漏,如果有某個參數沒有值,程序就會報錯。而且,在實際編程中,函數參數有可能遠遠不止兩個,如果其中一部分有默認值一部分沒有,但是順序又被打亂了,那么調用這個函數將會是非常糟糕的一件事情。所以,為了省去不必要的麻煩,Python解釋器將這個按道理來說也是編程習慣的做法變成了強制的規定。
當然,以上一大段都不重要,只要記住一點,有默認值的參數要放在所有沒有默認值的參數后面。
另外值得一提的是,一般參數在函數調用時,如果不給出參數名,不能置于有默認值的參數之后。
>>> def func_default2(a, b=1):
... print(a, b)
...
>>> func_default2(b=2, 3)
File "<stdin>", line 1
SyntaxError: positional argument follows keyword argument
>>>
range函數的練習
知道了上面的概念以后,我們來拿range
函數當作練習。由于還沒有介紹過生成器,而且我們練習的重點是函數參數的設計,因此我們只需要返回range()
對象就行。要求像Python內置的range函數給定參數的規定一樣
- 當只用一個變量調用這個函數時,這個變量指的是輸出的等差數列的終點,如
range(5)
- 當給定兩個變量時,分別指輸出的起始值和終點,,如
range(2, 5)
- 當給定三個變量時,在上一條的基礎上第三個變量指輸出時的步長,如
range(2, 5, -1)
(假定我們調用這個函數時總是用整數或浮點數)
分析一下如何實現這個函數,下面給出我的思路作為參考
- 一共需要三個參數是顯而易見的;
- 最直觀的感受是起始值是要有默認值的,如果不規定從哪里開始,那就從0開始;
- 步長也是要有默認值的,如果不規定,那么步長是1;
- 根據有默認值的參數要放在后面的原則,那么最理所當然的參數設計是
range_custom(stop, start=0, step=1)
- 這個方案看上去可行,但是不滿足剛才的后面兩個要求,如果我們這樣用兩個變量調用,起始值和終點是反的;
- 我們加個判斷就可以了,如果start用了初始值,那么說明我們調用的時候只給了一個參數,這個時候stop就是終點,如果start被重新賦值了說明給了至少兩個參數,那么這時候把stop和start的值調換一下就可以了;
- 現在這個函數似乎可以滿足大多數情況了,但是有一個bug,如果給定參數的時候給的start值就是0怎么辦呢?如
range_custom(-5, 0)
按目前的規則會被翻譯成range(0, -5)
,但是我們的目的卻是range(-5, 0)
; - 所以start的初始值不應該是數字而是別的數據類型,為了方便起見,我們把它的初始值賦為
None
,我們的程序雛形就出來了。
def range_custom(stop, start=None, step=1):
if start is None:
return range(stop)
return range(stop, start, step)
現在這個程序已經滿足我們的要求了,但是看上去不太舒服,可以改成
def range_custom(start, stop=None, step=1):
if stop is None:
return range(start)
return range(start, stop, step)
現在這個函數的參數順序在邏輯上更好理解一些,可以說基本上滿足我們的要求了。當然,本例只是為了說明參數的順序問題,并不是為了實現range函數。事實上Python的range函數還包括參數實例化,生成器等知識,在后面我們應該還有機會再接觸它。
可選參數
說到可選參數,可能有的人見過,卻也不明白到底是什么意思,它一般是這樣出現的
def func_option(*args):
return args
注意到我們聲明函數的時候在參數名前加了個*
星號,這是聲明可選參數的方法。那么可選參數到底有什么用呢?
可選參數的作用是用元組把所有多余的變量收集起來,這個元組的名字就是這個可選參數名。在上例func_option
中我們可以用任意多個變量調用它,比如a = func_option(1, 2, 3)
那么a
就會是元組(1, 2, 3)
。關于為什么是元組而不是列表,我們在上一篇Python進階-簡單數據結構中說過,元組在Python中往往是比列表更優先考慮使用的數據結構,具體原因在本文靠后深入自定義函數參數部分會討論。
我們剛才說可選參數會收集多余的變量。我這么說是有原因的。
>>> def func_option(a, *args, c=2):
... return args
...
>>> func_option2(1)
()
>>> func_option2(1, 2)
(2,)
>>> func_option2(1, 2, 3)
(2, 3)
注意到我們的*args
把除了給普通參數的第一個變量以外的值都放進了元組中。這樣做導致了一個,問題在于我們的有默認值的參數如果不給定參數名地調用的話,就永遠只能用默認值了。而且如果我們在調用函數時不把有默認值的參數放在最后面程序還會報錯。
>>> func_option2(c=1, 2, 3)
File "<stdin>", line 1
SyntaxError: positional argument follows keyword argument
那么有沒有好的辦法能規避這個問題呢?我們可以試試把可選參數放在有默認值的參數后面。
>>> def func_option3(a, c=2, *args):
... return args
...
>>> func_option3(1)
()
>>> func_option3(1, 2)
()
>>> func_option3(1, 2, 3)
(3,)
>>> func_option2(c=1, 2, 3)
File "<stdin>", line 1
SyntaxError: positional argument follows keyword argument
那么這種形式的函數能不能解決之前的問題呢??瓷先ゲ恍校贿^我們知道了,調用函數的時候,要盡量把有默認值的參數放在靠后
的位置賦予變量。那么這兩種我們到底該用哪個方法呢?在實際操作中,我們傾向于將可選參數放在有默認值的參數之后,而且如果參數較多,我們傾向于調用函數時都會所有變量都加上參數名。而且實際操作中,其實可選參數用得不那么多,相對來說,另一種可選參數其實用得更多。這種可選參數
的形式一般是這樣
def func_optionkw(**kwargs):
return args
在這種情況下,關鍵字可選參數都是作為鍵值對保存在參數名的的字典中。也就是說,在調用函數時,在滿足一般參數以后,變量都應該以賦值語句的形式給出,等號左邊作為鍵右邊作為值。如果不這樣做,就會報錯了。
>>> func_optionkw(3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: t2() takes 0 positional arguments but 1 was given
需要說明的是,一個自定義函數只能有一個可選參數,同時也可以有至多一個關鍵字參數。其中關鍵字參數應該放在普通可選參數之后。
現在我們來總結一下函數參數順序一般規律:
- 一般參數放在最前面
- 可選參數放在最后面
- 關鍵字可選參數放在一般可選參數后面
- 函數調用時盡量把有默認值的參數對應的變量放在靠后的位置
- 如果參數比較多,調用函數時,最好所有變量都指明參數名
以上這些,有的是為了防止函數定義時出錯,有的是為了防止函數調用時出錯,總之,應該養成良好的編程習慣。
自定義函數的返回值
我們使用自定義函數集成對變量的操作,那么我們如何獲得變量操作的結果呢?一般來說有兩種,一種是對變量進行操作使其本身變化,這種行為是極不提倡的,這是不利于上面提到過的safety的,因為通過函數操作變量會帶來不確定性,在下一部分我們會詳細介紹;還有一種就是用變量當作運算的初始值,最后返回運算的結果。在上面的例子中,我們一般都是后面這種方法定義函數。
需要說明的是,這個返回值說是運算的結果,其實類型非常寬容。它可以是經過操數值運算后的一個數據,他也可以是列表元組等數據結構,它可以是個函數,它還可以是調用某個函數后用其返回值當作自己的返回值,總之返回值非常靈活。
那么我們剛才說的通過函數對變量本身進行操作的方法需不需要返回值呢?一般來說是不需要的,在C語言中,我們習慣性會對這種函數設置一個return 0
這是為了檢測是否函數正常運行,在Python中我們當然也可以這么做。雖然我說這種方法不安全,不常用,但是幾乎每個C語言都會都會用到這個方法,這個方法一般用在main()
函數中。關于編程范式的知識在這里就不展開講了,我就只順便簡單講講Python中的main()
函數一般長什么樣子。
if __name__ = '__main__':
pass
不管見過沒見過,這個結構都是Python編程中非常普遍的方法。這個結構的功能是,如果該.py
文件不是被其他文件import
引用,就執行pass
部分的語句。這就相當于Python的main()
函數。如果我們直接執行Python文件,那么執行的就是這些語句。如果采用了這種結構,那么這個文件中的其他部分要么是靜態變量,要么就是定義好了的函數。我們通過這個結構來調用一系列集成過的自定義函數來完成某種復雜的功能。
深入自定義函數參數
在這個部分中,我們會重點講一下關于Python可變對象和不可變對象在函數中需要注意的地方。這個知識點幾乎是面試必考內容,因為它體現了一個Python使用者對Python數據類型的理解以及函數設計方面的認識
可變和不可變
首先我們要介紹一下到底什么是可變對象什么是不可變對象。在之前即使介紹數據結構我也沒有展開來講,為的就是現在和函數參數一起進行說明。我們就拿列表和元組舉例,這是我們之前講過的典型的可變和不可變對象。
首先是列表:
>>> list_sample = [1, 2, 3]
>>> list_sample_mirror = list_sample
>>> id(list_sample) # id函數用來查看變量在內存中的地址
1372626593864
>>> id(ist_sample_mirror)
1372626593864
>>> list_sample[1] = 5
>>> id(list_sample)
1372626593864
>>> list_sample[1] += [4]
>>> id(list_sample)
1372626593864
>>> print(list_sample_mirror)
[1, 5, 3, 4]
注意到我們可以更改列表的值,更改列表的值以后,本來和它初值相等的另一個列表也被改變了。出現這種現象的原因在于,由于Python的引用語義,變量賦值往往會指向一個內存中的最終對象,如果存在就直接引用。那么對于可變對象來說,改變它的值,就是對內存中的那個對象進行修改,因此其他引用這個對象的變量也受到“牽連”了。
那我們再來看元組又是什么情況呢:
>>> tuple_sample = (1, 2, 3)
>>> tuple_sample_mirror = tuple_sample
>>> id(tuple_sample)
2473662073160
>>> id(tuple_sample_mirror)
2473662073160
>>> tuple_sample[1] = 5
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> tuple_sample += (4, 5)
>>> tuple_sample
(1, 2, 3, 4, 5)
>>> id(tuple_sample)
2473625127928
>>> tuple_sample_mirror
(1, 2, 3)
>>> id(tuple_sample_mirror)
2473662073160
可以看到一樣是引用同一個內存對象,但是元組不允許改變其中的元素。不過好在元組也支持連接操作,但是和列表有什么區別呢,我們看到,連接后的元組其實已經不是原來那個元組了,其實Python按照要求的操作重新創建了一個元組將其賦值給這個變量。而另一個引用原來的元組的變量沒有受到任何影響。Python通過限制操作來控制元組的穩定性。
這種情況下,通過賦值得來的tuple_sample_mirror
就更加“safe”,它的值會保持在我們的意料之中。
需要說明的是,在函數中,這些事情一樣會發生。
列表
def func_mutable(list_a):
list_a += [1]
print(list_a)
a = [0]
func_mutable(a) # 輸出[0, 1]
print(a) # 輸出[0, 1]
func_mutable(a) # 輸出[0, 1, 1]
print(a) # 輸出[0, 1, 1]
元組
def func_immutable(tuple_a):
tuple_a += (1,)
print(tuple_a)
a = (0,)
func_mutable(a) # 輸出(0, 1)
print(a) # 輸出(0,)
func_mutable(a) # 輸出(0, 1)
print(a) # 輸出(0,)
以上其實就是可變對象和不可變對象的區別。需要注意的是,可變對象有些操作也是不改變這個對象的,如索引操作。而不可變對象只要不對變量重新賦值,那么原來的變量永遠不會變。
Python中另外一些數據類型幾乎都是不可變的,如字符串和數字以及布爾值還有None。由于可變和不可變帶來的相關操作細節非常多。比如說為什么在判斷None的時候優先使用is None
而不去判斷==None
,因為所有None都是用的同一個對象,判斷時只需要查找內存地址看是不是引用同一個地址,而不用去看地址里面的內容是不是一致了。
可變對象作為函數參數
現在我們回到函數的問題上來,即可變對象作為函數參數的操作處理。我們先看一個例子:
def func_mutable(list_a=[]):
list_a += [1]
print(list_a)
func_mutable()
func_mutable()
注意到這個函數只有一個有默認值的參數,這個參數的默認值是一個空列表。那么實際操作中,會有什么樣的問題出現呢?問題就在于,我們兩次調用這個函數的輸出是不一樣的。兩次分別是[1]
和[1, 1]
這是不合常理的。我們又沒有改變參數的默認值,為什么函數執行結果還能不一樣呢?原因就在于我們的參數默認值是個可變對象。
我們在Python精進-裝飾器與函數對象中先把函數比作了列表,后來修正成為了元組。那學過簡單數據結構以后,我今天要給出新的類比了,自定義函數其實更像是不可變字典。字典和不可變這兩個概念都已經介紹過,那么合在一起理解起來應該難度也不大。Python的自定義函數有許多內置方法來保存運行所需要的信息,就像是用鍵值對保存信息的字典,不僅如此,它的鍵和值分別都是不可變對象。Python自定義函數用來保存參數默認值的內置方法是__defaults__
,我們可以直接調用它來查看函數參數的默認值。那么我們就來試一下。
def func_mutable(list_a=[], tuple_b=()):
list_a += [1]
tuple_b += (1,)
print(list_a, tuple_b)
print(func_mutable.__defaults__)
func_mutable()
print(func_mutable.__defaults__)
func_mutable()
print(func_mutable.__defaults__)
執行這個文件的輸出結果是這樣的:
([], ())
[1] (1, )
([1], ())
[1, 1] (1, )
([1, 1], ())
可以清楚地看到,Python是用元組來保存參數默認值信息的。當元組中的可變對象被操作以后,元組保留了操作結果。同樣進行了操作,tuple_b
卻沒有改變默認值,而且它的輸出結果和我們設想的一樣,兩次都是同樣的輸出結果。
通過以上的對比我們不難看出,列表是不適合作為函數參數使用的,至少,不應該有默認值。如果一定要用有默認值的列表當作參數,有沒有辦法同時又能保證參數默認值一直是空列表不會變呢?方法是有的。
def func_mutable(list_a=[]):
list_exec = list_a
list_exec += [1]
print(list_a)
這樣做行不行呢?我們在函數體內新聲明一個變量來復制列表的值,對這個新變量而不是列表本身進行操作可不可以?通過前面的講解我們知道,這樣做是自欺欺人的。
而且,我剛才還有一點故意沒說。tuple_b += (1,)
這個操作在我們之前的試驗中,雖然元組自身不會變,但是變量會被重新賦值,那么為什么__defaults__
里面保存的不是這個新元組呢?其實,Python函數在調用是,相當于自動實例化了參數,即使你不用list_exec = list_a
,程序也是這樣做的,程序運行的時候操作對象是list_exec
而不是list_a
。之所以看上去像是直接對參數進行操作,那是為了方便學習者理解,但程序底層會使用更加安全的方式去執行。這也是為什么不要用可變對象當默認值,因為這樣的話,程序執行時,就真的相當于對參數本身進行操作了。
這也是為什么面試的時候老是考這樣的問題,因為如果你能理解這里面的區別,那么說明對Python的運算特點算是有一定的了解了。我們言歸正傳,除了剛才自欺欺人的辦法,有沒有真正有效的方法呢?方法是有的。
def func_mutable(list_a=[]):
list_exec = list_a.copy()
list_exec += [1]
print(list_a)
或者
def func_mutable(list_a=[]):
list_exec = list(list_a)
list_exec += [1]
print(list_a)
這兩種辦法都能解決剛才的問題,都能保證正確的輸出結果。那么到底該選哪個方法,可以看個人取舍,我傾向于推薦第一種方法。但是第二種方法也有好處,它不僅可以用在列表上,用在元組上也是可以的,而且會使我們的操作非常靈活。
那么我們再回頭看一下,我們剛才說Python會自動進行類似list_exec = list_a
這樣的處理,那么它為什么不用list_exec = list_a.copy()
呢?一方面,這種辦法浪費內存,而且運行起來效率要比前者低,另一方面,這樣其實也限制了很多的操作。如果我們對自己有信心,那么利用元組保存列表的形式來構建類似可變元組的方法其實是非常有用的。而且這樣做保留了用函數改變列表的可能性,簡單程序如果面向過程開發往往是最直接最高效的。
但是,我還是要重申,一般來說
- 盡量不要用列表當作變量傳入函數中,尤其不要依賴默認值;
- 如果一定要用列表變量當函數參數,那么在函數中盡量不要涉及修改列表的操作;
- 如果一定要在函數內部進行修改列表的操作,那么最好用安全的辦法復制列表;
- 如果是真的要用函數來改變列表,那么一定要有清晰的思路,程序非常簡單而且是臨時代碼
(以上這些對字典一樣適用)
其中第二點是最關鍵的。我們需要辨別對可變對象的哪些操作是不會改變列表的,哪些是只訪問這個列表的而不進行修改的。這些都是為了能夠提高代碼復用時的穩定性。
裝飾器和函數對象
這個就不展開來講了,跳轉本專題另一篇文章Python精進-裝飾器與函數對象。
最后的廢話
本文和上一篇Python進階-簡單數據結構一樣,字數真的很多。因為即使我只講一些簡單用法,而且我的確在這么做,但是還是有非常多的內容。不過已經是Python進階部分了,多了解一些技術細節也是應該的,但是我還要強調一次,編程重在練習。以上這些用過的簡單例子,大可以用命令行嘗試一下看看輸出結果加深印象。
我一開始的想法是爭取日更,但像現在這樣的一篇1W字日更顯然是不現實的。我也只能晚上睡覺前有空就多少寫一點,爭取周更。
下一篇計劃把之前最開始的一篇環境配置修補一下,補充說明一下Linux環境下Python的配置問題以及遠程連接的問題。