Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

5.2 隱式序列

序列可以使用一種程序結構來表示,它不將每個元素顯式儲存在內存中,這是高效處理有序數據的核心概念。為了將這個概念用於實踐,我們需要構造對象來提供序列中所有元素的訪問,但是不要事先把所有元素計算出來並儲存。

這個概念的一個簡單示例就是第二章出現的range序列類型。range表示連續有界的整數序列。但是,它的每個元素並不顯式在內存中表示,當元素從range中獲取時,才被計算出來。所以,我們可以表示非常大的整數範圍。只有範圍的結束位置才被儲存為range對象的一部分,元素都被憑空計算出來。

>>> r = range(10000, 1000000000)
>>> r[45006230]
45016230

這個例子中,當構造範圍示例時,並不是這個範圍內的所有 999,990,000 個整數都被儲存。反之,範圍對象將第一個元素 10,000 與下標相加 45,006,230 來產生第 45,016,230 個元素。計算所求的元素值並不從現有的表示中獲取,這是惰性計算的一個例子。計算機科學將惰性作為一種重要的計算工具加以讚揚。

迭代器是提供底層有序數據集的有序訪問的對象。迭代器在許多編程語言中都是內建對象,包括 Python。迭代器抽象擁有兩個組成部分:一種獲取底層元素序列的下一個元素的機制,以及一種標識元素序列已經到達末尾,沒有更多剩餘元素的機制。在帶有內建對象系統的編程語言中,這個抽象通常相當於可以由類實現的特定接口。Python 的迭代器接口會在下一節中描述。

迭代器的實用性來源於一個事實,底層數據序列並不能顯式在內存中表達。迭代器提供了一種機制,可以依次計算序列中的每個值,但是所有元素不需要連續儲存。反之,當下個元素從迭代器獲取的時候,這個元素會按照請求計算,而不是從現有的內存來源中獲取。

範圍可以惰性計算序列中的元素,因為序列的表示是統一的,並且任何元素都可以輕易從範圍的起始和結束位置計算出來。迭代器支持更廣泛的底層有序數據集的惰性生成,因為它們不需要提供底層序列任意元素的訪問途徑。反之,它們僅僅需要按照順序,在每次其它元素被請求的時候,計算出序列的下一個元素。雖然不像序列可訪問任意元素那樣靈活(叫做隨機訪問),有序數據序列的順序訪問對於數據處理應用來說已經足夠了。

5.2.1 Python 迭代器

Python 迭代器接口包含兩個消息。__next__消息向迭代器獲取所表示的底層序列的下一個元素。為了對__next__方法調用做出回應,迭代器可以執行任何計算來獲取或計算底層數據序列的下一個元素。__next__的調用讓迭代器產生變化:它們向前移動迭代器的位置。所以多次調用__next__會有序返回底層序列的元素。在__next__的調用過程中,Python 通過StopIteration異常,來表示底層數據序列已經到達末尾。

下面的Letters類迭代了從ad字母的底層序列。成員變量current儲存了序列中的當前字母。__next__方法返回這個字母,並且使用它來計算current的新值。

>>> class Letters(object):
        def __init__(self):
            self.current = 'a'
        def __next__(self):
            if self.current > 'd':
                raise StopIteration
            result = self.current
            self.current = chr(ord(result)+1)
            return result
        def __iter__(self):
            return self

__iter__消息是 Python 迭代器所需的第二個消息。它只是簡單返回迭代器,它對於提供迭代器和序列的通用接口很有用,在下一節會描述。

使用這個類,我們就可以訪問序列中的字母:

>>> letters = Letters()
>>> letters.__next__()
'a'
>>> letters.__next__()
'b'
>>> letters.__next__()
'c'
>>> letters.__next__()
'd'
>>> letters.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 12, in next
StopIteration

Letters示例只能迭代一次。一旦__next__()方法產生了StopIteration異常,它就從此之後一直這樣了。除非創建新的實例,否則沒有辦法來重置它。

迭代器也允許我們表示無限序列,通過實現永遠不會產生StopIteration異常的__next__方法。例如,下面展示的Positives類迭代了正整數的無限序列:

>>> class Positives(object):
        def __init__(self):
            self.current = 0;
        def __next__(self):
            result = self.current
            self.current += 1
            return result
        def __iter__(self):
            return self

5.2.2 for語句

Python 中,序列可以通過實現__iter__消息用於迭代。如果一個對象表示有序數據,它可以在for語句中用作可迭代對象,通過回應__iter__消息來返回迭代器。這個迭代器應擁有__next__()方法,依次返回序列中的每個元素,最後到達序列末尾時產生StopIteration異常。

>>> counts = [1, 2, 3]
>>> for item in counts:
        print(item)
1
2
3

在上面的實例中,counts列表返回了迭代器,作為__iter__()方法調用的回應。for語句之後反覆調用迭代器的__next__()方法,並且每次都將返回值賦給item。這個過程一直持續,直到迭代器產生了StopIteration異常,這時for語句就終止了。

使用我們關於迭代器的知識,我們可以拿while、賦值和try語句實現for語句的求值規則:

>>> i = counts.__iter__()
>>> try:
        while True:
            item = i.__next__()
            print(item)
    except StopIteration:
        pass
1
2
3

在上面,調用counts__iter__方法所返回的迭代器綁定到了名稱i上面,便於依次獲取每個元素。StopIteration異常的處理子句不做任何事情,但是這個異常的處理提供了退出while循環的控制機制。

5.2.3 生成器和yield語句

上面的LettersPositives對象需要我們引入一種新的字段,self.current,來跟蹤序列的處理過程。在上面所示的簡單序列中,這可以輕易實現。但對於複雜序列,__next__()很難在計算中節省空間。生成器允許我們通過利用 Python 解釋器的特性定義更復雜的迭代。

生成器是由一類特殊函數,叫做生成器函數返回的迭代器。生成器函數不同於普通的函數,因為它不在函數體中包含return語句,而是使用yield語句來返回序列中的元素。

生成器不使用任何對象屬性來跟蹤序列的處理過程。它們控制生成器函數的執行,每次__next__方法調用時,它們執行到下一個yield語句。Letters迭代可以使用生成器函數實現得更加簡潔。

>>> def letters_generator():
        current = 'a'
        while current <= 'd':
            yield current
            current = chr(ord(current)+1)
>>> for letter in letters_generator():
        print(letter)
a
b
c
d

即使我們永不顯式定義__iter__()__next__()方法,Python 會理解當我們使用yield語句時,我們打算定義生成器函數。調用時,生成器函數並不返回特定的產出值,而是返回一個生成器(一種迭代器),它自己就可以返回產出的值。生成器對象擁有__iter____next__方法,每個對__next__的調用都會從上次停留的地方繼續執行生成器函數,直到另一個yield語句執行的地方。

__next__第一次調用時,程序從letters_generator的函數體一直執行到進入yield語句。之後,它暫停並返回current值。yield語句並不破壞新創建的環境,而是為之後的使用保留了它。當__next__再次調用時,執行在它停留的地方恢復。letters_generator作用域中current和任何所綁定名稱的值都會在隨後的__next__調用中保留。

我們可以通過手動調用__next__()來遍歷生成器:

>>> letters = letters_generator()
>>> type(letters)
<class 'generator'>
>>> letters.__next__()
'a'
>>> letters.__next__()
'b'
>>> letters.__next__()
'c'
>>> letters.__next__()
'd'
>>> letters.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

在第一次__next__()調用之前,生成器並不會開始執行任何生成器函數體中的語句。

5.2.4 可迭代對象

Python 中,迭代只會遍歷一次底層序列的元素。在遍歷之後,迭代器在__next__()調用時會產生StopIteration異常。許多應用需要迭代多次元素。例如,我們需要對一個列表迭代多次來枚舉所有的元素偶對:

>>> def all_pairs(s):
        for item1 in s:
            for item2 in s:
                yield (item1, item2)
>>> list(all_pairs([1, 2, 3]))
[(1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3), (3, 1), (3, 2), (3, 3)]

序列本身不是迭代器,但是它是可迭代對象。Python 的可迭代接口只包含一個消息,__iter__,返回一個迭代器。Python 中內建的序列類型在__iter__方法調用時,返回迭代器的新實例。如果一個可迭代對象在每次調用__iter__時返回了迭代器的新實例,那麼它就能被迭代多次。

新的可迭代類可以通過實現可迭代接口來定義。例如,下面的可迭代對象LetterIterable類在每次調用__iter__時返回新的迭代器來迭代字母。

>>> class LetterIterable(object):
        def __iter__(self):
            current = 'a'
            while current <= 'd':
                yield current
                current = chr(ord(current)+1)

__iter__方法是個生成器函數,它返回一個生成器對象,產出從'a''d'的字母。

Letters迭代器對象在單次迭代之後就被“用完”了,但是LetterIterable對象可被迭代多次。所以,LetterIterable示例可以用於all_pairs的參數。

>>> letters = LetterIterable()
>>> all_pairs(letters).__next__()
('a', 'a')

5.2.5 流

流提供了一種隱式表示有序數據的最終方式。流是惰性計算的遞歸列表。就像第三章的Rlist類那樣,Stream實例可以響應對其第一個元素和剩餘部分的獲取請求。同樣,Stream的剩餘部分還是Stream。然而不像RList,流的剩餘部分只在查找時被計算,而不是事先存儲。也就是說流的剩餘部分是惰性計算的。

為了完成這個惰性求值,流會儲存計算剩餘部分的函數。無論這個函數在什麼時候調用,它的返回值都作為流的一部分,儲存在叫做_rest的屬性中。下劃線表示它不應直接訪問。可訪問的屬性rest是個方法,它返回流的剩餘部分,並在必要時計算它。使用這個設計,流可以儲存計算剩餘部分的方式,而不用總是顯式儲存它們。

>>> class Stream(object):
        """A lazily computed recursive list."""
        def __init__(self, first, compute_rest, empty=False):
            self.first = first
            self._compute_rest = compute_rest
            self.empty = empty
            self._rest = None
            self._computed = False
        @property
        def rest(self):
            """Return the rest of the stream, computing it if necessary."""
            assert not self.empty, 'Empty streams have no rest.'
            if not self._computed:
                self._rest = self._compute_rest()
                self._computed = True
            return self._rest
        def __repr__(self):
            if self.empty:
                return '<empty stream>'
            return 'Stream({0}, <compute_rest>)'.format(repr(self.first))
>>> Stream.empty = Stream(None, None, True)

遞歸列表可使用嵌套表達式來定義。例如,我們可以創建RList,來表達15的序列,像下面這樣:

>>> r = Rlist(1, Rlist(2+3, Rlist.empty))

與之類似,我們可以創建一個Stream來表示相同序列。Stream在請求剩餘部分之前,並不會實際計算下一個元素5

>>> s = Stream(1, lambda: Stream(2+3, lambda: Stream.empty))

這裡,1是流的第一個元素,後面的lambda表達式是用於計算流的剩餘部分的函數。被計算的流的第二個元素又是一個返回空流的函數。

訪問遞歸列表r和流s中的元素擁有相似的過程。但是,5儲存在了r之中,而對於s來說,它在首次被請求時通過加法來按要求計算。

>>> r.first
1
>>> s.first
1
>>> r.rest.first
5
>>> s.rest.first
5
>>> r.rest
Rlist(5)
>>> s.rest
Stream(5, <compute_rest>)

雖然 rrest 是一個單元素遞歸列表,但 s 的其餘部分包括一個計算其餘部分的函數;它將返回空流的事實可能還沒有被發現。

當構造一個 Stream 實例時,字段 self._computedFalse ,表示 Stream_rest 還沒有被計算。當通過點表達式請求 rest 屬性時,會調用 rest 方法,以 self._rest = self.compute_rest 觸發計算。由於 Stream 中的緩存機制,compute_rest 函數只被調用一次。

compute_rest 函數的基本屬性是它不接受任何參數,並返回一個 Stream

惰性求值使我們能夠用流來表示無限的順序數據集。例如,我們可以從任意 first 開始表示遞增的整數。

>>> def make_integer_stream(first=1):
      def compute_rest():
        return make_integer_stream(first+1)
      return Stream(first, compute_rest)
>>> ints = make_integer_stream()
>>> ints
Stream(1, <compute_rest>)
>>> ints.first
1

make_integer_stream首次被調用時,它返回了一個流,流的first是序列中第一個整數(默認為1)。但是,make_integer_stream實際是遞歸的,因為這個流的compute_rest以自增的參數再次調用了make_integer_stream。這會讓make_integer_stream變成遞歸的,同時也是惰性的。

>>> ints.first
1
>>> ints.rest.first
2
>>> ints.rest.rest
Stream(3, <compute_rest>)

無論何時請求整數流的rest,都僅僅遞歸調用make_integer_stream

操作序列的相同高階函數 -- mapfilter -- 同樣可應用於流,雖然它們的實現必須修改來惰性調用它們的參數函數。map_stream在一個流上映射函數,這會產生一個新的流。局部定義的compute_rest函數確保了無論什麼時候rest被計算出來,這個函數都會在流的剩餘部分上映射。

>>> def map_stream(fn, s):
        if s.empty:
            return s
        def compute_rest():
            return map_stream(fn, s.rest)
        return Stream(fn(s.first), compute_rest)

流可以通過定義compute_rest函數來過濾,這個函數在流的剩餘部分上調用過濾器函數。如果過濾器函數拒絕了流的第一個元素,剩餘部分會立即計算出來。因為filter_stream是遞歸的,剩餘部分可能會多次計算直到找到了有效的first元素。

>>> def filter_stream(fn, s):
        if s.empty:
            return s
        def compute_rest():
            return filter_stream(fn, s.rest)
        if fn(s.first):
            return Stream(s.first, compute_rest)
        return compute_rest()

map_streamfilter_stream展示了流式處理的常見模式:無論流的剩餘部分何時被計算,局部定義的compute_rest函數都會對流的剩餘部分遞歸調用某個處理函數。

為了觀察流的內容,我們需要將其截斷為有限長度,並轉換為 Python list

>>> def truncate_stream(s, k):
        if s.empty or k == 0:
            return Stream.empty
        def compute_rest():
            return truncate_stream(s.rest, k-1)
        return Stream(s.first, compute_rest)
>>> def stream_to_list(s):
        r = []
        while not s.empty:
            r.append(s.first)
            s = s.rest
        return r

這些便利的函數允許我們驗證map_stream的實現,使用一個非常簡單的例子,從37的整數平方。

>>> s = make_integer_stream(3)
>>> s
Stream(3, <compute_rest>)
>>> m = map_stream(lambda x: x*x, s)
>>> m
Stream(9, <compute_rest>)
>>> stream_to_list(truncate_stream(m, 5))
[9, 16, 25, 36, 49]

我們可以使用我們的filter_stream函數來定義素數流,使用埃拉託斯特尼篩法(sieve of Eratosthenes),它對整數流進行過濾,移除第一個元素的所有倍數數值。通過成功過濾出每個素數,所有合數都從流中移除了。

>>> def primes(pos_stream):
        def not_divible(x):
            return x % pos_stream.first != 0
        def compute_rest():
            return primes(filter_stream(not_divible, pos_stream.rest))
        return Stream(pos_stream.first, compute_rest)

通過截斷primes流,我們可以枚舉素數的任意前綴:

>>> p1 = primes(make_integer_stream(2))
>>> stream_to_list(truncate_stream(p1, 7))
[2, 3, 5, 7, 11, 13, 17]

流和迭代器不同,因為它們可以多次傳遞給純函數,並且每次都產生相同的值。素數流並沒有在轉換為列表之後“用完”。也就是說,在將流的前綴轉換為列表之後,p1的第一個元素仍舊是2

>>> p1.first
2

就像遞歸列表提供了序列抽象的簡單實現,流提供了簡單、函數式的遞歸數據結構,它通過高階函數的使用實現了惰性求值。