數(shù)據(jù)分析課程筆記 - 03 - Python基礎(chǔ)知識(shí)(三):函數(shù)

一、函數(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ù):xn,這兩個(gè)參數(shù)都是位置參數(shù)腾啥,調(diào)用函數(shù)時(shí)东涡,傳入的兩個(gè)值按照位置順序依次賦給參數(shù)xn 冯吓。

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ù)為例,我們希望檢查是否有 cityjob 參數(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ù)锄蹂,例如,只接收 cityjob 作為關(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ù)名 cityjob ,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 - 1num * 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)?TypeErrorException 的子類(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ù)代碼生成的警告
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末衣式,一起剝皮案震驚了整個(gè)濱河市寸士,隨后出現(xiàn)的幾起案子檐什,更是在濱河造成了極大的恐慌,老刑警劉巖弱卡,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件乃正,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡婶博,警方通過(guò)查閱死者的電腦和手機(jī)瓮具,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)凡人,“玉大人名党,你說(shuō)我怎么就攤上這事∧又幔” “怎么了传睹?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)岸晦。 經(jīng)常有香客問(wèn)我欧啤,道長(zhǎng),這世上最難降的妖魔是什么启上? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任堂油,我火速辦了婚禮,結(jié)果婚禮上碧绞,老公的妹妹穿的比我還像新娘。我一直安慰自己吱窝,他們只是感情好讥邻,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著院峡,像睡著了一般兴使。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上照激,一...
    開(kāi)封第一講書(shū)人閱讀 51,370評(píng)論 1 302
  • 那天发魄,我揣著相機(jī)與錄音,去河邊找鬼俩垃。 笑死励幼,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的口柳。 我是一名探鬼主播苹粟,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼跃闹!你這毒婦竟也來(lái)了嵌削?” 一聲冷哼從身側(cè)響起毛好,我...
    開(kāi)封第一講書(shū)人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎苛秕,沒(méi)想到半個(gè)月后肌访,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡艇劫,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年吼驶,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片港准。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡旨剥,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出浅缸,到底是詐尸還是另有隱情轨帜,我是刑警寧澤,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布衩椒,位于F島的核電站蚌父,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏毛萌。R本人自食惡果不足惜苟弛,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望阁将。 院中可真熱鬧膏秫,春花似錦、人聲如沸做盅。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)吹榴。三九已至亭敢,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間图筹,已是汗流浹背帅刀。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留远剩,地道東北人扣溺。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像瓜晤,于是被迫代替她去往敵國(guó)和親娇妓。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容