本文為《爬著學(xué)Python》系列第十篇文章。
在實(shí)際操作中反砌,可能函數(shù)是我們幾乎唯一的實(shí)現(xiàn)操作的方式,這是因?yàn)楹瘮?shù)能夠構(gòu)造一個(gè)高度集中的變量環(huán)境葱轩,在合理的設(shè)計(jì)下,它能使程序思路更加清晰的同時(shí)更利于調(diào)整與修改藐握。幾乎沒(méi)有哪個(gè)程序設(shè)計(jì)語(yǔ)言會(huì)不涉及自定義函數(shù)的靴拱。
在上一篇文章中我們留了許多內(nèi)容說(shuō)要在本文中介紹,它們是一些和函數(shù)參數(shù)相關(guān)的問(wèn)題猾普。函數(shù)是我們的對(duì)操作方式的一種整合袜炕,因此我們會(huì)通過(guò)函數(shù)來(lái)進(jìn)行運(yùn)算或者完成某些功能,這些功能涉及到變量時(shí)初家,我們必須清楚到底發(fā)生了哪些事情偎窘。廢話少說(shuō)吧。
創(chuàng)建自定義函數(shù)
Python的自定義函數(shù)格式中規(guī)中矩
def func_name(arg1):
pass
用def
引導(dǎo)自定義函數(shù)名溜在,用括號(hào)給出該函數(shù)的參數(shù)陌知,在冒號(hào)后換行通過(guò)縮進(jìn)確定函數(shù)體。在格式上和條件判斷語(yǔ)句有些相似掖肋。
當(dāng)然纵诞,我們從簡(jiǎn)單的開(kāi)始講起,這是Python自定義函數(shù)的簡(jiǎn)單形式培遵。一般能“動(dòng)手腳”的地方只有三個(gè),一個(gè)是def
前面可以用裝飾器(詳見(jiàn)我的另一篇文章Python精進(jìn)-裝飾器與函數(shù)對(duì)象)登刺,一個(gè)是函數(shù)參數(shù)籽腕,一個(gè)是執(zhí)行語(yǔ)句。
關(guān)于執(zhí)行語(yǔ)句部分纸俭,主要是函數(shù)的嵌套以及控制結(jié)構(gòu)的組合皇耗,這種內(nèi)容作為知識(shí)講解沒(méi)什么意思。大多數(shù)人都知道可以這么做揍很,但很多人做不好郎楼,是不是因?yàn)闆](méi)學(xué)好呢万伤?我覺(jué)得不是的,是練少了呜袁,積累項(xiàng)目經(jīng)驗(yàn)以后就會(huì)逐漸強(qiáng)化這方面的能力敌买。而裝飾器之前專門(mén)提前講過(guò),因此本文的重點(diǎn)會(huì)放在函數(shù)參數(shù)上阶界。之后也會(huì)在深入了解Python自定義函數(shù)參數(shù)設(shè)計(jì)的基礎(chǔ)上去認(rèn)識(shí)如何正確設(shè)置函數(shù)返回值虹钮。
自定義函數(shù)的參數(shù)
首先我要聲明一點(diǎn),我決定不講一般意義上的形參(形式參數(shù))和實(shí)參(實(shí)際參數(shù))的知識(shí)膘融。按道理來(lái)說(shuō)芙粱,即使Python不嚴(yán)格要求定義函數(shù)參數(shù),但這方面的知識(shí)有助于理解自定義函數(shù)中參數(shù)操作的情況氧映,還是應(yīng)該說(shuō)明一下的春畔。但是我仔細(xì)想了一下在Python編程中不知道這兩個(gè)概念真的完全沒(méi)有任何關(guān)系,我們可以簡(jiǎn)單地理解為在定義函數(shù)時(shí)括號(hào)中聲明的參數(shù)是我們?cè)诤瘮?shù)使用中會(huì)用到的參數(shù)岛都,在調(diào)用函數(shù)時(shí)括號(hào)中的變量就是參加函數(shù)運(yùn)算用到的變量律姨。是的,換個(gè)名字疗绣,參數(shù)(用于定義)和變量(用于調(diào)用)就足以理解了线召。
可能完全沒(méi)有基礎(chǔ)的同學(xué)看上面一段話還是有些不明白,這很正常多矮,我們還沒(méi)有講過(guò)函數(shù)的調(diào)用缓淹。沒(méi)關(guān)系再接下來(lái)的例子中我們會(huì)見(jiàn)到。不過(guò)這一節(jié)我們重點(diǎn)是看看函數(shù)定義時(shí)參數(shù)有哪些形式塔逃。
最普通的參數(shù)
最普通的自定義函數(shù)參數(shù)就是在括號(hào)中列出一系列我們要用的參數(shù)讯壶。
def print_times(_string, _time):
for i in range(_time):
print(_string)
print_times('Hello!', 3)
在這個(gè)例子中我們定義函數(shù)時(shí)定義了兩個(gè)變量,分別是_string
和_time
湾盗。這個(gè)函數(shù)的作用不用我過(guò)多說(shuō)明伏蚊,函數(shù)體只有一個(gè)循環(huán)語(yǔ)句,目的是重復(fù)輸出一個(gè)字符串格粪。首先要注意的是為什么我要在"string"和“time”前加下劃線呢躏吊,這是為了防止和其他變量出現(xiàn)沖突。如“string”有可能和內(nèi)置關(guān)鍵字沖突(其實(shí)不會(huì)帐萎,Python字符串內(nèi)置關(guān)鍵字是str比伏,這里是為了保險(xiǎn)起見(jiàn)),“date”有可能在使用了標(biāo)準(zhǔn)庫(kù)datetime
與其中的方法沖突疆导。為了減少歧義赁项,在定義函數(shù)的時(shí)候給變量前面加上下劃線是比較穩(wěn)妥的辦法。這個(gè)技巧是面向?qū)ο缶幊虝r(shí)類設(shè)計(jì)常用的手段,在自定義函數(shù)中一樣可以用悠菜。在后面的例子中舰攒,有時(shí)我會(huì)用比較長(zhǎng)的不太可能沖突的變量就可以不這么做了。
接下來(lái)就是函數(shù)的作用的問(wèn)題悔醋,我們需要重復(fù)輸出一個(gè)字符串摩窃,所以理所當(dāng)然的情況下我們只需要簡(jiǎn)單地涉及兩個(gè)操作對(duì)象,一個(gè)是要輸出的字符串篙顺,一個(gè)是這個(gè)字符串輸出的次數(shù)偶芍。這是比較好理解的。所以我們調(diào)用函數(shù)的時(shí)候德玫,我們給出的字符串是Hello
匪蟀,次數(shù)是3,這個(gè)函數(shù)就會(huì)輸出Hello
三次宰僧。
但是可能會(huì)有疑惑的是材彪,我們有必要自定義一個(gè)函數(shù)這么做嗎?我們直接用一個(gè)循環(huán)語(yǔ)句不是一樣可以完成這樣的工作嗎琴儿?這也就是我們?yōu)槭裁葱枰远x函數(shù)的問(wèn)題段化。
在文章開(kāi)頭我簡(jiǎn)單講了一下自定義函數(shù)可以把操作進(jìn)行整合,可以集中變量環(huán)境造成,這里我們仔細(xì)說(shuō)明一下這些話是什么意思显熏。
誠(chéng)然我們可以通過(guò)一個(gè)循環(huán)語(yǔ)句來(lái)完成重復(fù)輸出字符串的工作。但是晒屎,如果我們?cè)诔绦蛑行枰啻斡玫竭@個(gè)功能呢喘蟆,是不是我們每次都要再寫(xiě)一個(gè)循環(huán)語(yǔ)句呢?情況更糟的是鼓鲁,如果代碼寫(xiě)完了好幾天以后蕴轨,我突然想要在每次輸出這個(gè)字符串以后再輸出一個(gè)另一個(gè)字符串呢?如果我們使用了函數(shù)骇吭,這時(shí)候我們可以把函數(shù)改成這樣
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)
或者我們可以寫(xiě)一個(gè)裝飾器(功能會(huì)更局限橙弱,在此不演示了),總之方法有很多燥狰。
注意到我給新參數(shù)一個(gè)默認(rèn)值并且使用了一個(gè)判斷語(yǔ)句棘脐,這樣原來(lái)調(diào)用print_times
函數(shù)的地方不會(huì)報(bào)錯(cuò),會(huì)像原來(lái)一樣完成工作(有默認(rèn)值的參數(shù)會(huì)在下面介紹)龙致。我們可以去調(diào)用了print_times
函數(shù)的地方加上我們需要使用的函數(shù)荆残,它們就可以完成新功能了。
可能你還可以反駁净当,就算我寫(xiě)了幾遍循環(huán),我就去用了循環(huán)的地方添上不就行了嗎。那好像啼,我的問(wèn)題是俘闯,如果一個(gè)文件代碼量很大,那么多for語(yǔ)句忽冻,你要找出來(lái)是重復(fù)輸出字符串的地方恐怕也挺費(fèi)勁吧真朗,不小心改到別的循環(huán)運(yùn)行有問(wèn)題是不是還得回來(lái)找?如果用了函數(shù)僧诚,在任何編輯器中ctrl+F
查找print_times
結(jié)果就一目了然了(在編輯器如VS Code中你只要選中這個(gè)字段就能清楚看到遮婶,甚至不需要搜索,而且可以復(fù)選進(jìn)行同步修改)湖笨。
而且試想一下旗扑,這只是一個(gè)簡(jiǎn)單的重復(fù)輸出字符串的功能而已,如果是更復(fù)雜的功能慈省,函數(shù)的優(yōu)勢(shì)就更明顯了臀防。這還是沒(méi)有返回值的函數(shù),涉及到返回值時(shí)边败,函數(shù)的優(yōu)勢(shì)非常大袱衷,下面我們會(huì)談到。函數(shù)還可以在別的文件中引用笑窜,而不是直接復(fù)制粘貼一大段代碼過(guò)來(lái)致燥。
言歸正傳,我們來(lái)看看最開(kāi)始的簡(jiǎn)單的print_times
函數(shù)是怎么工作的排截。我們把_string
和_time
作為參數(shù)嫌蚤,在函數(shù)體的執(zhí)行語(yǔ)句中定義了一些操作,但是如果我們不調(diào)用這個(gè)函數(shù)匾寝,那么什么都不會(huì)發(fā)生搬葬。其實(shí)自定義函數(shù)就像是一個(gè)模板,我們給出要操作的對(duì)象的典型(就是參數(shù))艳悔,在函數(shù)體中給出它的操作語(yǔ)句急凰。定義自定義函數(shù)的時(shí)候它是不會(huì)真的對(duì)這些參數(shù)進(jìn)行操作的,它只是用來(lái)規(guī)定我們操作參數(shù)的方法猜年。我們定義了一些對(duì)這些參數(shù)的操作抡锈,然后把它打包成一個(gè)函數(shù)。意思就是乔外,要是以后要對(duì)一些變量用這個(gè)函數(shù)床三,那么程序就請(qǐng)按這樣操作吧。
于是杨幼,當(dāng)我們print_times('Hello!', 3)
這樣調(diào)用print_times
函數(shù)的時(shí)候撇簿,程序就會(huì)完成我們規(guī)定好了的工作聂渊。要注意的是,僅僅是print_times
的話一般代表這個(gè)函數(shù)本身四瘫,它有可能是函數(shù)變量汉嗽,也有可能是函數(shù)對(duì)象。而如果函數(shù)后面加上括號(hào)找蜜,在括號(hào)里面給出作為參數(shù)的變量饼暑,print_times('Hello!', 3)
就是調(diào)用這個(gè)函數(shù)。這些知識(shí)還是參考Python精進(jìn)-裝飾器與函數(shù)對(duì)象洗做。
需要說(shuō)明的是弓叛,函數(shù)調(diào)用的時(shí)候,變量的順序是要和函數(shù)參數(shù)定義的時(shí)候聲明參數(shù)的數(shù)量相等且順序一致的诚纸。除非我們?cè)诮o定參數(shù)的時(shí)候指明參數(shù)名撰筷,如
print_times(_time=3, _string='Hello!',)
這樣即使順序和參數(shù)聲明的時(shí)候的順序不一致,解釋器也能完成正常完成功能咬清。但是這個(gè)方法非常不推薦大家使用闭专,原因在后面會(huì)再提。之所以要說(shuō)函數(shù)參數(shù)的順序問(wèn)題旧烧,因?yàn)檫@涉及到其他形式的函數(shù)參數(shù)影钉,包括有默認(rèn)值的參數(shù)和可選參數(shù)。
接下來(lái)我們先介紹有默認(rèn)值的函數(shù)參數(shù)掘剪。
參數(shù)的初始值
其實(shí)參數(shù)有默認(rèn)值的函數(shù)我們?cè)谏厦婢鸵?jiàn)過(guò)一個(gè)平委,但是在這里我們先不去管他。我們先來(lái)看看這個(gè)所謂的參數(shù)默認(rèn)值是什么樣的夺谁。
def func_defualt(a=3)
print(a)
func()
func(2)
注意到形式其實(shí)很簡(jiǎn)單廉赔,就是在聲明函數(shù)參數(shù)的時(shí)候用賦值語(yǔ)句給參數(shù)一個(gè)初始值。在這樣的情況下匾鸥,我們本來(lái)調(diào)用函數(shù)是需要給出變量作為參數(shù)的蜡塌,但是如果我們的參數(shù)有默認(rèn)值,那么如果我們?cè)谡{(diào)用這個(gè)函數(shù)時(shí)不實(shí)例化這個(gè)參數(shù)勿负,那么程序就會(huì)用參數(shù)的默認(rèn)值進(jìn)行操作馏艾。上面的兩條調(diào)用語(yǔ)句,分別會(huì)輸出3
和2
奴愉。
接下來(lái)要說(shuō)的琅摩,就是剛才我們所說(shuō)過(guò)的參數(shù)順序的問(wèn)題。直接先說(shuō)結(jié)論锭硼,有默認(rèn)值的參數(shù)要放在所有沒(méi)有默認(rèn)值的參數(shù)后面房资。這個(gè)規(guī)定不像之前涉及過(guò)的編程習(xí)慣問(wèn)題,這是默認(rèn)Python解釋器規(guī)定的出錯(cuò)類型檀头。
>>> def func_default2(a=1,b):
... print(a, b)
...
File "<stdin>", line 1
SyntaxError: non-default argument follows default argument
>>>
Python之所以要這樣規(guī)定轰异,是為了減少程序出錯(cuò)的可能性岖沛,是出于safety的考慮。在程序中safety和security是不一樣的概念溉浙,security一般指程序抵御外部攻擊的能力烫止,safety則一般指程序運(yùn)行的穩(wěn)定性。
試想一下戳稽,如果我們能夠用def func(a=1,b):
這樣的形式定義函數(shù),那么調(diào)用這個(gè)函數(shù)的時(shí)候就可能會(huì)出現(xiàn)問(wèn)題期升。首先惊奇,如果你按照順序給出了所有參數(shù)的值,或者雖然打亂順序但是對(duì)應(yīng)好參數(shù)名用變量賦值了播赁,那么你有什么必要給這個(gè)參數(shù)一個(gè)默認(rèn)值呢颂郎?那到了想讓參數(shù)默認(rèn)值發(fā)揮作用的場(chǎng)景,你也只能把除了有默認(rèn)值的參數(shù)以外的其他參數(shù)都對(duì)應(yīng)好參數(shù)名用變量賦值容为,這不僅麻煩而且容易出現(xiàn)紕漏乓序,如果有某個(gè)參數(shù)沒(méi)有值,程序就會(huì)報(bào)錯(cuò)坎背。而且替劈,在實(shí)際編程中,函數(shù)參數(shù)有可能遠(yuǎn)遠(yuǎn)不止兩個(gè)得滤,如果其中一部分有默認(rèn)值一部分沒(méi)有陨献,但是順序又被打亂了,那么調(diào)用這個(gè)函數(shù)將會(huì)是非常糟糕的一件事情懂更。所以眨业,為了省去不必要的麻煩,Python解釋器將這個(gè)按道理來(lái)說(shuō)也是編程習(xí)慣的做法變成了強(qiáng)制的規(guī)定沮协。
當(dāng)然龄捡,以上一大段都不重要,只要記住一點(diǎn)慷暂,有默認(rèn)值的參數(shù)要放在所有沒(méi)有默認(rèn)值的參數(shù)后面聘殖。
另外值得一提的是,一般參數(shù)在函數(shù)調(diào)用時(shí)呜呐,如果不給出參數(shù)名就斤,不能置于有默認(rèn)值的參數(shù)之后。
>>> 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函數(shù)的練習(xí)
知道了上面的概念以后蘑辑,我們來(lái)拿range
函數(shù)當(dāng)作練習(xí)洋机。由于還沒(méi)有介紹過(guò)生成器,而且我們練習(xí)的重點(diǎn)是函數(shù)參數(shù)的設(shè)計(jì)洋魂,因此我們只需要返回range()
對(duì)象就行绷旗。要求像Python內(nèi)置的range函數(shù)給定參數(shù)的規(guī)定一樣
- 當(dāng)只用一個(gè)變量調(diào)用這個(gè)函數(shù)時(shí)喜鼓,這個(gè)變量指的是輸出的等差數(shù)列的終點(diǎn),如
range(5)
- 當(dāng)給定兩個(gè)變量時(shí)衔肢,分別指輸出的起始值和終點(diǎn),庄岖,如
range(2, 5)
- 當(dāng)給定三個(gè)變量時(shí),在上一條的基礎(chǔ)上第三個(gè)變量指輸出時(shí)的步長(zhǎng)角骤,如
range(2, 5, -1)
(假定我們調(diào)用這個(gè)函數(shù)時(shí)總是用整數(shù)或浮點(diǎn)數(shù))
分析一下如何實(shí)現(xiàn)這個(gè)函數(shù)隅忿,下面給出我的思路作為參考
- 一共需要三個(gè)參數(shù)是顯而易見(jiàn)的;
- 最直觀的感受是起始值是要有默認(rèn)值的邦尊,如果不規(guī)定從哪里開(kāi)始背桐,那就從0開(kāi)始;
- 步長(zhǎng)也是要有默認(rèn)值的蝉揍,如果不規(guī)定链峭,那么步長(zhǎng)是1;
- 根據(jù)有默認(rèn)值的參數(shù)要放在后面的原則又沾,那么最理所當(dāng)然的參數(shù)設(shè)計(jì)是
range_custom(stop, start=0, step=1)
- 這個(gè)方案看上去可行弊仪,但是不滿足剛才的后面兩個(gè)要求,如果我們這樣用兩個(gè)變量調(diào)用杖刷,起始值和終點(diǎn)是反的励饵;
- 我們加個(gè)判斷就可以了,如果start用了初始值挺勿,那么說(shuō)明我們調(diào)用的時(shí)候只給了一個(gè)參數(shù)曲横,這個(gè)時(shí)候stop就是終點(diǎn),如果start被重新賦值了說(shuō)明給了至少兩個(gè)參數(shù)不瓶,那么這時(shí)候把stop和start的值調(diào)換一下就可以了禾嫉;
- 現(xiàn)在這個(gè)函數(shù)似乎可以滿足大多數(shù)情況了,但是有一個(gè)bug蚊丐,如果給定參數(shù)的時(shí)候給的start值就是0怎么辦呢熙参?如
range_custom(-5, 0)
按目前的規(guī)則會(huì)被翻譯成range(0, -5)
,但是我們的目的卻是range(-5, 0)
麦备; - 所以start的初始值不應(yīng)該是數(shù)字而是別的數(shù)據(jù)類型孽椰,為了方便起見(jiàn),我們把它的初始值賦為
None
凛篙,我們的程序雛形就出來(lái)了黍匾。
def range_custom(stop, start=None, step=1):
if start is None:
return range(stop)
return range(stop, start, step)
現(xiàn)在這個(gè)程序已經(jīng)滿足我們的要求了,但是看上去不太舒服呛梆,可以改成
def range_custom(start, stop=None, step=1):
if stop is None:
return range(start)
return range(start, stop, step)
現(xiàn)在這個(gè)函數(shù)的參數(shù)順序在邏輯上更好理解一些锐涯,可以說(shuō)基本上滿足我們的要求了。當(dāng)然填物,本例只是為了說(shuō)明參數(shù)的順序問(wèn)題纹腌,并不是為了實(shí)現(xiàn)range函數(shù)霎终。事實(shí)上Python的range函數(shù)還包括參數(shù)實(shí)例化,生成器等知識(shí)升薯,在后面我們應(yīng)該還有機(jī)會(huì)再接觸它莱褒。
可選參數(shù)
說(shuō)到可選參數(shù),可能有的人見(jiàn)過(guò)涎劈,卻也不明白到底是什么意思广凸,它一般是這樣出現(xiàn)的
def func_option(*args):
return args
注意到我們聲明函數(shù)的時(shí)候在參數(shù)名前加了個(gè)*
星號(hào),這是聲明可選參數(shù)的方法蛛枚。那么可選參數(shù)到底有什么用呢炮障?
可選參數(shù)的作用是用元組把所有多余的變量收集起來(lái),這個(gè)元組的名字就是這個(gè)可選參數(shù)名坤候。在上例func_option
中我們可以用任意多個(gè)變量調(diào)用它,比如a = func_option(1, 2, 3)
那么a
就會(huì)是元組(1, 2, 3)
企蹭。關(guān)于為什么是元組而不是列表白筹,我們?cè)谏弦黄?a href="http://www.reibang.com/p/e6c4683a511d" target="_blank">Python進(jìn)階-簡(jiǎn)單數(shù)據(jù)結(jié)構(gòu)中說(shuō)過(guò),元組在Python中往往是比列表更優(yōu)先考慮使用的數(shù)據(jù)結(jié)構(gòu)谅摄,具體原因在本文靠后深入自定義函數(shù)參數(shù)部分會(huì)討論徒河。
我們剛才說(shuō)可選參數(shù)會(huì)收集多余的變量。我這么說(shuō)是有原因的送漠。
>>> 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
把除了給普通參數(shù)的第一個(gè)變量以外的值都放進(jìn)了元組中顽照。這樣做導(dǎo)致了一個(gè),問(wèn)題在于我們的有默認(rèn)值的參數(shù)如果不給定參數(shù)名地調(diào)用的話闽寡,就永遠(yuǎn)只能用默認(rèn)值了代兵。而且如果我們?cè)谡{(diào)用函數(shù)時(shí)不把有默認(rèn)值的參數(shù)放在最后面程序還會(huì)報(bào)錯(cuò)。
>>> func_option2(c=1, 2, 3)
File "<stdin>", line 1
SyntaxError: positional argument follows keyword argument
那么有沒(méi)有好的辦法能規(guī)避這個(gè)問(wèn)題呢爷狈?我們可以試試把可選參數(shù)放在有默認(rèn)值的參數(shù)后面植影。
>>> 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
那么這種形式的函數(shù)能不能解決之前的問(wèn)題呢∠延溃看上去不行思币,不過(guò)我們知道了,調(diào)用函數(shù)的時(shí)候羡微,要盡量把有默認(rèn)值的參數(shù)放在靠后
的位置賦予變量谷饿。那么這兩種我們到底該用哪個(gè)方法呢?在實(shí)際操作中妈倔,我們傾向于將可選參數(shù)放在有默認(rèn)值的參數(shù)之后博投,而且如果參數(shù)較多,我們傾向于調(diào)用函數(shù)時(shí)都會(huì)所有變量都加上參數(shù)名启涯。而且實(shí)際操作中贬堵,其實(shí)可選參數(shù)用得不那么多恃轩,相對(duì)來(lái)說(shuō),另一種可選參數(shù)其實(shí)用得更多黎做。這種可選參數(shù)
的形式一般是這樣
def func_optionkw(**kwargs):
return args
在這種情況下叉跛,關(guān)鍵字可選參數(shù)都是作為鍵值對(duì)保存在參數(shù)名的的字典中。也就是說(shuō)蒸殿,在調(diào)用函數(shù)時(shí)筷厘,在滿足一般參數(shù)以后,變量都應(yīng)該以賦值語(yǔ)句的形式給出宏所,等號(hào)左邊作為鍵右邊作為值酥艳。如果不這樣做,就會(huì)報(bào)錯(cuò)了爬骤。
>>> func_optionkw(3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: t2() takes 0 positional arguments but 1 was given
需要說(shuō)明的是充石,一個(gè)自定義函數(shù)只能有一個(gè)可選參數(shù),同時(shí)也可以有至多一個(gè)關(guān)鍵字參數(shù)霞玄。其中關(guān)鍵字參數(shù)應(yīng)該放在普通可選參數(shù)之后骤铃。
現(xiàn)在我們來(lái)總結(jié)一下函數(shù)參數(shù)順序一般規(guī)律:
- 一般參數(shù)放在最前面
- 可選參數(shù)放在最后面
- 關(guān)鍵字可選參數(shù)放在一般可選參數(shù)后面
- 函數(shù)調(diào)用時(shí)盡量把有默認(rèn)值的參數(shù)對(duì)應(yīng)的變量放在靠后的位置
- 如果參數(shù)比較多,調(diào)用函數(shù)時(shí)坷剧,最好所有變量都指明參數(shù)名
以上這些惰爬,有的是為了防止函數(shù)定義時(shí)出錯(cuò),有的是為了防止函數(shù)調(diào)用時(shí)出錯(cuò)惫企,總之撕瞧,應(yīng)該養(yǎng)成良好的編程習(xí)慣。
自定義函數(shù)的返回值
我們使用自定義函數(shù)集成對(duì)變量的操作狞尔,那么我們?nèi)绾潍@得變量操作的結(jié)果呢丛版?一般來(lái)說(shuō)有兩種,一種是對(duì)變量進(jìn)行操作使其本身變化沪么,這種行為是極不提倡的硼婿,這是不利于上面提到過(guò)的safety的,因?yàn)橥ㄟ^(guò)函數(shù)操作變量會(huì)帶來(lái)不確定性禽车,在下一部分我們會(huì)詳細(xì)介紹寇漫;還有一種就是用變量當(dāng)作運(yùn)算的初始值,最后返回運(yùn)算的結(jié)果殉摔。在上面的例子中州胳,我們一般都是后面這種方法定義函數(shù)。
需要說(shuō)明的是逸月,這個(gè)返回值說(shuō)是運(yùn)算的結(jié)果栓撞,其實(shí)類型非常寬容。它可以是經(jīng)過(guò)操數(shù)值運(yùn)算后的一個(gè)數(shù)據(jù),他也可以是列表元組等數(shù)據(jù)結(jié)構(gòu)瓤湘,它可以是個(gè)函數(shù)瓢颅,它還可以是調(diào)用某個(gè)函數(shù)后用其返回值當(dāng)作自己的返回值,總之返回值非常靈活弛说。
那么我們剛才說(shuō)的通過(guò)函數(shù)對(duì)變量本身進(jìn)行操作的方法需不需要返回值呢挽懦?一般來(lái)說(shuō)是不需要的,在C語(yǔ)言中信柿,我們習(xí)慣性會(huì)對(duì)這種函數(shù)設(shè)置一個(gè)return 0
這是為了檢測(cè)是否函數(shù)正常運(yùn)行,在Python中我們當(dāng)然也可以這么做醒第。雖然我說(shuō)這種方法不安全愕宋,不常用用僧,但是幾乎每個(gè)C語(yǔ)言都會(huì)都會(huì)用到這個(gè)方法,這個(gè)方法一般用在main()
函數(shù)中预吆。關(guān)于編程范式的知識(shí)在這里就不展開(kāi)講了誊涯,我就只順便簡(jiǎn)單講講Python中的main()
函數(shù)一般長(zhǎng)什么樣子屡谐。
if __name__ = '__main__':
pass
不管見(jiàn)過(guò)沒(méi)見(jiàn)過(guò)娄柳,這個(gè)結(jié)構(gòu)都是Python編程中非常普遍的方法争拐。這個(gè)結(jié)構(gòu)的功能是,如果該.py
文件不是被其他文件import
引用蝗岖,就執(zhí)行pass
部分的語(yǔ)句。這就相當(dāng)于Python的main()
函數(shù)榔至。如果我們直接執(zhí)行Python文件抵赢,那么執(zhí)行的就是這些語(yǔ)句。如果采用了這種結(jié)構(gòu)唧取,那么這個(gè)文件中的其他部分要么是靜態(tài)變量铅鲤,要么就是定義好了的函數(shù)。我們通過(guò)這個(gè)結(jié)構(gòu)來(lái)調(diào)用一系列集成過(guò)的自定義函數(shù)來(lái)完成某種復(fù)雜的功能枫弟。
深入自定義函數(shù)參數(shù)
在這個(gè)部分中邢享,我們會(huì)重點(diǎn)講一下關(guān)于Python可變對(duì)象和不可變對(duì)象在函數(shù)中需要注意的地方。這個(gè)知識(shí)點(diǎn)幾乎是面試必考內(nèi)容淡诗,因?yàn)樗w現(xiàn)了一個(gè)Python使用者對(duì)Python數(shù)據(jù)類型的理解以及函數(shù)設(shè)計(jì)方面的認(rèn)識(shí)
可變和不可變
首先我們要介紹一下到底什么是可變對(duì)象什么是不可變對(duì)象骇塘。在之前即使介紹數(shù)據(jù)結(jié)構(gòu)我也沒(méi)有展開(kāi)來(lái)講,為的就是現(xiàn)在和函數(shù)參數(shù)一起進(jìn)行說(shuō)明韩容。我們就拿列表和元組舉例款违,這是我們之前講過(guò)的典型的可變和不可變對(duì)象。
首先是列表:
>>> list_sample = [1, 2, 3]
>>> list_sample_mirror = list_sample
>>> id(list_sample) # id函數(shù)用來(lái)查看變量在內(nèi)存中的地址
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]
注意到我們可以更改列表的值群凶,更改列表的值以后插爹,本來(lái)和它初值相等的另一個(gè)列表也被改變了。出現(xiàn)這種現(xiàn)象的原因在于,由于Python的引用語(yǔ)義赠尾,變量賦值往往會(huì)指向一個(gè)內(nèi)存中的最終對(duì)象力穗,如果存在就直接引用。那么對(duì)于可變對(duì)象來(lái)說(shuō)气嫁,改變它的值当窗,就是對(duì)內(nèi)存中的那個(gè)對(duì)象進(jìn)行修改,因此其他引用這個(gè)對(duì)象的變量也受到“牽連”了杉编。
那我們?cè)賮?lái)看元組又是什么情況呢:
>>> 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
可以看到一樣是引用同一個(gè)內(nèi)存對(duì)象超全,但是元組不允許改變其中的元素。不過(guò)好在元組也支持連接操作邓馒,但是和列表有什么區(qū)別呢嘶朱,我們看到,連接后的元組其實(shí)已經(jīng)不是原來(lái)那個(gè)元組了光酣,其實(shí)Python按照要求的操作重新創(chuàng)建了一個(gè)元組將其賦值給這個(gè)變量疏遏。而另一個(gè)引用原來(lái)的元組的變量沒(méi)有受到任何影響。Python通過(guò)限制操作來(lái)控制元組的穩(wěn)定性救军。
這種情況下财异,通過(guò)賦值得來(lái)的tuple_sample_mirror
就更加“safe”,它的值會(huì)保持在我們的意料之中唱遭。
需要說(shuō)明的是戳寸,在函數(shù)中,這些事情一樣會(huì)發(fā)生拷泽。
列表
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,)
以上其實(shí)就是可變對(duì)象和不可變對(duì)象的區(qū)別疫鹊。需要注意的是,可變對(duì)象有些操作也是不改變這個(gè)對(duì)象的司致,如索引操作拆吆。而不可變對(duì)象只要不對(duì)變量重新賦值,那么原來(lái)的變量永遠(yuǎn)不會(huì)變脂矫。
Python中另外一些數(shù)據(jù)類型幾乎都是不可變的枣耀,如字符串和數(shù)字以及布爾值還有None。由于可變和不可變帶來(lái)的相關(guān)操作細(xì)節(jié)非常多庭再。比如說(shuō)為什么在判斷None的時(shí)候優(yōu)先使用is None
而不去判斷==None
捞奕,因?yàn)樗蠳one都是用的同一個(gè)對(duì)象,判斷時(shí)只需要查找內(nèi)存地址看是不是引用同一個(gè)地址拄轻,而不用去看地址里面的內(nèi)容是不是一致了缝彬。
可變對(duì)象作為函數(shù)參數(shù)
現(xiàn)在我們回到函數(shù)的問(wèn)題上來(lái),即可變對(duì)象作為函數(shù)參數(shù)的操作處理哺眯。我們先看一個(gè)例子:
def func_mutable(list_a=[]):
list_a += [1]
print(list_a)
func_mutable()
func_mutable()
注意到這個(gè)函數(shù)只有一個(gè)有默認(rèn)值的參數(shù)谷浅,這個(gè)參數(shù)的默認(rèn)值是一個(gè)空列表。那么實(shí)際操作中,會(huì)有什么樣的問(wèn)題出現(xiàn)呢一疯?問(wèn)題就在于撼玄,我們兩次調(diào)用這個(gè)函數(shù)的輸出是不一樣的。兩次分別是[1]
和[1, 1]
這是不合常理的墩邀。我們又沒(méi)有改變參數(shù)的默認(rèn)值掌猛,為什么函數(shù)執(zhí)行結(jié)果還能不一樣呢?原因就在于我們的參數(shù)默認(rèn)值是個(gè)可變對(duì)象眉睹。
我們?cè)?a href="http://www.reibang.com/p/016573e8f63e" target="_blank">Python精進(jìn)-裝飾器與函數(shù)對(duì)象中先把函數(shù)比作了列表荔茬,后來(lái)修正成為了元組。那學(xué)過(guò)簡(jiǎn)單數(shù)據(jù)結(jié)構(gòu)以后竹海,我今天要給出新的類比了慕蔚,自定義函數(shù)其實(shí)更像是不可變字典。字典和不可變這兩個(gè)概念都已經(jīng)介紹過(guò)斋配,那么合在一起理解起來(lái)應(yīng)該難度也不大孔飒。Python的自定義函數(shù)有許多內(nèi)置方法來(lái)保存運(yùn)行所需要的信息,就像是用鍵值對(duì)保存信息的字典艰争,不僅如此坏瞄,它的鍵和值分別都是不可變對(duì)象。Python自定義函數(shù)用來(lái)保存參數(shù)默認(rèn)值的內(nèi)置方法是__defaults__
甩卓,我們可以直接調(diào)用它來(lái)查看函數(shù)參數(shù)的默認(rèn)值鸠匀。那么我們就來(lái)試一下。
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__)
執(zhí)行這個(gè)文件的輸出結(jié)果是這樣的:
([], ())
[1] (1, )
([1], ())
[1, 1] (1, )
([1, 1], ())
可以清楚地看到逾柿,Python是用元組來(lái)保存參數(shù)默認(rèn)值信息的狮崩。當(dāng)元組中的可變對(duì)象被操作以后,元組保留了操作結(jié)果鹿寻。同樣進(jìn)行了操作,tuple_b
卻沒(méi)有改變默認(rèn)值诽凌,而且它的輸出結(jié)果和我們?cè)O(shè)想的一樣毡熏,兩次都是同樣的輸出結(jié)果。
通過(guò)以上的對(duì)比我們不難看出侣诵,列表是不適合作為函數(shù)參數(shù)使用的痢法,至少,不應(yīng)該有默認(rèn)值杜顺。如果一定要用有默認(rèn)值的列表當(dāng)作參數(shù)财搁,有沒(méi)有辦法同時(shí)又能保證參數(shù)默認(rèn)值一直是空列表不會(huì)變呢?方法是有的躬络。
def func_mutable(list_a=[]):
list_exec = list_a
list_exec += [1]
print(list_a)
這樣做行不行呢尖奔?我們?cè)诤瘮?shù)體內(nèi)新聲明一個(gè)變量來(lái)復(fù)制列表的值,對(duì)這個(gè)新變量而不是列表本身進(jìn)行操作可不可以?通過(guò)前面的講解我們知道提茁,這樣做是自欺欺人的淹禾。
而且,我剛才還有一點(diǎn)故意沒(méi)說(shuō)茴扁。tuple_b += (1,)
這個(gè)操作在我們之前的試驗(yàn)中铃岔,雖然元組自身不會(huì)變,但是變量會(huì)被重新賦值峭火,那么為什么__defaults__
里面保存的不是這個(gè)新元組呢毁习?其實(shí),Python函數(shù)在調(diào)用是卖丸,相當(dāng)于自動(dòng)實(shí)例化了參數(shù)纺且,即使你不用list_exec = list_a
,程序也是這樣做的坯苹,程序運(yùn)行的時(shí)候操作對(duì)象是list_exec
而不是list_a
隆檀。之所以看上去像是直接對(duì)參數(shù)進(jìn)行操作,那是為了方便學(xué)習(xí)者理解粹湃,但程序底層會(huì)使用更加安全的方式去執(zhí)行恐仑。這也是為什么不要用可變對(duì)象當(dāng)默認(rèn)值,因?yàn)檫@樣的話为鳄,程序執(zhí)行時(shí)裳仆,就真的相當(dāng)于對(duì)參數(shù)本身進(jìn)行操作了。
這也是為什么面試的時(shí)候老是考這樣的問(wèn)題孤钦,因?yàn)槿绻隳芾斫膺@里面的區(qū)別歧斟,那么說(shuō)明對(duì)Python的運(yùn)算特點(diǎn)算是有一定的了解了。我們言歸正傳偏形,除了剛才自欺欺人的辦法静袖,有沒(méi)有真正有效的方法呢?方法是有的俊扭。
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)
這兩種辦法都能解決剛才的問(wèn)題队橙,都能保證正確的輸出結(jié)果。那么到底該選哪個(gè)方法萨惑,可以看個(gè)人取舍捐康,我傾向于推薦第一種方法。但是第二種方法也有好處庸蔼,它不僅可以用在列表上解总,用在元組上也是可以的,而且會(huì)使我們的操作非常靈活姐仅。
那么我們?cè)倩仡^看一下花枫,我們剛才說(shuō)Python會(huì)自動(dòng)進(jìn)行類似list_exec = list_a
這樣的處理刻盐,那么它為什么不用list_exec = list_a.copy()
呢?一方面乌昔,這種辦法浪費(fèi)內(nèi)存隙疚,而且運(yùn)行起來(lái)效率要比前者低,另一方面磕道,這樣其實(shí)也限制了很多的操作供屉。如果我們對(duì)自己有信心,那么利用元組保存列表的形式來(lái)構(gòu)建類似可變?cè)M的方法其實(shí)是非常有用的溺蕉。而且這樣做保留了用函數(shù)改變列表的可能性伶丐,簡(jiǎn)單程序如果面向過(guò)程開(kāi)發(fā)往往是最直接最高效的。
但是疯特,我還是要重申哗魂,一般來(lái)說(shuō)
- 盡量不要用列表當(dāng)作變量傳入函數(shù)中,尤其不要依賴默認(rèn)值漓雅;
- 如果一定要用列表變量當(dāng)函數(shù)參數(shù)录别,那么在函數(shù)中盡量不要涉及修改列表的操作;
- 如果一定要在函數(shù)內(nèi)部進(jìn)行修改列表的操作邻吞,那么最好用安全的辦法復(fù)制列表组题;
- 如果是真的要用函數(shù)來(lái)改變列表,那么一定要有清晰的思路抱冷,程序非常簡(jiǎn)單而且是臨時(shí)代碼
(以上這些對(duì)字典一樣適用)
其中第二點(diǎn)是最關(guān)鍵的崔列。我們需要辨別對(duì)可變對(duì)象的哪些操作是不會(huì)改變列表的,哪些是只訪問(wèn)這個(gè)列表的而不進(jìn)行修改的旺遮。這些都是為了能夠提高代碼復(fù)用時(shí)的穩(wěn)定性赵讯。
裝飾器和函數(shù)對(duì)象
這個(gè)就不展開(kāi)來(lái)講了,跳轉(zhuǎn)本專題另一篇文章Python精進(jìn)-裝飾器與函數(shù)對(duì)象耿眉。
最后的廢話
本文和上一篇Python進(jìn)階-簡(jiǎn)單數(shù)據(jù)結(jié)構(gòu)一樣边翼,字?jǐn)?shù)真的很多。因?yàn)榧词刮抑恢v一些簡(jiǎn)單用法鸣剪,而且我的確在這么做组底,但是還是有非常多的內(nèi)容。不過(guò)已經(jīng)是Python進(jìn)階部分了西傀,多了解一些技術(shù)細(xì)節(jié)也是應(yīng)該的,但是我還要強(qiáng)調(diào)一次桶癣,編程重在練習(xí)拥褂。以上這些用過(guò)的簡(jiǎn)單例子,大可以用命令行嘗試一下看看輸出結(jié)果加深印象牙寞。
我一開(kāi)始的想法是爭(zhēng)取日更饺鹃,但像現(xiàn)在這樣的一篇1W字日更顯然是不現(xiàn)實(shí)的莫秆。我也只能晚上睡覺(jué)前有空就多少寫(xiě)一點(diǎn),爭(zhēng)取周更悔详。
下一篇計(jì)劃把之前最開(kāi)始的一篇環(huán)境配置修補(bǔ)一下镊屎,補(bǔ)充說(shuō)明一下Linux環(huán)境下Python的配置問(wèn)題以及遠(yuǎn)程連接的問(wèn)題。