一、函數(shù)簡(jiǎn)介
在程序中番甩,如果實(shí)現(xiàn)了某個(gè)功能的代碼需要多次使用侵贵,就把這段代碼塊組織為一個(gè)小模塊,這就是函數(shù)缘薛。函數(shù)可以提高編程效率模燥,實(shí)現(xiàn)代碼復(fù)用。之前我們接觸到的 print()
掩宜、input()
,也是函數(shù)么翰,它們是 python 的內(nèi)建函數(shù)牺汤。我們也可以自己創(chuàng)建函數(shù),也就是用戶(hù)自定義函數(shù)浩嫌。
例如檐迟,圓的面積的計(jì)算公式為 s = π × r^2
,我們可以創(chuàng)建一個(gè)計(jì)算圓的面積的函數(shù):s = area_of_circle(r)
码耐,就不用每次計(jì)算圓的面積都得寫(xiě)一遍這個(gè)公式追迟,而只需要直接調(diào)用這個(gè)函數(shù)就行。
二骚腥、函數(shù)定義和調(diào)用
函數(shù)必須先定義敦间,后調(diào)用。
1. 定義函數(shù)
定義函數(shù)的格式:
def 函數(shù)名():
代碼(代碼前有縮進(jìn),tab)
# 定義函數(shù),完成打印信息的功能
def printInfo():
print('************************************')
print(' Python 數(shù) 據(jù) 分 析 ')
print('************************************')
2. 調(diào)用函數(shù)
定義函數(shù)之后廓块,就相當(dāng)于完成了某個(gè)功能的代碼厢绝,想要讓代碼能夠執(zhí)行,需要調(diào)用函數(shù)带猴,通過(guò) 函數(shù)名()
即可完成調(diào)用昔汉。
# 定義函數(shù)后,函數(shù)是不能夠自動(dòng)執(zhí)行的拴清,需要調(diào)用函數(shù)靶病,函數(shù)才能執(zhí)行。
printInfo()
注意:
- 調(diào)用函數(shù)時(shí)口予,函數(shù)會(huì)從頭開(kāi)始執(zhí)行娄周,當(dāng)函數(shù)中的代碼執(zhí)行完畢后,則函數(shù)調(diào)用結(jié)束苹威。
- 函數(shù)中如果存在
return
語(yǔ)句昆咽,執(zhí)行到return
語(yǔ)句時(shí),函數(shù)調(diào)用結(jié)束牙甫。
函數(shù)的文檔說(shuō)明:
-
help(函數(shù)名)
能夠看到test函數(shù)的相關(guān)說(shuō)明例如:help(printInfo)
-
test.__doc__
直接查看文檔說(shuō)明
三掷酗、函數(shù)的參數(shù)
函數(shù)參數(shù)的存在使得函數(shù)變得非常靈活,不但使得函數(shù)能夠處理復(fù)雜多變的參數(shù)窟哺,還能簡(jiǎn)化函數(shù)的調(diào)用泻轰。Python中的函數(shù)參數(shù)有如下幾種:位置參數(shù)、默認(rèn)參數(shù)且轨、可變參數(shù)浮声、關(guān)鍵字參數(shù)。
1. 位置參數(shù)
計(jì)算 x^2
的函數(shù):
def power(x):
return x * x
對(duì)于 power(x)
函數(shù)旋奢,參數(shù) x
就是一個(gè)位置參數(shù)泳挥,也叫做必選參數(shù)。當(dāng)我們調(diào)用 power
函數(shù)時(shí)至朗,必須傳入有且僅有的一個(gè)參數(shù) x
:
>>> power(5)
25
>>> power(15)
225
現(xiàn)在屉符,如果我們要計(jì)算 x^3
,或 x^4
锹引,就可以把 power(x)
修改為 power(x, n)
矗钟,用來(lái)計(jì)算 x^n
:
def power(x, n):
s = 1
while n > 0:
n = n - 1
s = s * x
return s
注意:這里的 n
是用來(lái)控制 while
循環(huán)次數(shù)的,幾次方就循環(huán)幾次嫌变。
對(duì)于這個(gè)修改后的 power(x, n)
函數(shù)吨艇,可以計(jì)算任意 n 次方:
>>> power(5, 2)
25
>>> power(5, 3)
125
修改后的 power(x, n)
函數(shù)有兩個(gè)參數(shù):x
和 n
,這兩個(gè)參數(shù)都是位置參數(shù)腾啥,調(diào)用函數(shù)時(shí)东涡,傳入的兩個(gè)值按照位置順序依次賦給參數(shù)x
和 n
冯吓。
2. 默認(rèn)參數(shù)
由于我們使用平方的次數(shù)遠(yuǎn)遠(yuǎn)大于其他次方,因此我們可以給 n
設(shè)置一個(gè)默認(rèn)值软啼,這樣我們?cè)趥鲄⒌臅r(shí)候就只用傳一個(gè) x
的值即可:
def power(x, n=2):
s = 1
while n > 0:
n = n - 1
s = s * x
return s
這樣桑谍,當(dāng)我們調(diào)用 power(5) 時(shí),就相當(dāng)于調(diào)用 power(5, 2) 祸挪,而如果要計(jì)算其他次方锣披,我們可以再傳入第二個(gè)參數(shù)即可,比如 power(5, 3)
:
>>> power(5)
25
>>> power(5, 2)
25
>>> power(5, 3)
125
由此可見(jiàn)贿条,默認(rèn)參數(shù)可以簡(jiǎn)化函數(shù)的調(diào)用雹仿。尤其是在實(shí)際應(yīng)用中,比如要錄入一個(gè)班級(jí)的學(xué)生信息整以,由于大多數(shù)學(xué)生都來(lái)自同一個(gè)地方胧辽,因此類(lèi)似于 city 這樣的參數(shù)就可以設(shè)置為默認(rèn)參數(shù),只在有特殊情況的時(shí)候傳入公黑,而把 name邑商、gender 等變化較大的參數(shù)設(shè)置為必選參數(shù),這樣在調(diào)用函數(shù)的時(shí)候大多數(shù)情況下就只用傳入必選參數(shù)即可凡蚜,大大降低了函數(shù)調(diào)用的難度人断。
不過(guò),設(shè)置默認(rèn)參數(shù)時(shí)朝蜘,有幾點(diǎn)要注意:
- 一是必選參數(shù)在前恶迈,默認(rèn)參數(shù)在后,否則 Python 的解釋器會(huì)報(bào)錯(cuò)谱醇;
- 二是當(dāng)函數(shù)有多個(gè)參數(shù)時(shí)暇仲,把經(jīng)常變化的參數(shù)放前面,不經(jīng)常變化的參數(shù)放后面副渴。不經(jīng)常變化的參數(shù)就可以作為默認(rèn)參數(shù)奈附。
- 三是當(dāng)有多個(gè)默認(rèn)參數(shù)時(shí),調(diào)用時(shí)既可以按順序提供默認(rèn)參數(shù)煮剧,也可以不按順序加上參數(shù)名來(lái)提供桅狠,比如一個(gè)
enroll(name, gender, age=18, city="Beijing")
的函數(shù),在調(diào)用時(shí)可以輸入enroll('Bob', 'M', 17, "Tianjin")
按順序傳入 age 和 city 兩個(gè)默認(rèn)參數(shù)的值轿秧,也可以輸入enroll('Adam', 'M', city='Tianjin', age=17)
不按順序按關(guān)鍵詞傳入。
擴(kuò)展
默認(rèn)參數(shù)有個(gè)最大的坑咨堤,演示如下:
先定義一個(gè)函數(shù)菇篡,傳入一個(gè) list,添加一個(gè) END 再返回:
def add_end(L=[]):
L.append('END')
return L
當(dāng)你正常調(diào)用時(shí)一喘,結(jié)果似乎不錯(cuò):
>>> add_end([1, 2, 3])
[1, 2, 3, 'END']
>>> add_end(['x', 'y', 'z'])
['x', 'y', 'z', 'END']
當(dāng)你使用默認(rèn)參數(shù)調(diào)用時(shí)驱还,一開(kāi)始結(jié)果也是對(duì)的:
>>> add_end()
['END']
但是嗜暴,再次調(diào)用 add_end()
時(shí),結(jié)果就不對(duì)了:
>>> add_end()
['END', 'END']
>>> add_end()
['END', 'END', 'END']
這就奇了怪了议蟆,默認(rèn)參數(shù)是 []
闷沥,但函數(shù)好像每次都“記住了”上次添加了 END
后的 list。
原因是這樣的:
Python函數(shù)在定義的時(shí)候咐容,默認(rèn)參數(shù) L 的值就被計(jì)算出來(lái)了舆逃,即 []
,因?yàn)槟J(rèn)參數(shù) L 也是一個(gè)變量戳粒,它指向?qū)ο?[]
路狮,每次調(diào)用該函數(shù),如果改變了 L 的內(nèi)容蔚约,則下次調(diào)用時(shí)奄妨,默認(rèn)參數(shù)的內(nèi)容就變了,不再是函數(shù)定義時(shí)的 []
了苹祟。
因此砸抛,定義默認(rèn)參數(shù)要牢記一點(diǎn):默認(rèn)參數(shù)必須指向不變對(duì)象!
要修改上面的例子树枫,我們可以用 None
這個(gè)不變對(duì)象來(lái)實(shí)現(xiàn):
def add_end(L=None):
if L is None:
L = []
L.append('END')
return L
現(xiàn)在直焙,無(wú)論調(diào)用多少次,都不會(huì)有問(wèn)題:
為什么要設(shè)計(jì) str
团赏、None
這樣的不變對(duì)象呢箕般?因?yàn)椴蛔儗?duì)象一旦創(chuàng)建,對(duì)象內(nèi)部的數(shù)據(jù)就不能修改舔清,這樣就減少了由于修改數(shù)據(jù)導(dǎo)致的錯(cuò)誤丝里。此外,由于對(duì)象不變体谒,多任務(wù)環(huán)境下同時(shí)讀取對(duì)象不需要加鎖杯聚,同時(shí)讀一點(diǎn)問(wèn)題都沒(méi)有。我們?cè)诰帉?xiě)程序時(shí)抒痒,如果可以設(shè)計(jì)一個(gè)不變對(duì)象幌绍,那就盡量設(shè)計(jì)成不變對(duì)象。
3. 可變參數(shù)(不定長(zhǎng)參數(shù))
在Python函數(shù)中故响,還可以定義可變參數(shù)傀广。顧名思義,可變參數(shù)就是傳入的參數(shù)個(gè)數(shù)是可變的彩届,可以是1個(gè)伪冰、2個(gè)到任意個(gè),還可以是0個(gè)樟蠕,因此可變參數(shù)還被叫做不定長(zhǎng)參數(shù)贮聂。
以一個(gè)數(shù)學(xué)題為例靠柑,給定一組數(shù)字 a,b吓懈,c……
歼冰,計(jì)算 a^2 + b^2 + c^2 +....
。
要定義出這個(gè)函數(shù)耻警,我們必須確定輸入的參數(shù)隔嫡。由于參數(shù)個(gè)數(shù)不確定,我們首先想到可以把 a榕栏,b畔勤,c……
作為一個(gè) list 或 tuple 傳進(jìn)來(lái),這樣扒磁,函數(shù)可以定義如下:
def calc(numbers):
sum = 0
for n in numbers:
sum = sum + n * n
return sum
但是調(diào)用的時(shí)候庆揪,需要先組裝出一個(gè) list 或 tuple :
>>> calc([1, 2, 3])
14
>>> calc((1, 3, 5, 7))
84
如果利用可變參數(shù),調(diào)用函數(shù)的方式可以簡(jiǎn)化成這樣:
>>> calc(1, 2, 3)
14
>>> calc(1, 3, 5, 7)
84
所以妨托,我們把函數(shù)的參數(shù)改為可變參數(shù):
def calc(*numbers):
sum = 0
for n in numbers:
sum = sum + n * n
return sum
定義可變參數(shù)和定義一個(gè) list 或 tuple 參數(shù)相比缸榛,僅僅在參數(shù)前面加了一個(gè) *
號(hào)。在函數(shù)內(nèi)部兰伤,參數(shù) numbers
接收到的是一個(gè)tuple内颗,因此,函數(shù)代碼完全不變敦腔。但是均澳,調(diào)用該函數(shù)時(shí),可以傳入任意個(gè)參數(shù)符衔,包括0個(gè)參數(shù):
>>> calc(1, 2)
5
>>> calc()
0
如果已經(jīng)有一個(gè) list 或者 tuple找前,要調(diào)用一個(gè)可變參數(shù)怎么辦?可以這樣做:
>>> nums = [1, 2, 3]
>>> calc(nums[0], nums[1], nums[2])
14
這種寫(xiě)法當(dāng)然是可行的判族,問(wèn)題是太繁瑣躺盛,所以Python允許你在list或tuple前面加一個(gè) *
號(hào),把 list 或 tuple 的元素變成可變參數(shù)傳進(jìn)去:
>>> nums = [1, 2, 3]
>>> calc(*nums)
14
*nums
表示把 nums
這個(gè) list 的所有元素作為可變參數(shù)傳進(jìn)去形帮。
4. 命名關(guān)鍵字參數(shù)
對(duì)于關(guān)鍵字參數(shù)槽惫,函數(shù)的調(diào)用者可以傳入任意不受限制的關(guān)鍵字參數(shù)。至于到底傳入了哪些辩撑,就需要在函數(shù)內(nèi)部通過(guò) kw
檢查界斜。
仍以 person()
函數(shù)為例,我們希望檢查是否有 city
和 job
參數(shù):
def person(name, age, **kw):
if 'city' in kw:
# 有city參數(shù)
pass
if 'job' in kw:
# 有job參數(shù)
pass
print('name:', name, 'age:', age, 'other:', kw)
但是調(diào)用者仍可以傳入不受限制的關(guān)鍵字參數(shù):
>>> person('Jack', 24, city='Beijing', addr='Chaoyang', zipcode=123456)
如果要限制關(guān)鍵字參數(shù)的名字合冀,就可以用命名關(guān)鍵字參數(shù)锄蹂,例如,只接收 city
和 job
作為關(guān)鍵字參數(shù)水慨。這種方式定義的函數(shù)如下:
def person(name, age, *, city, job):
print(name, age, city, job)
和關(guān)鍵字參數(shù) **kw
不同得糜,命名關(guān)鍵字參數(shù)需要一個(gè)特殊分隔符 *
,*
后面的參數(shù)被視為命名關(guān)鍵字參數(shù)晰洒。
調(diào)用方式如下:
>>> person('Jack', 24, city='Beijing', job='Engineer')
Jack 24 Beijing Engineer
如果函數(shù)定義中已經(jīng)有了一個(gè)可變參數(shù)朝抖,后面跟著的命名關(guān)鍵字參數(shù)就不再需要一個(gè)特殊分隔符 *
了:
def person(name, age, *args, city, job):
print(name, age, args, city, job)
命名關(guān)鍵字參數(shù)必須傳入?yún)?shù)名,這和位置參數(shù)不同谍珊。如果沒(méi)有傳入?yún)?shù)名治宣,調(diào)用將報(bào)錯(cuò):
>>> person('Jack', 24, 'Beijing', 'Engineer')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: person() takes 2 positional arguments but 4 were given
由于調(diào)用時(shí)缺少參數(shù)名 city
和 job
,Python 解釋器把這 4 個(gè)參數(shù)均視為位置參數(shù)砌滞,但 person()
函數(shù)僅接受 2 個(gè)位置參數(shù)侮邀。
命名關(guān)鍵字參數(shù)可以有缺省值,從而簡(jiǎn)化調(diào)用:
def person(name, age, *, city='Beijing', job):
print(name, age, city, job)
由于命名關(guān)鍵字參數(shù) city
具有默認(rèn)值贝润,調(diào)用時(shí)绊茧,可不傳入 city 參數(shù):
>>> person('Jack', 24, job='Engineer')
Jack 24 Beijing Engineer
使用命名關(guān)鍵字參數(shù)時(shí),要特別注意打掘,如果沒(méi)有可變參數(shù)华畏,就必須加一個(gè) *
作為特殊分隔符。如果缺少 *
尊蚁,Python解釋器將無(wú)法識(shí)別位置參數(shù)和命名關(guān)鍵字參數(shù):
def person(name, age, city, job):
# 缺少 *亡笑,city和job被視為位置參數(shù)
pass
5. 參數(shù)組合
在 Python 中定義函數(shù),可以用必選參數(shù)横朋、默認(rèn)參數(shù)仑乌、可變參數(shù)、關(guān)鍵字參數(shù)和命名關(guān)鍵字參數(shù)琴锭,這 5 種參數(shù)都可以組合使用晰甚。但是請(qǐng)注意,參數(shù)定義的順序必須是:必選參數(shù)祠够、默認(rèn)參數(shù)压汪、可變參數(shù)、命名關(guān)鍵字參數(shù)和關(guān)鍵字參數(shù)古瓤。
比如定義一個(gè)函數(shù)止剖,包含上述若干種參數(shù):
def f1(a, b, c=0, *args, **kw):
print('a =', a, 'b =', b, 'c =', c, 'args =', args, 'kw =', kw)
def f2(a, b, c=0, *, d, **kw):
print('a =', a, 'b =', b, 'c =', c, 'd =', d, 'kw =', kw)
在函數(shù)調(diào)用的時(shí)候,Python 解釋器自動(dòng)按照參數(shù)位置和參數(shù)名把對(duì)應(yīng)的參數(shù)傳進(jìn)去落君。
>>> f1(1, 2)
a = 1 b = 2 c = 0 args = () kw = {}
>>> f1(1, 2, c=3)
a = 1 b = 2 c = 3 args = () kw = {}
>>> f1(1, 2, 3, 'a', 'b')
a = 1 b = 2 c = 3 args = ('a', 'b') kw = {}
>>> f1(1, 2, 3, 'a', 'b', x=99)
a = 1 b = 2 c = 3 args = ('a', 'b') kw = {'x': 99}
>>> f2(1, 2, d=99, ext=None)
a = 1 b = 2 c = 0 d = 99 kw = {'ext': None}
最神奇的是通過(guò)一個(gè) tuple 和 dict 穿香,你也可以調(diào)用上述函數(shù):
>>> args = (1, 2, 3, 4)
>>> kw = {'d': 99, 'x': '#'}
>>> f1(*args, **kw)
a = 1 b = 2 c = 3 args = (4,) kw = {'d': 99, 'x': '#'}
>>> args = (1, 2, 3)
>>> kw = {'d': 88, 'x': '#'}
>>> f2(*args, **kw)
a = 1 b = 2 c = 3 d = 88 kw = {'x': '#'}
所以,對(duì)于任意函數(shù)绎速,都可以通過(guò)類(lèi)似 func(*args, **kw)
的形式調(diào)用它皮获,無(wú)論它的參數(shù)是如何定義的。
雖然可以組合多達(dá) 5 種參數(shù)纹冤,但不要同時(shí)使用太多的組合洒宝,否則函數(shù)接口的可理解性很差购公。
四、函數(shù)返回值
1. “返回值”介紹
所謂“返回值”雁歌,就是程序中函數(shù)完成一件事情后宏浩,最后給調(diào)用者的結(jié)果
2. 帶有返回值的函數(shù)
想要在函數(shù)中把結(jié)果返回給調(diào)用者,需要在函數(shù)中使用 return
靠瞎,如下示例:
def cal(a, b):
c = a+b
return c
或者
def cal(a, b):
return a+b
3. 保存函數(shù)的返回值
保存函數(shù)的返回值示例如下:
#定義函數(shù)
def cal(a, b):
return a+b
#調(diào)用函數(shù)比庄,順便保存函數(shù)的返回值
result = cal(100,98)
#result已經(jīng)保存了cal的返回值,所以接下來(lái)就可以使用了print(result)
結(jié)果:
198
4. 多個(gè)返回值
(1)多個(gè)return?
def cal_nums():
print("---1---")
return 1
# 函數(shù)中下面的代碼不會(huì)被執(zhí)行乏盐,因?yàn)閞eturn除了能夠?qū)?shù)據(jù)返回之外佳窑,還有一個(gè)隱藏的功能:結(jié)束函數(shù)
print("---2---")
return 2
print("---3---")
總結(jié)1: 一個(gè)函數(shù)中可以有多個(gè) return
語(yǔ)句,但是只要有一個(gè) return
語(yǔ)句被執(zhí)行到父能,那么這個(gè)函數(shù)就會(huì)結(jié)束了神凑,因此后面的return
沒(méi)有什么用處。
不過(guò)法竞,如果程序設(shè)計(jì)為如下耙厚,是可以的。因?yàn)椴煌膱?chǎng)景下執(zhí)行不同的 return:
def cal_nums(num):
print("---1---")
if num == 100:
print("---2---")
return num+1
# 函數(shù)中下面的代碼不會(huì)被執(zhí)行岔霸,因?yàn)閞eturn除了能夠?qū)?shù)據(jù)返回之外薛躬,還有一個(gè)隱藏的功能:結(jié)束函數(shù)
else:
print("---3---")
return num+2
print("---4---")
result1 = cal_nums(100)
print(result1) # 打印101
result2 = cal_nums(200)
print(result2) # 打印202
(2)一個(gè)函數(shù)返回多個(gè)數(shù)據(jù)的方式
def calculate(a, b):
shang = a//b
yushu = a%b
return shang, yushu #默認(rèn)是元組
result = calculate(5, 2)
print(result) # 輸出(2, 1)
總結(jié)2: return
后面可以是元組,列表呆细、字典等型宝,只要是能夠存儲(chǔ)多個(gè)數(shù)據(jù)的類(lèi)型,就可以一次性返回多個(gè)數(shù)據(jù):
def my_function():
# return [1, 2, 3]
# return (1, 2, 3)
return {"num1": 1, "num2": 2, "num3": 3}
如果 return 后面有多個(gè)數(shù)據(jù)絮爷,那么默認(rèn)是元組趴酣,也就是無(wú)論是 return 1, 2, 3
,還是 return (1, 2, 3)
坑夯,返回結(jié)果都是元組 (1, 2, 3)
:
>>> def my_func():
... return 1,2,3
...
>>> result = my_func()
>>> print(result, type(result))
(1, 2, 3) <class 'tuple'>
>>> def my_func():
... return (1,2,3)
...
>>> result = my_func()
>>> print(result, type(result))
(1, 2, 3) <class 'tuple'>
>>> exit()
五岖寞、遞歸函數(shù)
在函數(shù)內(nèi)部,可以調(diào)用其他函數(shù)柜蜈。如果一個(gè)函數(shù)在內(nèi)部調(diào)用自身本身仗谆,這個(gè)函數(shù)就是遞歸函數(shù)。
舉個(gè)例子淑履,我們來(lái)計(jì)算階乘 n! = 1 x 2 x 3 x ... x n
隶垮,用函數(shù) fact(n)
表示,可以看出:
fact(n) = n! = 1 x 2 x 3 x ... x (n-1) x n = (n-1)! x n = fact(n-1) x n
所以秘噪,fact(n)
可以表示為 n x fact(n-1)
狸吞,只有 n=1
時(shí)需要特殊處理。于是,fact(n)
用遞歸的方式寫(xiě)出來(lái)就是:
def fact(n):
if n==1:
return 1
return n * fact(n - 1)
上面就是一個(gè)遞歸函數(shù)蹋偏”愠猓可以試試:
>>> fact(1)
1
>>> fact(5)
120
>>> fact(100) 93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000
遞歸函數(shù)的優(yōu)點(diǎn)是定義簡(jiǎn)單,邏輯清晰威始。理論上椭住,所有的遞歸函數(shù)都可以寫(xiě)成循環(huán)的方式,但循環(huán)的邏輯不如遞歸清晰字逗。
使用遞歸函數(shù)需要注意防止棧溢出。在計(jì)算機(jī)中宅广,函數(shù)調(diào)用是通過(guò)棧(stack)這種數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)的葫掉,每當(dāng)進(jìn)入一個(gè)函數(shù)調(diào)用,棧就會(huì)加一層棧幀跟狱,每當(dāng)函數(shù)返回俭厚,棧就會(huì)減一層棧幀。由于棧的大小不是無(wú)限的驶臊,所以挪挤,遞歸調(diào)用的次數(shù)過(guò)多,會(huì)導(dǎo)致棧溢出关翎】该牛可以試試 fact(1000) :
>>> fact(1000)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in fact
...
File "<stdin>", line 4, in fact
RuntimeError: maximum recursion depth exceeded in comparison
解決遞歸調(diào)用棧溢出的方法是通過(guò)尾遞歸優(yōu)化,事實(shí)上尾遞歸和循環(huán)的效果是一樣的纵寝,所以论寨,把循環(huán)看成是一種特殊的尾遞歸函數(shù)也是可以的。
尾遞歸是指爽茴,在函數(shù)返回的時(shí)候葬凳,調(diào)用自身本身,并且室奏, return
語(yǔ)句不能包含表達(dá)式火焰。這樣,編譯器或者解釋器就可以把尾遞歸做優(yōu)化胧沫,使遞歸本身無(wú)論調(diào)用多少次昌简,都只占用一個(gè)棧幀,不會(huì)出現(xiàn)棧溢出的情況琳袄。
上面的 fact(n)
函數(shù)由于 return n * fact(n - 1)
引入了乘法表達(dá)式江场,所以就不是尾遞歸了。要改成尾遞歸方式窖逗,需要多一點(diǎn)代碼址否,主要是要把每一步的乘積傳入到遞歸函數(shù)中:
def fact(n):
return fact_iter(n, 1)
def fact_iter(num, product):
if num == 1:
return product
return fact_iter(num - 1, num * product)
可以看到, return fact_iter(num - 1, num * product)
僅返回遞歸函數(shù)本身佑附,num - 1
和 num * product
在函數(shù)調(diào)用前就會(huì)被計(jì)算樊诺,不影響函數(shù)調(diào)用。
fact(5)
對(duì)應(yīng)的 fact_iter(5, 1)
的調(diào)用如下:
===> fact_iter(5, 1)
===> fact_iter(4, 5)
===> fact_iter(3, 20)
===> fact_iter(2, 60)
===> fact_iter(1, 120)
===> 120
尾遞歸調(diào)用時(shí)音同,如果做了優(yōu)化词爬,棧不會(huì)增長(zhǎng),因此权均,無(wú)論多少次調(diào)用也不會(huì)導(dǎo)致棧溢出顿膨。
遺憾的是,大多數(shù)編程語(yǔ)言沒(méi)有針對(duì)尾遞歸做優(yōu)化叽赊,Python 解釋器也沒(méi)有做優(yōu)化恋沃,所以,即使把上面的 fact(n)
函數(shù)改成尾遞歸方式必指,也會(huì)導(dǎo)致棧溢出囊咏。
六、局部變量與全局變量
1. 局部變量
局部變量塔橡,就是在函數(shù)內(nèi)部定義的變量梅割,其作用范圍是這個(gè)函數(shù)內(nèi)部,即只能在這個(gè)函數(shù)中使用葛家,在函數(shù)的外部是不能使用的户辞。因?yàn)槠渥饔梅秶皇窃谧约旱暮瘮?shù)內(nèi)部,所以不同的函數(shù)可以定義相同名字的局部變量(打個(gè)比方惦银,把你咆课、我是當(dāng)做成函數(shù),把局部變量理解為每個(gè)人手里的手機(jī)扯俱,你可有個(gè)iPhone11书蚪,我當(dāng)然也可以有個(gè)iPhone11了,互不相關(guān))
局部變量的作用迅栅,是為了臨時(shí)保存數(shù)據(jù)需要在函數(shù)中定義變量來(lái)進(jìn)行存儲(chǔ)殊校。當(dāng)函數(shù)調(diào)用時(shí),局部變量被創(chuàng)建读存,當(dāng)函數(shù)調(diào)用完成后這個(gè)變量就不能夠使用了为流。
def show():
# 定義局部變量
sal = 15000
print("薪資:", sal)
show()
print(sal)
# 這里就會(huì)報(bào)錯(cuò),因?yàn)樵谌肿饔糜蛳氯貌荆淮嬖?`sal` 這個(gè)變量敬察。
2. 全局變量
如果一個(gè)變量,既能在一個(gè)函數(shù)中使用尔当,也能在其他的函數(shù)中使用莲祸,這樣的變量就是全局變量。
比如全家每個(gè)人各有一部手機(jī)只能自己使用,但是家里還裝了一個(gè)固定電話锐帜,全家人都可以使用田盈,那么每個(gè)人自己的手機(jī)就是局部變量,固定電話就是全局變量缴阎。
# 定義全局變量
money = 1200
def test1():
print(money)
# 雖然沒(méi)有定義變量money但是可是使用全局變量money
def test2():
print(money)
# 雖然沒(méi)有定義變量money但是可是使用全局變量money
# 調(diào)用函數(shù)
test1()
test2()
運(yùn)行結(jié)果:
1200
1200
總結(jié):在函數(shù)外邊定義的變量叫做全局變量允瞧。全局變量能夠在所有的函數(shù)中進(jìn)行訪問(wèn)。
3. 全局變量和局部變量的沖突問(wèn)題
# 定義全局變量
x = 100
def test1():
# 定義局部變量,與全局變量名字相同
x = 300
print('---test1---%d'%x)
#修改
x = 200
print('修改后的%d'%x)
def test2():
print('x = %d'%x)
test1()
test2()
結(jié)果:
---test1---300
修改后的200
x = 100
總結(jié):當(dāng)函數(shù)內(nèi)出現(xiàn)局部變量和全局變量相同名字時(shí)蛮拔,函數(shù)內(nèi)部中的此時(shí)理解為定義了一個(gè)局部變量述暂,而不是修改全局變量的值。
4. 修改全局變量
函數(shù)中使用全局變量時(shí)可否進(jìn)行修改呢建炫?
# 定義全局變量
x = 100
def test1():
# 定義全局變量贸典,使用 global 函數(shù)聲明變量 x 為全局變量
global x
print('修改之前:%d'%x)
#修改
x = 200
print('修改后的%d'%x)
def test2():
print('x = %d'%x)
test1()
test2()
結(jié)果:
修改之前:100
修改后的200
x = 200
七、捕獲異常
程序一旦出錯(cuò)踱卵,還要一級(jí)一級(jí)上報(bào),直到某個(gè)函數(shù)可以處理該錯(cuò)誤(比如据过,給用戶(hù)輸出一個(gè)錯(cuò)誤信息)惋砂。所以高級(jí)語(yǔ)言通常都內(nèi)置了一套 try...except...finally...
的錯(cuò)誤處理機(jī)制,Python也不例外绳锅。
讓我們用一個(gè)例子來(lái)看看 try
的機(jī)制:
try:
print('try...')
r = 10 / 0
print('result:', r)
except ZeroDivisionError as e:
print('except:', e)
finally:
print('finally...')
print('END')
當(dāng)我們認(rèn)為某些代碼可能會(huì)出錯(cuò)時(shí)西饵,就可以用 try
來(lái)運(yùn)行這段代碼,如果執(zhí)行出錯(cuò)鳞芙,則后續(xù)代碼不會(huì)繼續(xù)執(zhí)行眷柔,而是直接跳轉(zhuǎn)至錯(cuò)誤處理代碼,即 except
語(yǔ)句塊原朝,執(zhí)行完 except
后驯嘱,如果有 finally
語(yǔ)句塊,則執(zhí)行 finally
語(yǔ)句塊喳坠,至此鞠评, 執(zhí)行完畢。
上面的代碼在計(jì)算 10 / 0 時(shí)會(huì)產(chǎn)生一個(gè)除法運(yùn)算錯(cuò)誤:
try...
except: division by zero
finally...
END
從輸出可以看到壕鹉,當(dāng)錯(cuò)誤發(fā)生時(shí)剃幌,后續(xù)語(yǔ)句 print('result:', r)
不會(huì)被執(zhí)行,except
由于捕獲到 ZeroDivisionError
晾浴,因此被執(zhí)行负乡。最后,finally
語(yǔ)句被執(zhí)行脊凰。然后抖棘,程序繼續(xù)按照流程往下走。
如果把除數(shù) 0 改成 2 ,則執(zhí)行結(jié)果如下:
try...
result: 5
finally...
END
由于沒(méi)有錯(cuò)誤發(fā)生钉答,所以 except
語(yǔ)句塊不會(huì)被執(zhí)行础芍,但是 finally
如果有,則一定會(huì)被執(zhí)行(可以沒(méi)有 finally
語(yǔ)句)数尿。
當(dāng)然仑性,錯(cuò)誤應(yīng)該有很多種類(lèi),如果發(fā)生了不同類(lèi)型的錯(cuò)誤右蹦,應(yīng)該由不同的 except
語(yǔ)句塊處理:
try:
print('try...')
r = 10 / int('a')
print('result:', r)
except ValueError as e:
print('ValueError:', e)
except ZeroDivisionError as e:
print('ZeroDivisionError:', e)
finally:
print('finally...')
print('END')
int()
函數(shù)可能會(huì)拋出 ValueError
诊杆,所以我們用一個(gè) except
捕獲 ValueError
,用另一個(gè) except
捕獲 ZeroDivisionError
何陆。
此外邻薯,如果沒(méi)有錯(cuò)誤發(fā)生蚁孔,可以在 except
語(yǔ)句塊后面加一個(gè) else
,當(dāng)沒(méi)有錯(cuò)誤發(fā)生時(shí),會(huì)自動(dòng)執(zhí)行 else 語(yǔ)句:
try:
print('try...')
r = 10 / int('2')
print('result:', r)
except ValueError as e:
print('ValueError:', e)
except ZeroDivisionError as e:
print('ZeroDivisionError:', e)
else:
print('no error!')
finally:
print('finally...')
print('END')
Python的錯(cuò)誤其實(shí)也是 class 猖凛,所有的錯(cuò)誤類(lèi)型都繼承自BaseException
,所以在使用 except
時(shí)需要注意的是两曼,它不但能捕獲該類(lèi)型的錯(cuò)誤敦迄,還把其子類(lèi)也“一網(wǎng)打盡”。比如:
try:
foo()
except Exception as e:
print('Exception')
except TypeError as e:
print('TypeError')
finally:
print('finally...')
第二個(gè) except
永遠(yuǎn)也捕獲不到 TypeError
佳魔,因?yàn)?TypeError
是 Exception
的子類(lèi)曙聂,如果有,也被第一個(gè) except
給捕獲了鞠鲜。
Python所有的錯(cuò)誤都是從 BaseException
類(lèi)派生的宁脊,常見(jiàn)的錯(cuò)誤類(lèi)型和繼承關(guān)系可以看下面這個(gè)鏈接:
https://docs.python.org/3/library/exceptions.html#exception-hierarchy
使用 try...except
捕獲錯(cuò)誤還有一個(gè)巨大的好處,就是可以跨越多層調(diào)用贤姆,比如函數(shù) main()
調(diào)用 foo()
榆苞, foo()
調(diào)用 bar()
,結(jié)果 bar()
出錯(cuò)了霞捡,這時(shí)语稠,只要 main()
捕獲到了,就可以處理:
def foo(s):
return 10 / int(s)
def bar(s):
return foo(s) * 2
def main():
try:
bar('0')
except Exception as e:
print('Error:', e)
finally:
print('finally...')
main()
輸出結(jié)果:
Error: division by zero
finally...
也就是說(shuō)弄砍,不需要在每個(gè)可能出錯(cuò)的地方去捕獲錯(cuò)誤仙畦,只要在合適的層次去捕獲錯(cuò)誤就可以了。這樣一來(lái)音婶,就大大減少了寫(xiě)try...except...finally
的麻煩慨畸。
python所有的標(biāo)準(zhǔn)異常類(lèi):
異常名稱(chēng) | 描述 |
---|---|
BaseException | 所有異常的基類(lèi) |
SystemExit | 解釋器請(qǐng)求退出 |
KeyboardInterrupt | 用戶(hù)中斷執(zhí)行(通常是輸入^C) |
Exception | 常規(guī)錯(cuò)誤的基類(lèi) |
StopIteration | 迭代器沒(méi)有更多的值 |
GeneratorExit | 生成器(generator)發(fā)生異常來(lái)通知退出 |
SystemExit | Python 解釋器請(qǐng)求退出 |
StandardError | 所有的內(nèi)建標(biāo)準(zhǔn)異常的基類(lèi) |
ArithmeticError | 所有數(shù)值計(jì)算錯(cuò)誤的基類(lèi) |
FloatingPointError | 浮點(diǎn)計(jì)算錯(cuò)誤 |
Over?owError | 數(shù)值運(yùn)算超出最大限制 |
ZeroDivisionError | 除(或取模)零 (所有數(shù)據(jù)類(lèi)型) |
AssertionError | 斷言語(yǔ)句失敗 |
AttributeError | 對(duì)象沒(méi)有這個(gè)屬性 |
EOFError | 沒(méi)有內(nèi)建輸入,到達(dá)EOF 標(biāo)記 |
EnvironmentError | 操作系統(tǒng)錯(cuò)誤的基類(lèi) |
IOError | 輸入/輸出操作失敗 |
OSError | 操作系統(tǒng)錯(cuò)誤 |
WindowsError | 系統(tǒng)調(diào)用失敗 |
ImportError | 導(dǎo)入模塊/對(duì)象失敗 |
KeyboardInterrupt | 用戶(hù)中斷執(zhí)行(通常是輸入^C) |
LookupError | 無(wú)效數(shù)據(jù)查詢(xún)的基類(lèi) |
IndexError | 序列中沒(méi)有沒(méi)有此索引(index) |
KeyError | 映射中沒(méi)有這個(gè)鍵 |
MemoryError | 內(nèi)存溢出錯(cuò)誤(對(duì)于Python 解釋器不是致命的) |
NameError | 未聲明/初始化對(duì)象 (沒(méi)有屬性) |
UnboundLocalError | 訪問(wèn)未初始化的本地變量 |
ReferenceError | 弱引用(Weak reference)試圖訪問(wèn)已經(jīng)垃圾回收了的對(duì)象 |
RuntimeError | 一般的運(yùn)行時(shí)錯(cuò)誤 |
NotImplementedError | 尚未實(shí)現(xiàn)的方法 |
SyntaxError | Python 語(yǔ)法錯(cuò)誤 |
IndentationError | 縮進(jìn)錯(cuò)誤 |
TabError | Tab 和空格混用 |
SystemError | 一般的解釋器系統(tǒng)錯(cuò)誤 |
TypeError | 對(duì)類(lèi)型無(wú)效的操作 |
ValueError | 傳入無(wú)效的參數(shù) |
UnicodeError | Unicode 相關(guān)的錯(cuò)誤 |
UnicodeDecodeError | Unicode 解碼時(shí)的錯(cuò)誤 |
UnicodeEncodeError | Unicode 編碼時(shí)錯(cuò)誤 |
UnicodeTranslateError | Unicode 轉(zhuǎn)換時(shí)錯(cuò)誤 |
Warning | 警告的基類(lèi) |
DeprecationWarning | 關(guān)于被棄用的特征的警告 |
FutureWarning | 關(guān)于構(gòu)造將來(lái)語(yǔ)義會(huì)有改變的警告 |
Over?owWarning | 舊的關(guān)于自動(dòng)提升為長(zhǎng)整型(long)的警告 |
PendingDeprecationWarning | 關(guān)于特性將會(huì)被廢棄的警告 |
RuntimeWarning | 可疑的運(yùn)行時(shí)行為(runtime behavior)的警告 |
SyntaxWarning | 可疑的語(yǔ)法的警告 |
UserWarning | 用戶(hù)代碼生成的警告 |