Python進階 - 函數式編程(1)

函數式編程作爲一種數學模型實際上已經發展接近百年了。由於函數式編程的概念比較龐大,因此我分爲幾篇文章來介紹,有一些概念在Python中可能是缺省的,但我仍傾向於把概念解釋清楚,因爲函數式編程本身是一種思想;本篇文章主要介紹函數式編程的基本概念,以及Python中的一些工具。

一、什麼是函數式編程

函數式編程是一種編程的風格或方法。一般編程的方法分爲面向過程編程和麪向對象編程,函數式編程是在面向對象編程的基礎上發展出的一種編程方法。在函數式編程中,我們儘可能的將功能來用函數實現,函數和變量的地位是相等的,也可以作爲一種變量來進行存儲。

二、Python中的函數式編程

Python在自帶的函數式編程語法中主要有以下幾個工具:map函數、reduce函數、filter函數

1. map函數

map函數需要至少兩個參數,第一個參數是一個函數的對象,第二個參數是一個可迭代對象(iterable,指列表、元組、字典等結構),之後可以繼續傳遞可迭代的對象,數量由第一個函數的參數決定。第一個參數的函數會將施加在後面每一個可迭代對象的元素上。正因如此,我們在定義map函數需要用到的函數時,需要注意,函數的參數是可迭代對象中的元素,而並非可迭代對象本身。

需要注意的是,我們如何傳遞函數的對象呢?原來,在Python中,函數的名字即爲函數對象的名字,當我們在函數對象後面加上圓括號,纔是調用該函數。

下面我們舉幾個例子來幫助理解:

  1. 計算列表中的數字的平方
def square(x):
	return x ** 2

nums = [1,2,3,4,5]
square_nums = map(square,nums)
print(square_nums)

# 結果
[1,4,9,16,25]
  1. 將兩個長度相等的列表相加
def add_list(x,y):
	return x + y

nums1 = [1,2,3,4,5]
nums2 = [1,1,1,1,1]
new_nums = map(add_list,nums1,nums2)
print(new_nums)

# 結果
[2,3,4,5,6]

注意,map函數傳入的可迭代對象數量由它第一個參數所傳入的函數對象決定的

2. reduce函數

注意:使用reduce函數必須首先從functools中導入,即:

from functools import reduce

reduce函數需要至少2個參數,第一個參數是一個函數,叫做累加器。累加器只能傳2個參數,多或少都不可以,第二個參數是一個可迭代對象,第三個參數是可選參數,爲初始值。

reduce函數通過第一個函數來定義累加的過程,並將第二個可迭代對象映射爲一個值。執行的過程爲,首先會選取可迭代對象中第一個和第二個值,將其累加,再之後的操作,會將上一次的操作結果與可迭代對象下一個元素進行累加。第三個可選參數是初始值,如果存在的話會從初始值開始添加。我們通過幾個例子來加深理解:

  1. 求一個列表的整數和
from functools import reduce

def add(x,y):
	print(x,y)
	return x + y

nums = [1,2,3,4,5]
sum = reduce(add,nums)
print(sum)

# 結果
1 2
3 3
6 4
10 5
15

我們可以看到,reduce函數首先取了nums的前兩個元素1和2進行相加,獲得結果3,再把結果3與下一個元素3相加,得6,之後一直重複將上一次的和與下一個元素相加的操作,直到最後一個元素。

  1. 將一個字符串列表添加,並添加前綴
def concatenate(x,y):
	return x + y

web_strs = ["www.","google.","com"]
result_str = reduce(concatenate, web_strs, "https://")
print(result_str)

# 結果
https://www.google.com

可以看到,我們添加了一個初始值"https://",它會作爲最開始的值開始與字符串列表中的元素進行累加

3. filter函數

filter函數需要兩個參數,第一個參數是一個函數,第二個參數是一個可迭代對象。

filter函數會將第一個函數一次施加在第二個參數,可迭代對象的每一個元素上,並且返回結果爲True的值。也就是說,第一個參數起到了判斷是否滿足條件的作用,因此,最好直接返回布爾類型的結果。

在我的好奇下,我嘗試了返回其他類型返回值的函數,仍然可以正常運行filter函數,但結果會強轉爲布爾類型。數值類型轉換布爾類型容易理解,我又試驗了Python中字符串如何轉換爲布爾類型。在Python 3中,只有空字符串會轉換爲False,只要字符串不爲空,即爲True。

另一種情況,是判斷函數有多個返回值的情況。衆所周知,Python允許多個返回值的存在,由於filter函數只會接收一個函數的返回值,因此實際上filter函數接收的是一個返回值元組,再將其轉換爲布爾值。而當元組轉換成布爾值時,似乎只有()(0)會轉換爲False,其餘都是True

當然,上面一段信息和Filter函數本身沒有關係,但我們在使用中仍然要注意,最好讓判別函數直接返回布爾值類型,下面我們舉一個例子,選取列表中大於0的數

def isPositive(num):
	return True if num > 0 else False

nums = [-1,-2,1,2,3,-3,4,-5]
pos_nums = list(filter(isPositive, nums))
print(pos_nums)

# 結果
[1,2,3,4]

注意filter函數返回的是一個迭代器對象,需要調用next函數來訪問下一個對象,如果想獲得列表對象,需要像上面的代碼塊一樣進行強轉

三、lambda表達式與匿名函數

1. lambda表達式

如上文的代碼所示,有些函數的內容實際上比較簡略,如果使用def關鍵字,可能會造成代碼變得冗雜。lambda關鍵字使得我們可以快速定義一個函數,且直接獲取函數對象,格式爲:
函數對象 = lambda 參數: 函數體
我們把函數的參數寫在lambda關鍵字後,如果有多個則用逗號隔開,在參數後寫一個冒號,後面是一個表達式作爲函數體,並將其結果作爲返回值返回,例如我們將上文的add函數用lambda關鍵字重新實現:

'''
def add(x, y):
	return x + y
'''

add = lambda x, y: x + y
add(3,4)
 
# 結果
7
2. 匿名函數

如果我們的函數比較簡潔,且我們明知只會調用一次,不會再次被調用,尤其是在上文介紹的apply函數、filter函數和reduce函數等工具中,我們可以直接使用lambda函數來創建函數對象,下面我們來舉一個例子。例如上文的reduce函數連接網站地址的例子中,我們可以改寫爲:

from functools import reduce

web_strs = ["www.","google.","com"]
web_str = reduce(lambda x,y:x+y, web_strs, "https://")

在上文我們首先定義了concatenate函數,在將函數的對象傳入到reduce函數中;而我們使用lambda關鍵字會直接創建一個函數對象,而因爲我們沒有將該函數對象保存在一個對象中,因此這個函數對象沒有名字,被稱爲匿名函數。因爲匿名函數沒有名字,我們在未來無法調用,因此匿名函數可以理解爲一個比較簡單、一次性的函數。

四、函數式編程下的控制結構

在函數式編程中,我們要把程序使用多個函數來實現。一般的表達式可以直接放在函數中,而其他的控制結構,也就是判斷、循環、break和continue關鍵字等,如何在函數中進行判斷呢?

1. 函數式編程中的分支判斷

在Python中,我們可以使用三元運算符來實現分支判斷,例如我們來判斷一個數字是否是正數的時候:

def isPositive(num):
	if num > 0:
		return True
	else:
		return False

# 可以改寫爲
def isPositive(num):
	return True if num > 0 else False

# 還可以改寫爲
isPositive = lambda num: True if num > 0 else False

如果我們的數據在列表中,我們可以使用filter函數來根據自己實現的判別函數進行數據過濾

在後續的文章中,我會介紹其他更強大的工具

2.函數式編程中的循環

循環的過程是將一個執行過程重複多次,顯而易見,我們可以通過遞歸來實現循環的過程。遞歸的含義爲,一個函數調用其自身,因爲循環需要有維持的條件,遞歸需要設置退出的條件。假設,我們使用循環求1到某個數的和 (當然,這樣並不是最優解,直接使用數列求和公式是效率最高的方法):

def sum_up(num):
	result = 0
	for i in range(1,num+1):
		result = result + i
	return result

# 可以改寫爲
def sum_up(num):
	if num == 0:
		return 0
	else:
		return num + sum_up(num-1)

迭代的定義雖然簡單,但使用地簡潔巧妙是比較困難的,需要大量的經驗和聯繫

五、函數式編程有什麼好處?

我們爲什麼費了這麼大力氣,要使用函數式編程呢?原來的編程方法不好嗎?函數式編程有以下幾個好處:

  1. 接近自然語言,易於理解
    由於函數的名字是自己起的,相比表達式來說,執行函數更接近於自然語言,更容易理解,比如:
# 傳統寫法
(1 + 2) * 3 - 4

# 函數式寫法
substract(multiply(3,add(1,2)) - 4)

# 還可以改寫爲
add(1,2).multiply(3).substract(4)
  1. 方便管理與測試
    只要我們的函數代碼可以通過單元測試,那在外部調用的時候就不會出現問題

  2. 單一職責
    函數式編程強調,函數只能通過參數來返回值,即不能在函數內部與外部作用域的變量進行交互,尤其不能修改外部變量的值。通過這種思想我們避免了代碼中的耦合

  3. 引用透明
    像第3點提到的,函數不能與外部變量交互,因此函數的運行不依賴外部的值,也就是說,只要函數在兩次調用的時候傳遞的參數相同,它們的返回值一定相同

  4. 易於併發編程
    由於函數式編程不會與外部變量進行交互,因此也就不會有死鎖的情況出現,可以很安心地進行併發編程

  5. 易於熱升級
    由於函數的接口是統一的,底層如何改變不會影響函數的調用

然而,函數式編程也有很多缺點,導致了它在至今在工業界仍然被命令式編程所壓制:

  1. 執行速度慢
    因爲大部分情況下要使用遞歸,因此函數式編程最大的缺點就是執行慢

  2. 算法問題
    大部分算法的效率已經比較差了,使用函數式編程會更差;並且大部分算法都是使用命令式編程實現的,函數式編程在很多算法上需要從0開始

  3. 併發問題
    雖然函數式編程可以很好的解決併發死鎖問題,但因爲函數式編程執行效率低,在實踐中效果並不理想;然而我們使用併發的唯一理由就是要提高效率

儘管這樣,近期已經出現了多門例如Scala和Haskell的語言,Scala在分佈式系統領域已經做出了傑出的貢獻。在實際應用中,也很少有純函數式編程,通常會和命令行編程混合,來完成需要的功能。

參考文獻

[1] Python 函數式編程
[2] 什麼是函數式編程?函數式編程有什麼好處?
[3] Python入坑函數:從入門到走火入魔
[4] Python map() 函數
[5] Python reduce() 函數
[6] Python 3中reduce函數的使用
[7] 在Python中從字符串轉換爲布爾值
[8] 純函數式編程的缺陷


我和幾位大佬建立了一個微信公衆號,歡迎關注後查看更多技術乾貨文章
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章