7. 函數式編程
函數式編程,即 Functional Programming,有以下幾個特點:
- 沒有副作用:對於任意一個函數,只要輸入是確定的,輸出就是確定的,這種純函數沒有副作用;
- 接近自然語言,易於理解:函數式編程的自由度很高,可以寫出很接近自然語言的代碼;
- 函數是“第一等公民”:函數與其他數據類型一樣,處於平等地位,可以賦值給其他變量,也可以作爲參數,傳入另一個函數,或者作爲別的函數的返回值。
Python 對函數式編程提供部分支持。由於Python允許使用變量,因此,Python不是純函數式編程語言。
7.1 高階函數
高階函數,即 Higher-order function,有幾個特點:
變量可以指向函數
這裏以 abs()
函數來作說明:
print(abs(-10)) # 10
# 直接打印 abs 呢?
print(abs) # <built-in function abs>
# 把函數本身賦值給變量
f = abs
print(f) # <built-in function abs>
# 使用變量 f 來調用 abs() 函數
print(f(-100)) # 100
可以看到,可以用變量指向函數,並且可以通過這個變量來調用函數。
函數名也是變量
函數名其實就是指向函數的變量。
通過 abs()
這個內置函數進行說明,這個函數的作用是計算絕對值。這裏我們完全可以把 abs
這個函數名看作變量,它指向了一個可以計算絕對值的函數。既然 abs
是一個變量,當然可以指向其他對象。
# 函數名也是變量
# abs = 10
# print(abs(-10)) # 報錯
'''
Traceback (most recent call last):
File "higher_order_function_intro.py", line 13, in <module>
print(abs(-10))
TypeError: 'int' object is not callable
'''
上面我們把 abs
這個函數名,當做變量,指向了 10
。接着,又期望 abs(-10)
可以得出結果 10
。實際上,是報錯的。其實現在 abs
是指向了 10
,而不再指向絕對值函數了。這一點是需要注意的。
傳入函數
既然變量可以指向函數,而函數的參數可以接收變量,那麼一個函數就可以接收另一個函數作爲參數,這種函數就是高階函數。
# -----傳入函數------
def add(x, y, f):
return f(x) + f(y)
print(add(-5, 6, abs)) # 11
7.1.1 map/reduce
map
函數接收多個參數,第一個是函數,後面的參數是一個或多個Iterable
,用逗號隔開,返回一個 Iterator
。map
函數語法:
map(function, iterable, …)
map
函數將傳入的函數依次作用到Iterable
中的每個元素上,把結果作爲 Iterator
返回。map
函數是 Python 中的內置函數。
from collections.abc import Iterator, Iterable
# map() 函數
# 把 f(x) = x ^ 2, 作用在一個 list [1,2,3,4,5,6,7,8,9] 上
def f(x):
return x ** 2
r = map(f, [1,2,3,4,5,6,7,8,9])
print(isinstance(r, Iterator)) # True,說明 r 是 Iterator。
print(isinstance(r, Iterable)) # True,說明 r 是 Iterable。
# Iterator 是一個惰性序列
# 迭代方式一:
print(list(r))
# 迭代方式二:
for y in r:
print(y)
利用 map
函數,把 list
中的每個整數轉爲字符串:
print(list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9])))
# 打印結果:['1', '2', '3', '4', '5', '6', '7', '8', '9']
利用 map 函數,對兩個列表相同位置的元素進行相加
print(list(map(lambda x, y : x + y, [1, 3, 5, 7, 9], [2, 4, 6, 8, 10]))) # [3, 7, 11, 15, 19]
reduce
函數:第一個參數是函數,第二個參數是 Iterable
,第三個參數是可選參數,reduce
函數的返回值與作爲第一個參數的函數的返回值是一致的。需要注意的是,作爲第一個參數的函數必須接收兩個參數。reduce
函數的語法:
reduce(function, sequence [, initial] )
reduce
函數的作用是如果不提供 initial
參數,則先用函數對可迭代序列中的第一個元素和第二個元素進行操作,得到一個結果,再用函數對上一步得到的結果和第三個元素進行操作,以此類推,直到達到最後的結果爲止。如果提供 initial
參數,則先用函數對 initial
的值和可迭代序列中的第一個元素進行操作。
reduce
函數並不是 Python 的內置函數,使用 reduce
函數需要先進行導入:
from functools import reduce
計算列表的和:
def add(x, y):
return x + y
print(reduce(add, [1, 2, 3, 4, 5])) # 15
把序列[1,3,5,7,9]變爲整數13579:
def trans(x, y):
return 10 * x + y
print(reduce(trans, [1, 3, 5, 7, 9])) # 13579
把 str
類型 轉 int
類型:
DIGITS = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4,
'5': 5, '6': 6, '7': 7, '8': 8, '9': 9}
def str2int(s):
def char2num(ch):
return DIGITS[ch]
def fn(x, y):
return 10 * x + y
return reduce(fn, map(char2num, s))
print('1378') # 1378
上面的例子涉及了局部函數:Python 支持在函數體內定義函數,這既是局部函數。
7.1.2 filter
filter
函數接收兩個參數,第一個參數是函數,第二個參數是可迭代對象,返回值是迭代器對象。
filter
函數是 Python 的內置函數,filter
函數的語法爲:
filter(function, iterable)
filter
函數的作用是過濾序列,將序列中的每個元素作爲參數傳遞給函數,函數判斷結果是 True
或 False
,爲 True
時,則該元素保留在新的列表中,爲False
時,則該元素不保留在新的列表中。
需要注意的是,當 function
參數不是 None
時,filter(function,iterable)
和生成器表達式 (item for item in iterable if function(item))
是等價的;當 function
參數是None
時,等價於 (item for item in iterable if item)
。
在一個 list 中,刪掉偶數,只保留奇數:
# 判斷是不是奇數的方法
def is_odd(n):
return n & 1 != 0
print(list(filter(is_odd, [1, 2, 3, 5, 8, 9, 12]))) # [1, 3, 5, 9]
在一個 list 中,刪除負數,只保留整數:
print(list(filter(lambda x : x > 0, [-1, -2, 3, 4, 5]))) # [3, 4, 5]
第一個參數爲 None
時,保留判斷爲 True
的元素:
print(list(filter(None, [0, 1, '', False, True]))) # [1, True]
7.1.3 sorted
sorted
函數的語法:
sorted(iterable, *, key=None, reverse=False)
第一個參數是一個可迭代對象,
第二個參數是個函數,用來處理進行比較的元素,只有一個參數,具體的函數的參數就是取自於可迭代對象中,指定可迭代對象中的一個元素來進行排序。
第三個參數是排序規則,reverse = True 降序 , reverse = False 升序(默認)。
返回一個新的 list。
參數列表裏的 *
表示命名關鍵字參數,意思就是後面兩個參數的名字 key
和 value
是被限定的,而且如果傳遞實參時,必須要帶上參數名。
我們還注意到,key
和 value
這兩個參數使用了默認參數。
# 對 list 進行從小到大排序
print(sorted([36, -3, 1, 20, -5])) # [-5, -3, 1, 20, 36]
# 對 list 進行從大到小排序
print(sorted([36, -3, 1, 20, -5], reverse=True)) # [36, 20, 1, -3, -5]
# 對 list 按絕對值從小到大排序
print(sorted([36, -3, 1, 20, -5], key=abs)) # [1, -3, -5, 20, 36]
7.2 返回函數
函數作爲返回值
以求和函數爲例說明:
# 函數作爲返回值,不會立即求和
def lazy_sum(*args):
# 這裏定義了局部函數
def sum():
result = 0
for n in args:
result += n
return result
return sum
f = lazy_sum(1, 2, 3, 4)
print(f) # <function lazy_sum.<locals>.sum at 0x7f51b0fd28c8>
print(f()) # 10
上面的代碼使用了閉包,那麼什麼是閉包呢?
一個函數定義中引用了函數外定義的變量,並且該函數可以在其定義環境外被執行。這樣的一個函數我們稱之爲閉包。
因爲在上面的代碼中,函數 sum
就是一個閉包,它引用了 lazy_sum
的參數 args
。
每次調用 lazy_sum
,都會返回一個新的函數,即便是傳入的參數是一模一樣的:
# 每次調用 lazy_sum ,都會返回一個新的函數變量
f1 = lazy_sum(1, 2, 3, 4)
f2 = lazy_sum(1, 2, 3, 4)
print(f1) # <function lazy_sum.<locals>.sum at 0x7f8c919ea950>
print(f2) # <function lazy_sum.<locals>.sum at 0x7f8c919ea9d8>
但是, 返回閉包時牢記一點:返回函數不要引用任何循環變量,或者後續會發生變化的變量。
7.3 匿名函數
以之前學習 map
函數時,把容器中的數字,經過 f(x) = x^2,變換每一個元素,得到新的容器的例子來說明:
之前的寫法是:
def f(x):
return x ** 2
print(list(map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])))
# 打印結果:[1, 4, 9, 16, 25, 36, 49, 64, 81]
匿名函數的寫法是:
print(list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9])))
# 打印結果:[1, 4, 9, 16, 25, 36, 49, 64, 81]
可以看到我們使用 lambda x: x * x
替代了函數 f
。
關鍵字 lambda
表示匿名函數,冒號前面的 x
表示函數參數,和函數一樣,參數列表可以包含多個參數,也可以不包括任何參數;冒號後面的 x * x
表示函數體,但是匿名函數的函數體只能有一個表達式,並且不用寫return
,返回值就是這個表達式的結果。
匿名函數也是函數,而函數可以賦值給變量,所以匿名函數也可以賦值給變量。
g = lambda x, y: x + y
print(g(1, 1)) # 2
匿名函數也可以作爲返回值返回。
def add(x, y):
return lambda: x + y
print(add(2, 3)()) # 5
注意上面的例子中,匿名函數的參數列表爲空,但是冒號可以不能省略的。
7.4 裝飾器
裝飾器這一部分有點不好懂。
Python 中的裝飾器(decorator)允許在不修改原函數定義的情況下,在代碼允許期間動態添加功能。這樣的好處是,抽離出大量與函數功能本身無關的代碼到裝飾器中並獲得重用。
7.5 偏函數
偏函數(partial function),是由 Python 的 functools
模塊提供的功能。
偏函數的作用是:當函數的參數個數太多,需要簡化調用時,可以使用 functools.partial
創建一個新的函數,新函數可以固定住原函數的部分參數,從而在調用時更簡單。
以 int
函數把字符串轉爲整數的例子來說明:
大量需要一個把字符串,按二進制轉爲十進制的整數,可以這樣寫:
print(int('100000', base = 2)) # 32,把 100000 按照二進制,轉爲十進制是32
print(int('10000', base = 2)) # 16,把 10000 按照二進制,轉爲十進制是16
print(int('1000', base = 2)) # 8,把 1000 按照二進制,轉爲十進制是8
抽取一個函數 int2
,是這樣的:
def int2(x, base=2):
return int(x, base)
print(int2('100000')) # 32
print(int2('10000')) # 16
print(int2('1000')) # 8
使用偏函數,需要先導入 import functools
:
int2 = functools.partial(int, base = 2)
print(int2('100000')) # 32
print(int2('10000')) # 16
print(int2('1000')) # 8
print(int2('100000', base = 10)) # 100000