我們在惰性求值中,我們介紹了「惰性列表」的概念,這個概念,其實在Python
種也有部分原生支援。這就是很受新手困擾的生成器和迭代器了。但之前,我們首先要回顧一下關於列表的功能。
從二元元組到列表
首先,我們可以用\(\lambda\)演算定義一個二元的元組,或者叫pair
:
pair
: \(\lambda a b f.f a b\)first
: \(\lambda p. p(\lambda a b. a)\)second
: \(\lambda p. p(\lambda a b.b)\)
具體實現如下:
pair = lambda a: lambda b: lambda f: f(a)(b)
first = lambda p: p(lambda a: lambda b: a)
second = lambda p: p(lambda a: lambda b: b)
我們可以定義測試一下:
>>> p = pair(1)(2)
>>> first(p)
1
>>> second(p)
2
當然有了pair
,定義一個列表就不是難事,即下面的方式組合就好(我們還是用python
自帶的元組表示):
(1, (2, 3, (4, ()))))
我們將在後面的章節裡分別用元組和類/型別的方式來定義列表。但在這篇文章裡,我們先回到之前python
的自帶的概念來,看函數語言程式設計如何處理遍歷問題的。
列表操作
列表操作,是函數語言程式設計的一個重要概念,實時上它是通過遞迴來實現對一個線性結果的遍歷。比如下面的類C風格的程式碼:
ls = [1, 2, 3, 4]
for i in range(0, len(ls)):
ls[i] = ls[i] + 1
這裡出現了兩個副作用,一個是i
的自增,另一個是對ls
的原地操作。而且,它們也用到了變數的概念。當然,這種寫法其實無可厚非,可維護性也尚可,算是可以容忍的副作用。當然我們最簡單的實現,相當於大家都知道是列表表示式(當然,事實上它還是有副作用的):
[i + 1 for i in ls]
當然,大部分人也見過列表表示式的完整操作,可以自帶篩選:
[i + 1 for in in ls if i % 2 == 0]
這就是函數語言程式設計遍歷資料最簡單的操作,當然,它們還有一個名字,就是map
和filter
,在Python
中,它們返回的就是可迭代物件(我們可以呼叫list
轉換成列表):
map(lambda x: x + 1, ls) # [i + 1 for i in ls]
filter(lambda x: x % 2 == 0, ls) # [i for i in ls if x % 2 == 0]
另一個常用的列表操作是reduce
,它起到的是聚合作用,我們只要定義一個二元運算,就可以將列表從頭合併到尾聚合操作。
reduce
操作檢視解決的問題就是遍歷後彙總值的過程。譬如,我們要實現ls
的求和,在一般的程式式程式設計中,我們會使用如下的方法:
res = 0
for i in ls:
res += i # 或者和下面更類似的寫法 res = res + i
而,使用reduce
,我們僅需要如下程式碼即可完成。
from functools import reduce
reduce(lambda x: x + y, ls)
具體的計算過程如下:
- 獲取
ls
第一個值1
和第二個值2
,套用lambda x, y: x + y
,得到3
。 - 獲取
ls
第三個值3
,套用第一步的結果3
和lambda
得到6
。 - 獲取
ls
第三個值4
,套用第二步的結果6
和lambda
得到10
- 完成計算返回結果。
但,其實如果檢視Python
的reduce
函式的引數,我們會發現它還可以帶入初始值,當帶入初始值時,在各類函式式語句中,一般把它叫做fold_left
/foldl
函式。這個有沒有初始值效果會不一樣很多。第一個就是處理列表是空的問題:
reduce(lambda x, y: x + y, []) # 報錯
reduce(lambda x, y: x + y, [], 0) # return 0
我們甚至可以把這個和前面的程式式程式設計的各種元素對應起來,0
相當於res = 0
,lambda x, y: x + y
表達的就是res = res + i
。但是,其實foldl
比reduce
更強大的層面,在於,這個運算本身可以涉及不同型別。我們採用型別標誌,就會發現reduce
函式本身的運算只能是Callable[[S, S], S]
/(S, S) -> S
,但其實我們在很多場景中,需要的是一個型別裝換。比如:
[1, 2, 3, 4]
=>"1234"
[1, 2, 3, 4]
=>[[1], [2], [3], [4]]
- ...
如果單純使用reduce
我們無法操作這種涉及型別轉換的內容,foldl
帶入的二元運算型別標註則是Callable[[S, T], S]
/(S, T) -> S
。這就讓我們可以通過設定一個另一個型別的初始值,來實現這件事,比如上面轉換成字串的例子,我們很容易找到下面的二元運算(注意前後順序):
lambda x, y: x + str(y)
而初始值僅需設定一個空的""
字串即可,即如下實現(嘗試自己實現一下[1, 2, 3, 4]
=> [[1], [2], [3], [4]]
吧!):
reduce(lambda x, y: x + str(y), ls, "")
總結
本篇文章中,我們回顧了Python
原生的列表,以及介紹函數語言程式設計通過列表表示式/列表操作來實現過程式中常見的資料遍歷的問題來規避for
/while
中不可避免的副作用。我們接下來將會使用pair
的概念從頭實現一個列表,然後我們就進入到正式的惰性列表的概念中,看看惰性列表如何處理這類問題,以及用函式式思考流式處理、執行緒的概念。