一、概述
Python是一門多范式的編程語言,它同時支持過程式、面向對象和函數式的編程范式。因此,在Python中提供了很多符合 函數式編程 風格的特性和工具。
以下是對 Python中的函數式編程 的簡要總結,關於這一主題更全面的討論可以參考 Functional Programming HOWTO。
二、lambda表達式(匿名函數)
除了 Python基礎:函數 中介紹的 def語句,Python還提供了另外一種定義函數的方法: lambda表達式。
lambda表達式的語法如下:
lambda [arguments]: expression
與def語句類似,lambda表達式創建的函數:
- 也是可調用對象(接受0個或多個參數,返回一個值)
- 也是一等公民(first-class)
- 具有同樣的 參數風格 和 作用域規則
- 也支持嵌套定義(def中的lambda,或lambda中的lambda)
但是lambda表達式與def語句之間,也存在很多顯著的差異:
差異點 | 函數(lambda表達式) | 函數(def語句) |
---|---|---|
函數體 | 只能是單行表達式(expression) | 可以是任意復雜的語句(statement) |
函數返回值 | 返回值就是函數體中的表達式的求值結果 | 由函數體中的return語句指定 返回值 |
函數名 | 定義后直接返回函數對象(匿名函數) | 定義后自動為函數對象綁定函數名 |
函數定義位置 | 可以在任何允許函數對象出現的位置定義(支持即時定義,即時調用) | 只能在允許語句出現的位置定義(先定義,后調用) |
用途 | 多用於一次性使用的簡單函數 | 適用於一切函數和類方法 |
以下是lambda表達式的簡單示例:
# def語句
>>> def func(x, y): return x + y # 自動綁定函數名為func
...
>>> func
<function func at 0xb76eff7c>
>>> func(1, 2) # 先定義,后調用
3
# lambda表達式
>>> lambda x, y: x + y # 匿名函數(直接返回函數對象)
<function <lambda> at 0xb76ef0d4>
>>> (lambda x, y: x + y)(1, 2) # 即時定義,即時調用
3
>>> f = lambda x, y: x + y # 手動綁定函數名
>>> f(1, 2) # 也可以先定義,后調用
3
>>>
>>> ((lambda x: (lambda y: x + y))(1))(2) # 嵌套定義的lambda(較復雜,盡量避免)
3
三、內建函數filter()、map()、reduce()
1、filter()
函數原型:filter(function, iterable)
說明:返回一個由iterable中的某些元素組成的列表,這些元素使得function返回True。若iterable為字符串(或元組),則返回字符串(或元組);否則,總是返回列表。如果function為None,則默認為恆等函數(identity function,類似 f(x) = x)。
示例:
# for循環版本
>>> res = []
>>> for x in 'a1b2c3d4e5f6':
... if x.isalpha():
... res.append(x)
...
>>> res
['a', 'b', 'c', 'd', 'e', 'f']
# filter版本
s = 'a1b2c3d4e5f6'
>>> filter((lambda x: x.isalpha()), s) # iterable為字符串,則返回字符串
'abcdef'
>>> filter((lambda x: x.isalpha()), tuple(s)) # iterable為元組,則返回元組
('a', 'b', 'c', 'd', 'e', 'f')
>>> filter((lambda x: x.isalpha()), list(s)) # iterable為其他迭代對象,則返回列表
['a', 'b', 'c', 'd', 'e', 'f']
>>> filter(None, list(s)) # function為None,則默認為恆等函數
['a', '1', 'b', '2', 'c', '3', 'd', '4', 'e', '5', 'f', '6']
2、map()
函數原型:map(function, iterable, ...)
說明:逐個以iterable中的元素為參數調用function,並返回結果的列表。如果存在多個iterable,則以最長的為准(其他不足的補None),逐個並行取出元素作為參數調用function(如map(function, iter1, iter2)會返回列表[function(iter1[0], iter2[0]), function(iter1[1], iter2[1]), ...])。如果function為None,則默認為恆等函數。
示例:
# for循環版本
>>> res = []
>>> for x in [1, 2, 3, 4, 5]:
... res.append(x ** 2)
...
>>> res
[1, 4, 9, 16, 25]
# map版本
>>> map((lambda x: x ** 2), [1, 2, 3, 4, 5])
[1, 4, 9, 16, 25]
>>> map(None, [1, 2, 3, 4, 5]) # function為None,則默認為恆等函數
[1, 2, 3, 4, 5]
>>> map((lambda x, y: x + y), [1, 2, 3], [4, 5, 6]) # 存在多個iterable,則返回[1+4, 2+5, 3+6]
[5, 7, 9]
>>> map(None, [1, 2, 3], [4, 5]) # 以最長的iterable為准,其他不足的補None
[(1, 4), (2, 5), (3, None)]
3、reduce()
函數原型:reduce(function, iterable[, initializer])
說明:以累加方式逐個取出iterable中的元素作為參數調用(具有雙參數的)function,從而最終將iterable簡化為一個值(如reduce(function, [1, 2, 3])會返回function(function(1, 2), 3))。如果存在initializer,則在累加調用中,以它作為初始的第一個參數。function必須是可調用對象(不能為None)。
示例:
# for循環版本
>>> total = 0
>>> for x in [1, 2, 3, 4, 5]:
... total += x
...
>>> total
15
# reduce版本
>>> reduce((lambda x, y: x + y), [1, 2, 3, 4, 5]) # 相當於((((1+2)+3)+4)+5)
15
>>> reduce((lambda x, y: x + y), [1, 2, 3, 4, 5], 10) # 帶有initializer的reduce,相當於(((((10+1)+2)+3)+4)+5)
25
>>> sum([1, 2, 3, 4, 5], 10) # 等效於上面的reduce
25
四、閉包
閉包(closure)是一個內嵌函數,它能夠記住其 外圍作用域 中的所有名字,即使這個作用域 看起來 已經不在外圍。
在以下示例中,內嵌函數action就是一個閉包:
>>> def maker(N):
... def action(x):
... return x * N
... return action
...
>>> mul10 = maker(10)
>>> mul10(3)
30
>>> mul10(5)
50
盡管函數調用mul10 = maker(10)
已經返回並退出了,但后續的mul10卻能夠記住整數10,從而計算入參的10倍數。
實際上,外圍作用域(如函數maker對應的代碼范圍)中的所有名字(如參數N)都作為環境信息被綁定到了action函數上,因此每次調用action時都可以訪問這些環境信息。特別地,可以通過特殊屬性func_closure
來獲取一個函數的自由變量綁定:
>>> def maker(N):
... def action(x):
... return x * N
... print(action.func_closure) # 打印出action函數的func_closure屬性值
... return action
...
>>> N = 10
>>> print('int N: id = %#0x, val = %d' % (id(N), N)) # N的值為10(整數10的地址是0x8e82044)
int N: id = 0x8e82044, val = 10
>>> mul10 = maker(N) # action.func_closure中含有整數10(即自由變量N)
(<cell at 0x90e96bc: int object at 0x8e82044>,)
閉包的這種 能夠記住環境狀態 的特性非常有用,Python中有一些其他特性就是借助閉包來實現的,比如 裝飾器。
五、偏函數應用
1、基本用法
偏函數應用(Partial Function Application)是一種簡化函數調用的方式,主要表現為對函數的部分參數進行固化。
Python中的偏函數應用是借助 functools.partial 來完成的。例如有一個專用於生成文章標題的函數title:
>>> def title(topic, part):
... return topic + u':' + part
...
如果要為 『Python基礎』 系列的多篇文章生成標題,可以有以下兩種方式:
# 普通版本
>>> print title(u'Python基礎', u'開篇')
Python基礎:開篇
>>> print title(u'Python基礎', u'函數')
Python基礎:函數
>>> print title(u'Python基礎', u'函數式編程')
Python基礎:函數式編程
# 偏函數版本
>>> from functools import partial
>>> pybasic_title = partial(title, u'Python基礎')
>>> print pybasic_title(u'開篇')
Python基礎:開篇
>>> print pybasic_title(u'函數')
Python基礎:函數
>>> print pybasic_title(u'函數式編程')
Python基礎:函數式編程
從上面的示例可以看出,如果在編碼過程中遇到了“多次用相同的參數調用一個函數”的場景,就可以考慮使用偏函數來固化這些相同的參數,進而簡化函數調用。
2、等效實現
1)默認參數
在上述示例中,如果將函數title的定義改為def title(part, topic=u'Python基礎')
也可以達到相同的效果。但是這種方式的不足之處也很明顯:
- 需要修改已有函數title的定義
- 默認參數只能有一個固定值,定義后即不能更改
相比之下,偏函數具有很好的靈活性:既不用修改已有函數的定義,又可以為函數的參數固化不同的值。
2)lambda表達式
使用 lambda表達式 也可以實現類似偏函數的功能,並且與默認參數不同的是,可以針對不同的參數值定義不同的lambda表達式(因為lambda表達式通常是一次性使用的)。例如上述示例中的pybasic_title也可以實現為:
>>> pybasic_title = (lambda part: u'Python基礎:' + part)
>>> print pybasic_title(u'開篇')
Python基礎:開篇
>>> print pybasic_title(u'函數')
Python基礎:函數
>>> print pybasic_title(u'函數式編程')
Python基礎:函數式編程
但是,由於lambda表達式本身的限制(參考 『lambda表達式』 一節),在具有復雜函數的場景中,還得使用偏函數。
3)閉包
最后,使用 閉包 同樣可以等效地實現偏函數的功能,並且與lambda表達式不同的是,它沒有任何限制場景。還是上面的例子:
>>> def title(topic):
... def topic_title(part):
... return topic + u':' + part
... return topic_title
...
>>> pybasic_title = title(u'Python基礎')
>>> print pybasic_title(u'開篇')
Python基礎:開篇
>>> print pybasic_title(u'函數')
Python基礎:函數
>>> print pybasic_title(u'函數式編程')
Python基礎:函數式編程
可以看出,這個閉包版本的唯一缺點是它需要對函數title進行重新定義(與默認參數的情況有些類似)。
總而言之,如果需要對 已有函數 進行參數固化,偏函數是最佳選擇。
六、列表解析
關於 列表解析(List Comprehensions),在 Python基礎:序列(列表、元組) 中有過簡單介紹。
這里主要強調兩點:
- 列表解析可以用來代替上面提到的一些函數式編程方法
- 列表解析還有一個生成器版本的近親:生成器表達式
1、用列表解析代替filter()和map()
1)filter()
列表解析可以完全代替filter():
- function不為None時:
[item for item in iterable if function(item)]
等價於filter(function, iterable)
- function等於None時:
[item for item in iterable if item]
等價於filter(None, iterable)
2)map()
在以下情況中,列表解析可以代替map():
- 只有一個iterable時
- function不為None:
[function(item) for item in iterable]
等價於map(function, iterable)
- function等於None:
[item for item in iterable]
等價於map(None, iterable)
- function不為None:
- 多個iterable長度相同時
- function不為None:
[function(*args) for args in zip(iter1, iter2, ...)]
等價於map(function, iter1, iter2, ...)
- function等於None:
zip(iter1, iter2, ...)
等價於map(None, iter1, iter2, ...)
- function不為None:
如果多個iterable具有不同的長度,那么列表解析就無法代替map()了。
2、生成器表達式
生成器表達式(Generator Expressions)與列表解析在語法和功能方面都非常相似。二者的根本差異是:生成器表達式返回一個 生成器,而列表解析返回一個列表。如下所示:
差異點 | 生成器表達式 | 列表解析 |
---|---|---|
表示方法 | (expr for item in iterable if cond_expr) | [expr for item in iterable if cond_expr] |
返回值 | 一個生成器 | 一個列表 |
與列表解析相比,生成器表達式具有 延遲計算(lazy evaluation)的特點,因此在使用內存上更有效。關於生成器表達式的實際案例,可以參考 Python核心編程(第二版) 中的 『8.13』 一節:『生成器表達式』。