Python函數語言程式設計系列012:惰性列表之生成器與迭代器

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

因為本系列還是基於一些已經對Python有一定熟悉度的讀者,所以我們在此不做非常多的贅述來介紹基本知識了。而是回我們之前的主題,我們要用迭代器和生成器實現之前的指數函式。

當然,我們這裡還是需要回到惰性列表是什麼這個問題。事實上,回到原來惰性求值的概念,惰性列表的概念其實是「需要時才計算出值」的列表。我們在呼叫iter的時候,其實對常見的物件並沒有特別大的優勢。我們可以假想,其實iter轉化[1, 2, 3, 4]的結果其實如下:

def yield_list():
    yield 1
    yield 2
    yield 3
    yield 4

唯一的優勢,我們之前已經提到過了,就是反覆套用函式fg時,我們是計算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裡面的repeataccumulate

def iterate(f, x):
    return accumulate(repeat(x), lambda fx, _: f(fx))

簡單實踐

例子一:求指數

我們回到之前求指數的例子中,我們可以實現惰性列表的版本。

第一個思路,我們就是直接用iteratex開始,每次乘以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]

相關文章