Python 3 Tutorial 第四堂(2)略談函數式程式設計


Python 是個支援多重典範的語言,雖然不鼓勵,不過想在 Python 中進行一些函數式程式設計(Functional programming),基本上也有一些支援的元素。

惰性求值

在〈Python 3 Tutorial 第四堂(1)資料處理函式〉談到的 zipfiltermap,其實都是函數式程式設計中必備的基本元素,而在 Python 3.x 中,mapfilter 傳回的實例並不是 list,而是個產生器(Generator),具有惰性求值的特性,這也是函數式語言中常見的特性之一。

再來看一次〈Python 3 Tutorial 第四堂(1)資料處理函式〉中的例子:

lt = ['Justin', 'caterpillar', 'openhome']
print(list(filter(lambda ele: len(ele) > 6, lt)))
print(list(filter(lambda ele: 'i' in ele, lt)))
print(list(map(lambda ele: ele.upper(), lt)))
print(list(map(lambda ele: len(ele), lt)))

單看上頭這個程式,感覺你並沒有改變程式中 lt 的狀態,也就是沒有任何具有副作用的操作,而這就像是函數式程式設計的風格。

List 包含式

實際上,你也可以使用 for 包含式來進行類似的操作:

lt = ['Justin', 'caterpillar', 'openhome']
print([ele for ele in lt if len(ele) > 6])
print([ele for ele in lt if 'i' in ele])
print([ele.upper() for ele in lt])
print([len(ele) for ele in lt])

上頭的 for 包含式會產生 list 的結果,實際上,你也可以將 for 包含式兩側的 []() 來得到一個產生器,,像是將 [ele for ele in lt if len(ele) > 6] 改為 (ele for ele in lt if len(ele) > 6),同樣也就會具有惰性的效果。

正如〈Python 3 Tutorial 第二堂(3)容器、流程、for 包含式〉中談過的,for 包含式其實概念是來自函數式程式設計中的 List 包含式(List comprehension),例如數學式 S = { 2 . x | x ∈ N, x ≦ 10},用純函數式語言 Haskell 是寫為 [2 * x | x <- N, x <= 10],而在 Python 中就是寫成 [2 * x for x in N if x <= 10]

filtermap 能做得到的,for 包含式基本上都做得到,大多情況下,for 包含式比較常見,不過有時 lambda 透過適當的命名,改用 filtermap 會有比較好的可讀性。例如:

lt = ['Justin', 'caterpillar', 'openhome']
print(list(map(len, lt)))

看到了嗎?上頭直接將 len 函式傳入,可以將函式作為值傳遞的方便性就是如此,當函式本身具有名稱時,就可以增加可讀性。

實際上,函式可作為值傳遞、filtermapfor 包含式、惰性求值等,這些都是函數式程式設計中常見且極為基本的設計元素,你不用做完全的純函數式設計,只要在 Python 中適當地採用,在可讀性與效率方面,也許都會有所增益。

關於 reduce

在 Python 2.x 中,__builtin__ 中有個 reduce 函式,有時在別的語言中會被稱為 foldLeft,它其實代表了一種高度抽象化後的流程重用,只要是打算從清單中求值的需求,基本上都可以使用它。不過,reduce 的概念比較抽象而不易閱讀理解,通常會基於 reduce 打造更具體的函式,因此,在 Python 3.x 中,reduce 不再位於 __builtin__ 模組,而被移至 funtools 模組了。

以下對於 reduce 的介紹,是給你想更進一步瞭解函數式程式設計時使用,若不想理解,暫時跳過也沒關係。

舉例來說,如果你想要知道 [1, 2, 3, 4, 5] 的加總,雖然可以直接撰寫迴圈來求值,不過,也可以撰寫為 reduce(lambda sum, elem: sum + elem, [1, 2, 3, 4, 5], 0) 來求值,試試在 Python 互動環境中鍵入,結果會是 15。

>>> import functools
>>> functools.reduce(lambda sum, elem: sum + elem, [1, 2, 3, 4, 5], 0)
15
>>> 

初學者會有點難懂 reduce 的原理,可以藉由這段動畫來理解,你也就會知道,為什麼它又在別的語言中,被稱為 foldLeft:

資料處理函式

reduce 接受的 lambda 部份,改為一個具體名稱的 add 函式,那麼就可以寫為 為 reduce(add, [1, 2, 3, 4, 5], 0) ,配合上圖,reduce 的運作就像是折紙,從 0 開始,每折一次就與藍色數字進行一次 add,折完後的結果就是加總值。

這個 reduce 是極為通用的函式,可以有一百萬種用法,只要你想從某個清單中求值,都可以使用 reduce,只要你依需求給 reduce 要處理的函式與初值。

想要進一步知道 reduce 的原理,可以看看〈List 處理模式〉中的說明。

練習 7:使用 reduce

使用 reducefor 包含式,將以下的範例進行重構,目標是清除所有顯式的迴圈流程:

def ascending(a, b): return a - b
def descending(a, b): return -ascending(a, b)
# selection sort
def sorted(xs, compare = ascending):
    return [] if not xs else __select(xs, compare)

def __select(xs, compare):
    selected = xs[0]
    for elem in xs[1:]:
        if compare(elem, selected) < 0:
            selected = elem

    remain = []
    selected_list = []
    for elem in xs:
        if elem != selected:
            remain.append(elem)
        else:
            selected_list.append(elem)

    return xs if not remain else selected_list + __select(remain, compare)

print(sorted([2, 1, 3, 6, 5]))
print(sorted([2, 1, 3, 6, 5], descending))

如果你能完成練習 7,你已經做了一次函數式程式設計了。無論如何,藉由這個練習,瞭解到 Python 可進行多重典範設計,只要你願意的話,函數式是可以的設計之一。