Python函數語言程式設計系列009:惰性列表之常規列表

三次方根發表於2021-10-20

我們在惰性求值中,我們介紹了「惰性列表」的概念,這個概念,其實在Python種也有部分原生支援。這就是很受新手困擾的生成器迭代器了。但之前,我們首先要回顧一下關於列表的功能。

從二元元組到列表

首先,我們可以用\(\lambda\)演算定義一個二元的元組,或者叫pair:

  1. pair: \(\lambda a b f.f a b\)
  2. first: \(\lambda p. p(\lambda a b. a)\)
  3. 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]

這就是函數語言程式設計遍歷資料最簡單的操作,當然,它們還有一個名字,就是mapfilter,在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)

具體的計算過程如下:

  1. 獲取ls第一個值1和第二個值2,套用lambda x, y: x + y,得到3
  2. 獲取ls第三個值3,套用第一步的結果3lambda得到6
  3. 獲取ls第三個值4,套用第二步的結果6lambda得到10
  4. 完成計算返回結果。

但,其實如果檢視Pythonreduce函式的引數,我們會發現它還可以帶入初始值,當帶入初始值時,在各類函式式語句中,一般把它叫做fold_left/foldl函式。這個有沒有初始值效果會不一樣很多。第一個就是處理列表是空的問題:

reduce(lambda x, y: x + y, []) # 報錯
reduce(lambda x, y: x + y, [], 0) # return 0

我們甚至可以把這個和前面的程式式程式設計的各種元素對應起來,0相當於res = 0lambda x, y: x + y表達的就是res = res + i。但是,其實foldlreduce更強大的層面,在於,這個運算本身可以涉及不同型別。我們採用型別標誌,就會發現reduce函式本身的運算只能是Callable[[S, S], S]/(S, S) -> S,但其實我們在很多場景中,需要的是一個型別裝換。比如:

  1. [1, 2, 3, 4] => "1234"
  2. [1, 2, 3, 4] => [[1], [2], [3], [4]]
  3. ...

如果單純使用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的概念從頭實現一個列表,然後我們就進入到正式的惰性列表的概念中,看看惰性列表如何處理這類問題,以及用函式式思考流式處理、執行緒的概念。

相關文章