這篇文章會讓讀者瞭解到在 python 背景下的函數語言程式設計。除了作為學術課程的一部分,大部分程式設計師很少會接觸到函數語言程式設計語言,比如 Lisp 或者 Haskell。由於 Python 是一門應用廣泛的語言,並且支援大部分函數語言程式設計的結構,所以這篇文章會盡可能展示它們的用途和優點。對於 Python 這門語言,函數語言程式設計可能不是最好或者最 Pythonic 的方式,但是在一些應用中,函數語言程式設計有它的優點,這也是本文接下來要講的東西。
1. 什麼是函數語言程式設計?
函數語言程式設計是一種以純函式為中心的程式設計正規化。如果你曾經寫過程式,你可能會將函式與子程式聯絡在一起,這是典型的‘CS’視角。這裡,我們更傾向於其數學定義:
純函式是指:任何函式基本上都可以用一個數學表示式來表示。這就意味著沒有副作用:沒有 IO 操作、沒有全域性狀態的變化、沒有資料庫的互動(你確實無法用一個數學表示式來實現這些,對嗎?)。純函式的輸出只依賴其輸入。所以,如果你使用相同的輸入多次呼叫純函式,每次得到的結果都一樣。當然,對於實際的函數語言程式設計語言如 Haskell,也能夠實現 IO 操作等功能,但是重點仍然是純函式。
2. lambda結構
Python 中,初始化一個純函式最簡單的方式就是使用 lambda 關鍵字:
1 2 3 4 5 6 7 |
>>> square_func = lambda x: x**2 >>> square_func(2) 4 >>> some_list = [(1, 3), (5, 2), (6, 1), (4, 6)] >>> some_list.sort(key=lambda x: x[0]**2 - x[1]*3) >>> some_list [(1, 3), (4, 6), (5, 2), (6, 1)] |
lambda 關鍵字可以讓你在一行內定義一個函式,如果僅僅是一個數學表示式,這種定義方法就非常方便。事實上,lambda 關鍵字在函數語言程式設計(不只針對 Python )中非常重要,其根源於 Lambda Calculus — 函數語言程式設計的“鼻祖”之一。
使用 lambda 初始化的函式也被稱作匿名函式。看一下上面程式碼的第五行,你把一個用 lambda 初始化的函式傳給排序方法。但並未給它命名,只是動態地定義並將它作為引數傳遞,因此稱它是“匿名的”。當然, 你也可以將匿名函式賦值給一個變數(見第一行),然後像普通函式一樣呼叫它們(見第二行)。
3. 函式是“第一等”公民
在函數語言程式設計中,函式是“第一等公民”。這基本上就意味著你可以像使用其他物件那樣使用函式 — 賦值給變數、作為引數傳遞,甚至作為函式返回值。之前已經在程式碼中見過這種用法,下面是一些示例:
1 2 3 4 5 6 |
>>> square_func = lambda x: x_*2 >>> function_product = lambda F, m: lambda x: F(x)_m >>> square_func(2) 4 >>> function_product(square_func, 3)(2) 12 |
square_func
本身是一個函式。另一方面,function_product
是一個高階函式,它有兩個輸入 — 函式F(x)
和乘數 m
。function_product
返回 F'(x)
, 等同於 m * F(x)
。因此,對於上面第五行程式碼,function_product(square_func, 3)
被呼叫時帶有引數2
,返回 2² * 3 = 12。
4. 資料和資料流的不變性
物件的不變性意味著一旦物件被初始化,你就絕不能再修改其資料的值。在函數語言程式設計中,無論何時你針對某些資料呼叫函式,你總會得到一個新的例項 — 你從不會“更新”任何變數的值。從程式設計角度來說,這就意味著,一旦你初始化一個變數如 x=3
,變數x將不再出現在宣告的左邊。
所以,任何函式式的程式碼都可以被看作是前饋資料流,你絕不會“回來”改變任何變數的值。因此,資料總是向前移動,從輸入到最終的輸出 — 從一個函式到另一個函式。
這種資料的不變性引出了另外一個屬性,稱之為引用透明性。這就意味著,只要所需的變數定義好,一個表示式的值在程式中任何地方都是一樣的。由於你不會更新任何變數或物件(包括函式)的值,所以在任何上下文中,一旦被定義,它們的意義就相同。基於此,函式式程式碼非常容易分析和除錯。你無需追蹤變數狀態的值或者記住任何更新。
資料不變性可以讓我們利用製表法 — 基本上,只要你“記住“一些代價高昂的函式輸出以及一種查表的某些常見引數即可。這就是犧牲記憶體來減少了計算的複雜度。
5. 遞迴
函數語言程式設計並未通過 while
或者 for
宣告提供迭代功能,同樣也未提供狀態更新。所以,遞迴在函數語言程式設計中就顯得尤為重要。值得記住的是,任何可迭代的程式碼都可以轉換為可遞迴的程式碼。
下面是計算第n個 Fibonacci 數的函式式版本:
1 2 3 4 5 6 7 8 9 |
>>> fibonacci = (lambda x, x_1=1, x_2=0: x_2 if x == 0 else fibonacci(x - 1, x_1 + x_2, x_1)) >>> fibonacci(1) 1 >>> fibonacci(5) 5 >>> fibonacci(6) 8 |
第二行定義了計算的基本條件,第三行實現了遞迴呼叫。思考一下,x
、x_1
以及 x_2
是必要的狀態變數,它們更新後的版本被傳到每一次遞迴中,這就是在函式式程式碼處理狀態的機制。
編寫函式式程式碼時,最好實現尾遞迴,特別是對純函式式的語言如 Schema 。這樣做的好處是:尾遞迴程式碼很容易通過編譯器優化成可迭代的程式碼(儘管這不適用於 Python),使得編譯後的程式碼更加高效。
6. 惰性求值
這是一個 Python 沒有采用函數語言程式設計的一個方面。在很多純函式式語言中,如Haskell,無需求值的物件是不會被求值的。求值意味著要計算函式表示式的值。考慮下面的程式碼:
1 |
length([3 + 4, 5, 1/0]) |
在例如Python這樣的語言中,出現 1/0 將會立刻丟擲異常。但是,如果我們實現惰性求值,函式就會返回3。因為列表中有三個物件,計數時無需計算它們的值。這會在資料流中造成一些圖規約,導致更少的函式呼叫(這會有忽略錯誤的風險)。
Python 3.x 確實支援不同種類的惰性求值,比如 some_dict.keys()
,呼叫時返回迭代器,這可以防止載入所有資料到記憶體中,從程式設計角度來說,這樣更高效。
7. 序列中沒有迭代器
雖然這是一點不太重要,但是由於迭代器下一個元素的值依賴於它的狀態(這違反了引用透明性),而在純函式式程式碼中沒有迭代器。如果我們編寫純函式式程式碼,替代序列,我們只處理明確且不變的tuple,在 Python 中可以使用 tuple()
從迭代器中生成 tuple。
8. map, reduce and filter
map
、 reduce
以及 filter
是三個高階函式,在所有純函式式語言中都包含這三個函式,當然 Python 中也有。它們的普遍性證明了,為了使函式式程式碼更加優雅,它們是多麼頻繁地被使用。
map 通過對 list 或者 array 中所有元素呼叫函式,基本上提供了一種並行性。下面是一個示例:
1 2 |
>>> map(lambda x: x**2, [1, 2, 3]) [1, 4, 9] |
注意 map 為何是並行的,由於你針對陣列中的是所有元素呼叫了同一個函式,並且沒有做任何修改,所以你可以以任何既定順序呼叫函式。
filter 針對序列提供另外一種並行。輸入一個 bool 值和函式,返回一個序列,保留序列中呼叫函式後返回 True 的值,示例如下:
1 2 |
>>> filter(lambda x: x % 2 == 0, [1,2,3,4,5,6]) [2, 4, 6] |
上面的程式碼過濾出列表中所有的偶數。
reduce 是一個結構體,它在序列上執行一系列迭代。其第一個引數是函式 F(a, x)
,F
接收兩個引數 — 一個累加器 a
和當前輸入 x
。F
計算並返回一個新值 a
。reduce中第二個引數是序列本身 — [x1, x2, ..., x_n]
,第三個引數初始的累加器 a
,內部實現是:
i. a_1 = F(a, x1)
ii. a_2 = F(a_1, x2)
iii. a_3 = F(a_2, x3)
最後得到的是 an。 一個示例:
1 2 |
>>> reduce(lambda x, y: x + y, [1, 2, 3], 0) 6 |
作為列表中的元素,累加器不必是相同型別,也可以是列表。下面是使用 reduce 翻轉序列的例子:
1 2 |
>>> reduce(lambda L, element: [element] + L, [1, 2, 3], []) [3, 2, 1] |
注意上面的程式碼中未改變列表(提醒一下,reduce
需要從 Python 3.x 中的 functools
中匯入)
此外,需要理解的是,因為每一次 reduce 對一個元素呼叫 F
函式,都依賴於上個元素產生的值,所以 reduce 不是並行的。
尾註 1:為了更好地讓您理解如何在 Python 中使用上述關鍵字或操作進行函數語言程式設計,本文提供了非常簡潔的例子和程式碼,同時也引入了一些先進的技術,如管道。一定要看一看!
尾註 2:對於為什麼函數語言程式設計是一些專門業務更喜愛的方式,本文提供了一些觀點,並對一些函式式程式碼提供了易懂的註解(寫的不好的地方請見諒)。
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!