因為本系列還是基於一些已經對Python
有一定熟悉度的讀者,所以我們在此不做非常多的贅述來介紹基本知識了。而是回我們之前的主題,我們要用迭代器和生成器實現之前的指數函式。
當然,我們這裡還是需要回到惰性列表是什麼這個問題。事實上,回到原來惰性求值的概念,惰性列表的概念其實是「需要時才計算出值」的列表。我們在呼叫iter
的時候,其實對常見的物件並沒有特別大的優勢。我們可以假想,其實iter
轉化[1, 2, 3, 4]
的結果其實如下:
def yield_list():
yield 1
yield 2
yield 3
yield 4
唯一的優勢,我們之前已經提到過了,就是反覆套用函式f
和g
時,我們是計算g(f(x))
而不是先把列表裡每個值套用f
再套用g
。這裡有個極大的優勢,就是提前終止時可以避免沒有必要的運算。比如,下面一個for
裡面的例子,我們是為了發現列表ls
中應用f
函式後如果結果等於a
就返回index否則返回None
:
def find_index_apply_f(f, ls, a):
for i, x in enumerate(ls):
if f(x) == a:
return i
else:
continue
return None
>>> find_index_apply_f(lambda x: x + 1, [1, 2, 3, 4, 5], 3)
1
現在,這裡提前跳出可以減少非常多的運算量,但是如果使用一個普通列表卻很難,我們在使用map
之後必然已經全都計算了,但如果惰性求值,我們可以就在需要的時候停止就行。這個是列表操作替代迴圈必須實現的東西。
第二個惰性列表的最大應用,就是無窮列表,比如下面一個生成器,我們可以生成一個無限長度的全是x
的列表。後面我們會聊到我們在各種場合中已經用到了這個抽象。
def yield_x_forever(x):
while True:
yield x
實現一些常用的(惰性)列表操作
大部分操作迭代器/生成器的函式,我們都可以在itertoools
中找到。但,我們這裡還是要實現一些非常函式式的函式,方便以後的操作:
1. head
head
很簡單,即取出(惰性)列表第一個元素:
head = next
2. take
take
的目標是列表前N個值,這個可以實現成觸發計算(轉化成非惰性物件,一般為一個值或者列表)或者不觸發計算的版本。下面我們實現的是觸發計算的函式。
def take(n, it):
"""將前n個元素固定轉為列表
"""
return [x for x in islice(it, n)]
take_curry = lambda n: lambda it: take(n, it)
3. drop
drop
則相反是刪去前N個值。
def drop(n, it):
"""剔除前n個元素
"""
return islice(it, n, None)
4. tail
tail
是刪去head
後的列表,可以用drop
實現:
from functools import partial
tail = partial(drop, 1)
5. iterate
iterate
是重點要用到的函式,就是通過一個迭代函式還有初始值,實現一個無窮列表:
def iterate(f, x):
yield x
yield from iterate(f, f(x))
比如,實現所有正偶數的無窮列表:
positive_even_number = iterate(lambda x: x + 2, 2)
當然,更簡單地寫法是使用itertools
裡面的repeat
和accumulate
:
def iterate(f, x):
return accumulate(repeat(x), lambda fx, _: f(fx))
簡單實踐
例子一:求指數
我們回到之前求指數的例子中,我們可以實現惰性列表的版本。
第一個思路,我們就是直接用iterate
從x
開始,每次乘以x
,然後取出前n
個值,拿到最後一個:
power = lambda x, n: take(n, iterate(lambda xx: xx * x, x))[-1]
另一個就是先生成一個無窮長度的x
,取出前n
個,相乘來reduce
:
power = lambda x, n: reduce(
lambda x, y: x * y,
take(n, iterate(lambda _: x, x))
)
當然,我們還可以用生成器生成無窮長列表:
def yield_power(x, init=x):
yield init
yield from yield_power(x, init * x)
例子二:查詢
我們回到上面解說的例子,我們要找到一個無窮列表中套用f
後,第一個等於a
的值的index
。如果不是惰性的話,這個必須提前跳出也不可能實現。
def find_a_in_lazylist(f, lls, a):
return head(filter(lambda x: f(x[1]) == a, enumerat(lls)))[0]
總結
本章回顧了利用Python
自帶的生成器、迭代器實現惰性列表,並展示如何運用這些概念做一些資料操作應用。當然在其中,我們要深刻感受到,函數語言程式設計與資料是非常親近的,它關注資料勝於專案結構,這點和物件式程式設計非常不同。大部分物件式程式設計的教程傾向於概述分層、結構這些概念,真是因為這個是物件式程式設計擅長的地方。
在我實現的教學專案fppy
(點選這裡前往github
)中,我用內建的python
模組實現了一個LazyList
類,用它可以用鏈式寫法完成上面的所有例子:
power1 = lambda x, n: LazyList.from_iter(x)(lambda xx: x * x).take(n).last
power2 = lambda x, n: LazyList.from_iter(x)(lambda _: x).take(n).reduce(lambda xx, yy: xx * yy)
find_a_in_lazylist = lambda f, lls, a: LazyList(lls)\
.zip_with(LazyList.from_iter(0)(lambda x: x + 1))\
.filter(lambda x: f(x[1]) == a)\
.split_head()[0]