《Python從小白到大牛》第10章 函數式編程
Python中的函數很靈活,它可以在模塊中,但類之外定義,即函數,作用域是當前模塊;也可以在別的函數中定義,即嵌套函數;還可以在類中定義,即方法。
定義函數
在前面的學習過程中也用到了一些函數,如果len()、min()和max(),這些函數都由Python官方提供的,稱為內置函數(Built-in
Functions, 縮寫BIF)。
註意
Python作為解釋性語言函數必須先定義後調用,也就是定義函數必須在調用函數之前,否則會有錯誤發生。
本節介紹自定義函數,自定義函數的語法格式如下:
def 函數名(參數列表) :
函數體
return 返回值
在Python中定義函數時,關鍵字是def,函數名需要符合標識符命名規範;多個參數列表之間可以用逗號(,)分隔,當然函數也可以沒有參數。如果函數有返回數據,就需要在函數體最後使用return語句將數據返回;如果沒有返回數據,則函數體中可以使用return
None或省略return語句。
函數定義示例代碼如下:
# coding=utf-8 # 代碼文件:chapter10/ch10.1.py def rectangle_area(width, height): ① area = width * height return area ② r_area = rectangle_area(320.0, 480.0) ③ print("320x480的長方形的面積:{0:.2f}".format(r_area))
上述代碼第①行是定義計算長方形面積的函數rectangle_area,它有兩個參數,分別是長方形的寬和高,width和height是參數名。代碼第②行代碼是通過return返回函數計算結果。代碼第③行是調用rectangle_area函數。
函數參數
Python中的函數參數很靈活,具體體現在傳遞參數有多種形式上。本節介紹幾種不同形式參數和調用方式。
使用關鍵字參數調用函數
為了提高函數調用的可讀性,在函數調用時可以使用關鍵字參數調用。采用關鍵字參數調用函數,在函數定義時不需要做額外的工作。
示例代碼如下:
# coding=utf-8 # 代碼文件:chapter10/ch10.2.1.py def print_area(width, height): area = width * height print("{0} x {1} 長方形的面積:{2}".format(width, height, area)) print_area(320.0, 480.0) # 沒有采用關鍵字參數函數調用 ① print_area(width=320.0, height=480.0) # 采用關鍵字參數函數調用② print_area(320.0, height=480.0) # 采用關鍵字參數函數調用 ③ # print_area(width=320.0, height) # 發生錯誤 ④ print_area(height=480.0, width=320.0) # 采用關鍵字參數函數調用 ⑤
print_area函數有兩個參數,在調用時沒有采用關鍵字參數函數調用,見代碼第①行,也可以使用關鍵字參數調用函數,見代碼第②行、第③行和第⑤行,其中width和height是參數名。從上述代碼比較可見采用關鍵字參數調用函數,調用者能夠清晰地看出傳遞參數的含義,關鍵字參數對於有多參數函數調用非常有用。另外,采用關鍵字參數函數調用時,參數順序可以與函數定義時參數順序不同。
註意
在調用函數時,一旦其中一個參數采用了關鍵字參數形式傳遞,那麽其後的所有參數都必須采用關鍵字參數形式傳遞。代碼第④行的函數調用中第一個參數width采用了關鍵字參數形式,而它後面的參數沒有采用關鍵字參數形式,因此會有錯誤發生。
參數默認值
在定義函數的時候可以為參數設置一個默認值,當調用函數的時候可以忽略該參數。來看下面的一個示例:
# coding=utf-8
# 代碼文件:chapter9/ch10.2.2.py
def make_coffee(name="卡布奇諾"):
return "制作一杯{0}咖啡。".format(name)
上述代碼定義了makeCoffee函數,可以幫助我做一杯香濃的咖啡。由於我喜歡喝卡布奇諾,就把它設置為默認值。在參數列表中,默認值可以跟在參數類型的後面,通過等號提供給參數。
在調用的時候,如果調用者沒有傳遞參數,則使用默認值。調用代碼如下:
coffee1 = make_coffee("拿鐵") ①
coffee2 = make_coffee() ②
print(coffee1) # 制作一杯拿鐵咖啡。
print(coffee2) # 制作一杯卡布奇諾咖啡。
其中第①行代碼是傳遞"拿鐵"參數,沒有使用默認值。第②行代碼沒有傳遞參數,因此使用默認值。
提示
在其他語言中make_coffee函數可以采用重載實現多個版本。Python不支持函數重載,而是使用參數默認值的方式提供類似函數重載的功能。因為參數默認值只需要定義一個函數就可以了,而重載則需要定義多個函數,這會增加代碼量。
可變參數
Python中函數的參數個數可以變化,它可以接受不確定數量的參數,這種參數稱為可變參數。Python中可變參數有兩種,在參數前加*或**形式,*可變參數在函數中被組裝成為一個元組,**可變參數在函數中被組裝成為一個字典。
- *可變參數
下面看一個示例:
def sum(*numbers, multiple=1):
total = 0.0
for number in numbers:
total += number
return total * multiple
上述代碼定義了一個sum()函數,用來計算傳遞給它的所有參數之和。*numbers是可變參數。在函數體中參數numbers被組裝成為一個元組,可以使用for循環遍歷numbers元組,計算他們的總和,然後返回給調用者。
下面是三次調用sum()函數的代碼:
print(sum(100.0, 20.0, 30.0)) # 輸出150.0
print(sum(30.0, 80.0)) # 輸出110.0
print(sum(30.0, 80.0, multiple=2)) # 輸出220.0 ①
double_tuple = (50.0, 60.0, 0.0) # 元組或列表 ②
print(sum(30.0, 80.0, *double_tuple)) # 輸出220.0 ③
可以看到,每次所傳遞參數的個數是不同的,前兩次調用時都省略了multiple參數,第三次調用時傳遞了multiple參數,此時multiple應該使用關鍵字參數傳遞,否則有錯誤發生。
如果已經有一個元組變量(見代碼第②行),能否傳遞給可變參數呢?這需要使用對元組進行拆包,見代碼第③行在元組double_tuple前面加上單星號(*),單星號在這裏表示將double_tuple拆包為50.0,
60.0, 0.0形式。另外,double_tuple也可以是列表對象。
註意
*可變參數不是最後一個參數時,後面的參數需要采用關鍵字參數形式傳遞。代碼第①行30.0,
80.0是可變參數,後面multiple參數需要關鍵字參數形式傳遞。
- **可變參數
下面看一個示例:
def show_info(sep=‘:‘, **info):
print(‘-----info------‘)
for key, value in info.items():
print(‘{0} {2} {1}‘.format(key, value, sep))
上述代碼定義了一個show_info()函數,用來輸出一些信息,其中參數sep信息分隔符號,默認值是冒號(:)。**info是可變參數,在函數體中參數info被組裝成為一個字典。
註意 **可變參數必須在正規參數之後,如果本例函數定義改為show_info(**info,sep=‘:‘)形式,會發生錯誤。
下面是三次調用show_info()函數的代碼:
show_info(‘->‘, name=‘Tony‘, age=18, sex=True) ①
show_info(sutdent_name=‘Tony‘, sutdent_no=‘1000‘, sep=‘-‘) ②
stu_dict = {‘name‘: ‘Tony‘, ‘age‘: 18} # 創建字典對象
show_info(**stu_dict, sex=True, sep=‘=‘) # 傳遞字典stu_dict ③
上述代碼第①行是調用函數show_info(),第一個參數‘-\>‘是傳遞給sep,其後的參數name=‘Tony‘,
age=18,
sex=True是傳遞給info,這種參數形式事實上就是關鍵字參數,註意鍵不要用引號包裹起來。
代碼第②行是調用函數show_info(),sep也采用關鍵字參數傳遞,這種方式sep參數可以放置在參數列表的任何位置,其中的關鍵字參數被收集到info字典中。
代碼第③行是調用函數show_info(),其中字典對象stu_dict,傳遞時stu_dict前面加上雙星號(**),雙星號在這裏表示將stu_dict拆包為key=value對的形式。
函數返回值
Python函數的返回值也是比較靈活的,主要有三種形式:無返回值、單一返回值和多返回值。前面使用的函數基本都單一返回值,本節重點介紹無返回值和多返回值兩種形式。
無返回值函數
有的函數只是為了處理某個過程,此時可以將函數設計為無返回值的。所謂無返回值,事實上是返回None,None表示沒有實際意義的數據。
無返回值函數示例代碼如下:
# coding=utf-8
# 代碼文件:chapter9/ch10.3.1.py
def show_info(sep=‘:‘, **info): ①
"""定義**可變參數函數"""
print(‘-----info------‘)
for key, value in info.items():
print(‘{0} {2} {1}‘.format(key, value, sep))
return # return None 或省略 ②
result = show_info(‘->‘, name=‘Tony‘, age=18, sex=True)
print(result) # 輸出 None
def sum(*numbers, multiple=1): ③
"""定義*可變參數函數"""
if len(numbers) == 0:
return # return None 或省略 ④
total = 0.0
for number in numbers:
total += number
return total * multiple
print(sum(30.0, 80.0)) # 輸出110.0
print(sum(multiple=2)) # 輸出 None ⑥
上述代碼定義了兩個函數,這個兩個函數事實上是在10.3.2節示例基礎上的重構。其中代碼第①行的show_info()只是輸出一些信息,不需要返回數據,因此可以省略return語句。如果一定要使用return語句,見代碼第②行在函數結束前使用return或return
None。
對於本例中的show_info()函數強加return語句顯然是多此一舉,但是有時使用return或return
None是必要的。代碼第③行定義了sum()函數,如果numbers中數據是空的,後面的求和計算也就沒有意義了,可以在函數的開始判斷numbers中算法有數據,如果沒有數據則使用return或return
None跳出函數,見代碼第④行。
多返回值函數
有時需要函數返回多個值,實現返回多個值的實現方式有很多,簡單實現是使用元組返回多個值,因為元組作為數據結構可以容納多個數據,另外元組是不可變的,使用起來比較安全。
下面來看一個示例:
# coding=utf-8
# 代碼文件:chapter9/ch10.3.2.py
def position(dt, speed): ①
posx = speed[0] * dt ②
posy = speed[1] * dt ③
return (posx, posy) ④
move = position(60.0, (10, -5)) ⑤
print("物體位移:({0}, {1})".format(move[0], move[1])) ⑥
這個示例是計算物體在指定時間和速度時的位移。第①行代碼是定義position函數,其中dt參數是時間,speed參數是元組類型,speed第一個元素X軸上的速度,speed第二個元素Y軸上的速度。position函數的返回值也是元組類型。
函數體中的第②行代碼是計算X方向的位移,第③行代碼是計算Y方向的位移。最後,第④行代碼將計算後的數據返回,(posx,
posy)是元組類型實例。
第⑤行代碼調用函數,傳遞的時間是60.0秒,速度是(10,
-5)。第⑥行代碼打印輸出結果,結果如下:
物體位移:(600.0, -300.0)
函數變量作用域
變量可以在模塊中創建,作用域是整個模塊,稱為全局變量。變量也可以在函數中創建,默認情況下作用域是整個函數,稱為局部變量。
示例代碼如下:
# coding=utf-8
# 代碼文件:chapter9/ch10.4.py
# 創建全局變量x
x = 20 ①
def print_value():
print("函數中x = {0}".format(x)) ②
print_value()
print("全局變量x = {0}".format(x))
輸出結果:
函數中x = 20
全局變量x = 20
上述代碼第①行是創建全局變量x,全局變量作用域是整個模塊,所以在print_value()函數中也可以訪問變量x,見代碼第②行。
修改上述示例代碼如下:
# 創建全局變量x
x = 20
def print_value():
# 創建局部變量x
x = 10 ①
print("函數中x = {0}".format(x))
print_value()
print("全局變量x = {0}".format(x))
輸出結果:
函數中x = 10
全局變量x = 20
上述代碼的是print_value()函數中添加x =
10語句,見代碼第①行,這會函數中創建x變量,函數中的x變量與全局變量x命名相同,在函數作用域內會屏蔽全局x變量。
函數中創建的變量默認作用域是當前函數,這可以防止程序員少犯錯誤,因為函數中創建的變量,如果作用域是整個模塊,那麽在其他函數中也可以訪問,Python無法從語言層面定義常量,所以在其他函數中由於誤操作修改了變量,這樣一來很容易導致程序出現錯誤。即便筆者不贊同,但Python提供這種可能,通過在函數中將變量聲明為global的,可以把變量的作用域變成全局的。
修改上述示例代碼如下:
# 創建全局變量x
x = 20
def print_value():
global x ①
x = 10 ②
print("函數中x = {0}".format(x))
print_value()
print("全局變量x = {0}".format(x))
輸出結果:
函數中x = 10
全局變量x = 10
代碼第①行是在函數中聲明x變量作用域為全局變量,所以代碼第②行修改x值,就是修改全局變量x的數值。
生成器
在一個函數中使用return關鍵字返回數據,但是有時候會使用yield?關鍵字返回數據。yield?關鍵字的函數返回的是一個生成器(generator)對象,生成器對象是一種可叠代對象。
如果想計算平方數列,通常的實現代碼如下:
def square(num): ①
n_list = []
for i in range(1, num + 1):
n_list.append(i * i) ②
return n_list ③
for i in square(5): ④
print(i, end=‘ ‘)
返回結果是:
1 4 9 16 25
首先定義一個函數,見代碼第①行。代碼第②行通過循環計算一個數的平方,並將結果保存到一個列表對象n_list中。最後返回列表對象,見代碼第③行。代碼第④行是遍歷返回的列表對象。
在Python中還可以有更加好解決方案,實現代碼如下:
def square(num):
for i in range(1, num + 1):
yield i * i ①
for i in square(5): ②
print(i, end=‘ ‘)
返回結果是:
1 4 9 16 25
代碼第①行使用了yield關鍵字返回平方數,不再需要return關鍵字了。代碼第②行調用函數square()返回的是生成器對象。生成器對象是一種可叠代對象,可叠代對象通過next()方法獲得元素,代碼第②行的for循環能夠遍歷可叠代對象,就是隱式地調用生成器的next()方法獲得元素的。
顯式地調用生成器的next()方法,在Python Shell中運行示例代碼如下:
>>> def square(num):
for i in range(1, num + 1):
yield i * i
>>> n_seq = square(5)
<generator object square at 0x000001C8F123CE60>
>>> n_seq.__next__() ①
1
>>> n_seq.__next__()
4
>>> n_seq.__next__()
9
>>> n_seq.__next__()
16
>>> n_seq.__next__()
25
>>> n_seq.__next__() ②
Traceback (most recent call last):
File "<pyshell#33>", line 1, in <module>
n_seq.__next__()
StopIteration
>>>
上述代碼第①行\~第②行調用了6次next()方法,但第6次調用則會拋出StopIteration異常,這是因為已經沒有元素可叠代了。
生成器函數是通過yield返回數據,與return不同的是:return語句一次返回所有數據,函數調用結束;而yield語句只返回一個元素數據,函數調用不會結束,只是暫停,直到next()方法被調用,程序繼續執行yield語句之後的語句代碼。這個過程如圖10-1所示。
註意
生成器特別適合用於遍歷一些大序列對象,它無須將對象的所有元素都載入內存後才開始進行操作。而是僅在叠代至某個元素時才會將該元素載入內存。
嵌套函數
在本節之前定義的函數都是全局函數,並將他們定義在全局作用域中。函數還可定義在另外的函數體中,稱作“嵌套函數”。
示例代碼:
# coding=utf-8
# 代碼文件:chapter10/ch10.6.py
def calculate(n1, n2, opr):
multiple = 2
# 定義相加函數
def add(a, b): ①
return (a + b) * multiple
# 定義相減函數
def sub(a, b): ②
return (a - b) * multiple
if opr == ‘+‘:
return add(n1, n2)
else:
return sub(n1, n2)
print(calculate(10, 5, ‘+‘)) # 輸出結果是30
# add(10, 5) 發生錯誤 ③
# sub(10, 5) 發生錯誤 ④
上述代碼中定義了兩個嵌套函數:add()和sub(),見代碼第①行和第②行。嵌套函數可以訪問所在外部函數calculate()中的變量multiple,而外部函數不能訪問嵌套函數局部變量。另外,嵌套函數的作用域在外部函數體內,因此在外部函數體之外直接訪問嵌套函數會發生錯誤,見代碼第③行和第④行。
函數式編程基礎
函數式編程(functional
programming)與面向對象編程一樣都一種編程範式,函數式編程,也稱為面向函數的編程。
Python並不是徹底的函數式編程語言,但是還是提供了一些函數式編程必備的技術,主要有:函數類型和Lambda表達式,他們是實現函數式編程的基礎。
函數類型
Python提供了一種函數類型function,任何一個函數都有函數類型,但是函數調用時,就創建了函數類型實例,即函數對象。函數類型實例與其他類型實例一樣,在使用場景上沒有區別:它可以賦值給一個變量;也可以作為參數傳遞給一個函數;還可以作為函數返回值使用。
為了理解函數類型,先重構10.6節中嵌套函數的示例,示例代碼如下:
# coding=utf-8
# 代碼文件:chapter10/ch10.7.1.py
def calculate_fun(opr): ①
# 定義相加函數
def add(a, b):
return a + b
# 定義相減函數
def sub(a, b):
return a - b
if opr == ‘+‘:
return add ②
else:
return sub ③
f1 = calculate_fun(‘+‘) ④
f2 = calculate_fun(‘-‘) ⑤
print(type(f1))
print("10 + 5 = {0}".format(f1(10, 5))) ⑥
print("10 - 5 = {0}".format(f2(10, 5))) ⑦
輸出結果:
<class ‘function‘>
10 + 5 = 30
10 - 5 = 10
上述代碼第①行重構了calculate_fun()函數的定義,現在只接收一個參數opr。代碼第②行是在opr
‘+‘為True時返回add函數名,否則返回sub函數名,見代碼第③行。這裏的函數名本質上函數對象。calculate_fun()函數與10.5節示例calculate()函數不同之處在於,calculate_fun()函數返回的是函數對象,calculate()函數返回的是整數(相加或相減計算之後的結果)。所以代碼第④行的f1是add()函數對象,代碼第⑤行的f2是sub()函數對象。
函數對象是可以與函數一樣進行調用的。事實上在第⑥行之前沒有真正調用add()函數進行相加計算,f1(10,
5)表達式才真的調用了add()函數。第⑦行的f2(10, 5)表達式是調用sub()函數。
Lambda表達式
理解了函數類型和函數對象,學習Lambda表達式就簡單了。Lambda表達式本質上一種匿名函數,匿名函數也是函數有函數類型,也可以創建函數對象。
定義Lambda表達式語法如下:
lambda 參數列表 : Lambda體
lambda是關鍵字聲明這是一個Lambda表達式,“參數列表”與函數的參數列表是一樣的,但不需要小括號包裹起來,冒號後面是“Lambda體”,Lambda表達式主要的代碼在此處編寫,類似於函數體。
註意
Lambda體部分不能是一個代碼塊,不能包含多條語句,只有一條語句,語句會計算一個結果返回給Lambda表達式,但是與函數不同是,不需要使用return語句返回。與其他語言中的Lambda表達式相比,Python中提供Lambda表達式只能處理一些簡單的計算。
重構10.7.1節示例,代碼如下:
# coding=utf-8
# 代碼文件:chapter10/ch10.7.2.py
def calculate_fun(opr):
if opr == ‘+‘:
return lambda a, b: (a + b) ①
else:
return lambda a, b: (a - b) ②
f1 = calculate_fun(‘+‘)
f2 = calculate_fun(‘-‘)
print(type(f1))
print("10 + 5 = {0}".format(f1(10, 5)))
print("10 - 5 = {0}".format(f2(10, 5)))
輸出結果:
<class ‘function‘>
10 + 5 = 30
10 - 5 = 10
上述代碼第①行替代了add()函數,第②行替代了sub()函數,代碼變的非常的簡單。
三大基礎函數
函數式編程本質是通過函數處理數據,過濾、映射和聚合是處理數據的三大基本操作。針對但其中三大基本操作Python提供了三個基礎的函數:filter()、map()和reduce()。
- filter()
過濾操作使用filter()函數,它可以對可叠代對象的元素進行過濾,filter()函數語法如下:
filter(function, iterable)
其中參數function是一個函數,參數iterable是可叠代對象。filter()函數調用時iterable會被遍歷,它的元素逐一傳入function函數,function函數返回布爾值,在function函數中編寫過濾條件,如果為True的元素被保留,如果為False的元素被過濾掉。
下面通過一個示例介紹一下filter()函數使用,示例代碼如下:
users = [‘Tony‘, ‘Tom‘, ‘Ben‘, ‘Alex‘]
users_filter = filter(lambda u: u.startswith(‘T‘), users) ①
print(list(users_filter))
輸出結果:
[‘Tony‘, ‘Tom‘]
代碼第①行調用filter()函數過濾users列表,過濾條件是T開通的元素,lambda u:
u.startswith(‘T‘)是一個Lambda表達式,它提供了過濾條件。filter()函數還不是一個列表,需要使用list()函數轉換過濾之後的數據為列表。
再看一個示例:
number_list = range(1, 11)
nmber_filter = filter(lambda it: it % 2 == 0, number_list)
print(list(nmber_filter))
該示例實現了獲取1\~10數字中的偶數,輸出結果如下:
[2, 4, 6, 8, 10]
- map()
映射操作使用map()函數,它可以對可叠代對象的元素進行變換,map()函數語法如下:
map(function, iterable)
其中參數function是一個函數,參數iterable是可叠代對象。map()函數調用時iterable會被遍歷,它的元素逐一傳入function函數,在function函數中對元素進行變換。
下面通過一個示例介紹一下map()函數使用,示例代碼如下:
users = [‘Tony‘, ‘Tom‘, ‘Ben‘, ‘Alex‘]
users_map = map(lambda u: u.lower(), users) ①
print(list(users_map))
輸出結果:
[‘tony‘, ‘tom‘, ‘ben‘, ‘alex‘]
上述代碼第①行調用map()函數將users列表元素轉換為小寫字母,變換使用Lambda表達式lambda
u:
u.lower()。map()函數返回的還不是一個列表,需要使用list()函數將過濾之後的數據轉換為列表。
函數式編程時數據可以從一個函數“流”入另外一個函數,但是遺憾的是Python並不支持“鏈式”API。例如想獲取users列表中“T”開通的名字,再將其轉換為小寫字母,這樣的需求需要使用filter()函數進行過濾,再使用map()函數進行映射變換。實現代碼如下:
users = [‘Tony‘, ‘Tom‘, ‘Ben‘, ‘Alex‘]
users_filter = filter(lambda u: u.startswith(‘T‘), users)
# users_map = map(lambda u: u.lower(), users_filter) ①
users_map = map(lambda u: u.lower(), filter(lambda u: u.startswith(‘T‘), users)) ②
print(list(users_map))
上述代碼第①行和第②行實現相同功能。
- reduce()
聚合操作會將多個數據聚合起來輸出單個數據,聚合操作中最基礎的是歸納函數reduce(),reduce()函數會將多個數據按照指定的算法積累疊加起來,最後輸出一個數據。
reduce()函數語法如下:
reduce(function, iterable[, initializer])
參數function是聚合操作函數,該函數有兩個參數,參數iterable是可叠代對象;參數initializer初始值。
下面通過一個示例介紹一下reduce()函數使用。下面示例實現了對一個數列求和運算,代碼如下:
from functools import reduce ①
a = (1, 2, 3, 4)
a_reduce = reduce(lambda acc, i: acc + i, a) # 10 ②
# a_reduce = reduce(lambda acc, i: acc + i, a, 2) # 12 ③
print(a_reduce)
reduce()函數是在functools模塊中定義的,所以要使用reduce()函數需要導入functools模塊,見代碼第①行。代碼第②行是調用reduce()函數,其中lambda
acc, i: acc +
i是進行聚合操作的Lambda表達式,該Lambda表達式有兩個參數,其中acc參數是上次累積計算結果,i當前元素,acc
+
i表達式是進行累加。reduce()函數最後的計算結果是一個數值,直接可以使用通過reduce()函數返回。代碼第行是傳入了初始值2,則計算的結果是12。
本章小結
通過對本章內容的學習,讀者可以熟悉在Python中如何定義函數、函數參數和函數返回值,了解函數變量作用域和嵌套函數。最後還介紹了Python中函數式編程基礎。
配套視頻
http://edu.51cto.com/topic/1507.html
配套源代碼
http://www.zhijieketang.com/group/8
作者微博:@tony_關東升郵箱:[email protected]
br/>郵箱:[email protected]
Python讀者服務QQ群:628808216
《Python從小白到大牛》第10章 函數式編程