1. 程式人生 > >Python中iteration(迭代)、iterator(迭代器)、generator(生成器)等相關概念的理解

Python中iteration(迭代)、iterator(迭代器)、generator(生成器)等相關概念的理解

在閱讀Python tutorial類這一章的時候出現了iterator的概念,我是一個是程式設計的半吊子,雖然在其它語言(比如Java和C++)中也聽過這個概念,但是一直沒認真的去理解,這次我參考了一些文章,總結了一些我的看法。
首先,我在理解相關的概念的時候總是試圖探索引入相關概念的背後的真正意圖,我們看到的多半都是用法,那麼為什麼要這麼用,也許搞清楚了每件事情背後的目的,接下來產生的解決方案才能順理成章水到渠成。那麼這篇文章大多數是我通過現有的一些線索,推測出背後的一些可能的,也許我的理解充滿各種主觀因素,但是至少能夠自圓其說,也請各位能不吝賜教。其次,其中有些表述可能會有些囉嗦。

1. “迭代”這個概念引入主要是解決什麼問題的

首先你要知道什麼叫做迭代

迭代就是單向地、逐個地訪問某個容器中的元素的行為。 (可以理解為線性的方式訪問容器中元素,單向、逐個是其特徵)
在程式實現中我們最常進行的一種操作就是將容器(以下認為“資料結構”和“容器”是同義詞)裡元素一個接一個的取出來,但是為了實現這個簡單的目的,針對不同的資料結構,我每次寫的程式碼還都不一樣(這背後其實是要了解每個資料結構的特性,並根據這些特性構造合適的程式碼),太麻煩了,於是我們想能不能這樣?能不能寫一個工具,每次我們需要在某個資料結構上進行迭代操作的時候,就呼叫這個工具,這個工具可以我們把不同資料結構在方法實現細節上的不同遮蔽掉,這個工具就是迭代器,在python中是由類實現的。
迭代器是是一個抽象的概念,它代表了一種目的,而並非細節。所有實現了在某個特定資料結構上進行迭代行為的類都是迭代器。

延展概念

迭代是遍歷的一種特例,遍歷(traverse)是可以在資料結構上來回的遊走,不僅可以往前,還可以往後,同時還能保證不重不漏的,也就是把非線性的東西對映成線性的訪問方式,而且還是不重不漏的,迭代是單向的而且只來一次。

2. 在這個程式語言中,這個概念是如何實現的

我們來看看這個東西在語言設計的層面是如何實現的:
我們假設,在理想的情況下,如果有一個模組或者一個函式裡面實現了所有資料型別的迭代方式,假設這個模組叫iterator,裡面有一個方法叫做iteration,那麼每次我用的時候,先import iterator,然後用iterator.iteration(帶迭代的變數)實現了一次迭代,這是比較理想的方式,但是現實中很難做到這點,除了系統內建的資料結構之外,使用者自己實現的資料結構咋辦,於是放棄這種大一統的思路,而把所有的實現都下放給使用者自己實現。在這種情況下,為了保證每個使用者寫的迭代器能夠被其它使用者使用,需要制定一些規範讓大家遵守,我斗膽把這個規範稱之為“迭代規範”吧,這個規範可以分成兩個層面來理解,一個是使用層面,一個是實現層面。

  • 首先我們來看使用層面,在python中迭代器的使用是這麼一個套路:

    • 第一步,由待迭代的容器變數建立一個對應的迭代器。
      在python中,有一個內建的函式iter(),這個函式以待迭代的容器變數為引數,創建出對應於這種容器的迭代器。
    • 第二步,呼叫迭代器的next方法,每一次呼叫next方法只會得到一個元素。
      在python中,迭代器裡面有一個next()方法,我們可以直接呼叫這個方法,但是常用的方法是利用python的內建函式next(),這個函式以迭代器為引數,相當於呼叫了迭代器的next()方法,簡單一點。
  • 然後是實現層面,也分成兩步:

    • 第一步,我稱之為“資料的可迭代宣告”(或者叫做“可迭代實現”)
      你必須要證明你的資料結構是可迭代(iteratable)的,從觀念上或者數學上看你的資料結構裡的資料必須是有限可列或者至少是可列的(可列的概念其實是有理數或者無理數中用到的概念)。只有資料結構可迭代,才有為這個資料結構構造迭代器的意義。在具體的實現層面,一個可迭代的資料結構要滿足下面的要求(這個資料結構一般來說是一個類),必須實現iter方法,這個方法需要返回一個迭代器。
    • 第二步,叫做“迭代器的實現”
      實現iterator需要實現兩個方法,這兩個方法一起被稱之為迭代器協議(iterator protocol)。
      • 第一個方法是iter,這個方法返回的是實現迭代器的這個類自身,你會奇怪,實現了iter方法的不是說明這個類是可迭代的嗎?是的,迭代器一般來說也是可迭代的。
      • 第二個方法是next方法,這個方法返回的是容器中的下個元素,如果沒有更多的元素了,則會raise一個StopIteration異常。

迭代器協議的要點和難點在next方法的實現上(這為生成器的引入埋下了伏筆),而在next方法的實現有三個要點,其中最難的一點在於理解“資料現用現生成”或者說“用到某個元素的時候才把它生成(計算)出來”的思想。
這裡就要引入另一個問題了,我斗膽把它稱之為“資料的準備問題”
啥意思呢?比方說在程式中,在用一個數據之前我們首先得“有”一個數據,那麼我們如何“有”一個數據?
兩種方法,一種方式是我把所有可能要用到元素都生成出來並且全部儲存在記憶體中然後再在使用的過程中去拿我要用的元素;還有一種是我用某個元素的前一刻時才去生成元素,然後再去用。你會覺得,我靠這還算一個問題麼,必須後一種啊,前一種明顯是在浪費空間麼(參考firstn的例子)。很不幸的時候,在python2有很多底層的資料實現就是沒有效率,你單看他們的時候都沒有問題,但是一旦程式碼的規模變大,層層呼叫之後就不好說了。好,我們統一觀點之後,再討論下一個問題。
在使用資料的前一刻把資料生成可能嗎?
其實是可能的,這也分兩種可能,一種可能是我們要自己“憑空創造”資料,其實說“憑空創造”其實不準確,因為本質上任何可列的資料都是自然數的函式(這個函式是數學意義上的函式),只要有函式(對映法則)以及自變數,求出因變數不是很簡單的事情嗎?還有一種可能就是資料原來就存在了(比如說存在資料庫裡或者檔案裡),我們只需要取出來就可以,那麼在這種情況下我們只要獲取儲存資料的位置資訊就能獲取到實際的資料值了,其實也可以理解為建立自然數和位置資訊的對映。理解了這點,另外兩個實現細節就好說了,比如我們經常會用(不是絕對)一個遊標來記錄位置資訊,可能這個遊標是一個計數器,然後我們利用迴圈的方式讓其自增從而實現迭代特性中的“單向”,“逐個”特徵。再有就是最後一定要返回StopIteration。小結一下next方法的要點就是:
1. 資料現用現生成
2. 單向,逐個特性的實現
3. 最後返回StopIteration

在語法上,返回StopIteration比其它兩點都重要,next方法啥也不做,直接返回StopIteration都行。例如下面這個例子

>>> class Simplest_Iterator(object):
...     def next(self): # Python 2 compatibility
...         return self.__next__()
...     def __next__(self):
...         raise StopIteration
...
>>> z = Simplest_Iterator()
>>> next(z)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in __next__
StopIteration

值得注意的是,依據在“資料的可迭代宣告”這一步中iter方法返回的容器物件是自身還是其它物件的不同,迭代器在使用上會表現出不同的特性。
1. iter方法返回的是容器物件本身。這種實現方式叫做“容器本身既是迭代器”,這種情況迭代器構造出來之後只能使用一次。
2. iter方法返回的是其他的物件。這種實現方式叫做“資料和迭代器的分離”,其實這個更像是工具的思想,我第一次閱讀與迭代器實現相關的文件時腦海裡首先想到是這種方法。

那麼這兩種方式有什麼區別呢?在Python中,大多數容器型別(哪些屬於容器型別,參考這篇文章)都是採用第二種方式實現的,都是採用“資料和迭代器分離”的實現的方式的,這是因為“可迭代”其實從觀念上意味著,我在這個種容器上反覆進行迭代的,如果“容器本身即是迭代器”的話,每次容器每次呼叫iter方法返回的是自身,在經過一次迭代之後,遊標指到容器最後一個元素後面,同時丟擲了異常,迭代器就再也無法工作了,除非你利用一些方法把next方法中的遊標置為初始狀態。而我們採用資料和迭代器分離的方式話,每次容器呼叫iter方法的時候,都會重新生成一個新的迭代器物件,這樣就可以無限次的在容器上進行迭代了。

資料和迭代器分離的實現方式示意圖

資料即使迭代器的實現方式示意圖

下面用例子來說明,我們實現一個與內建函式xrange的類似的類
實現方式1:“容器本身即是迭代器”

class yrange:
    def __init__(self, n):
        self.i = 0
        self.n = n

    def __iter__(self):
        return self

    def __next__(self):
        if self.i < self.n:
            i = self.i
            self.i += 1
            return i
        else:
            raise StopIteration()

之後我們來做一下測試:

>>> y = yrange(3)
>>> y.next()
0
>>> y.next()
1
>>> y.next()
2
>>> y.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 14, in next
StopIteration

實現方式2. 資料和迭代器的分離的例子:

class zrange:
    def __init__(self, n):
        self.n = n

    def __iter__(self):
        return zrange_iter(self.n)

class zrange_iter:
    def __init__(self, n):
        self.i = 0
        self.n = n

    def __iter__(self):
        # Iterators are iterables too.
        # Adding this functions to make them so.
        return self

    def __next__(self):
        if self.i < self.n:
            i = self.i
            self.i += 1
            return i
        else:
            raise StopIteration()

然後我們比較一下兩個類的不同

>>> y = yrange(5)
>>> list(y)
[0, 1, 2, 3, 4]
>>> list(y)
[]
>>> z = zrange(5)
>>> list(z)
[0, 1, 2, 3, 4]
>>> list(z)
[0, 1, 2, 3, 4]

3. 這些概念有什麼用?

  1. 毫無疑問第一個應用就是做遍歷
    學過資料結構的都知道所有操作的基礎都是基於遍歷,如果解決了遍歷的問題,就等於說解決了一大半的問題
    甚至檔案都可以進行迭代哦

  2. 可以用來生成資料(我估計生成器的名字就是這樣來的吧)
    在迭代器實現的核心思想就是“資料用到的時候才生成”,那麼我們是不是可以用這個方式來生成資料?而且這種實現方式意外的節省空間?這點正好引入下一個要討論的知識點,也就是生成器
    最有說服力的是那個斐波那契數列的例子

4. 生成器

簡單來說,生成器就是迭代器實現的簡化。相比用一個實現迭代器協議的類來實現迭代器而言,我們只要定義一個函式就可以實現迭代器,這個函式就是生成器。
那麼這個函式如何定義呢?我們以yrange迭代器為例,生成器實現如下:

def yrange(n):
    i = 0
    while i < n:
        yield i
        i += 1

怎麼理解這個程式碼,有很多人說這個東西簡單,但是我不這麼覺得,其實越是看似簡單的東西里面越是有深刻的知識在裡面。就像我在next方法實現那部分中對“在使用資料的前一刻把資料生成可能嗎?”這個問題進行的討論一樣,對於資料的生成,除了在已經儲存資料的地方進行獲取以外,還有一種方式是“把它計算出來”,而計算的方法,本質上是建立一個從自然數序列到我們所要的序列的一個對映。對於後一種方式來說,我們要做的事情就是,遍歷自然數序列,對於每個自然數,把它作為對映法則的因變數放入到對映法則中去進行運算,把運算出來的結果返回出去。比如對映關係是f(x),那麼這個程式碼可以這麼寫:

def generatorexample(n):
    i = 0
    while i < n:
        yield f(i)
        i += 1

也就說把計算的結果(在程式中就是一個表示式)返回出去的那步用yield關鍵字來返回就行了。

那麼這個背後實現機制是怎麼樣的,
1. 當generator function被呼叫的時候,這個函式會返回一個generator物件之後什麼都不做。
2. 當next方法被呼叫的時候,函式就會開始執行直到yield所在的位置,計算出來的值在這個位置被返回,之後這個函式就停下了。之後再呼叫next方法的時候,函式繼續執行,直到遇到下一個yield。
3. 如果執行完的程式碼,還沒有遇到yield,就會丟擲StopIteration異常。

>>> def foo():
...     print "begin"
...     for i in range(3):
...         print "before yield", i
...         yield i
...         print "after yield", i
...     print "end"
...
>>> f = foo()
>>> f.next()
begin
before yield 0
0
>>> f.next()
after yield 0
before yield 1
1
>>> f.next()
after yield 1
before yield 2
2
>>> f.next()
after yield 2
end
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>>

再比如:

def my_generator():
    print("return first value")
    yield 1
    print("return second value")
    yield 2
    print("return last value")
    yield 3
    print("raise StopIteration")

z = my_generator()
next(z)
next(z)
next(z)
next(z)

這裡面有一些地方一下說明一下,其實如果深究起“generator”來,其實這還是一個非常模糊的詞呢,generator到底是說那個函式呢?還是說這個函式返回的值呢?
在官方的glossary裡面,generator就指得是那個包含yield語句的函式體,也就是“generator function”,而這個如果要指代這個函式返回的物件,一般稱之為“generator iterator”,官方建議為了避免歧義,最好把詞說全一點。
而我還是喜歡這篇文章的描述,“generator function ”就是那個函式體,“generator”表示“generator function”這個函式返回的物件。

5. 生成器表示式

生成器表示式可以看成迭代器在生成器的基礎上進一步簡化(還能在懶一點嗎),用好理解的話說就是——生成器表示式可以看成列表推導式的生成器版。列表推導是可以看我的另一篇文章
雖所簡化了形式,但是我感覺更接近迭代器的本質了——也就是“構造一個和自然數序列一一對應的序列”

6. 總結一下這幾個概念的關係

借用一張圖
這裡寫圖片描述

7.迭代思想在Python中的廣泛存在

在tutorial裡面有這麼一句話The use of iterators pervades and unifies Python.
基本上來說迭代的思想在Python這門語言的實現過程中已經滲透在各個角落,已經是底層的設計思想了,很多語法都是基於迭代這個概念向上建造的。以下是一些例子

  1. 很多容器型別都是iterable
    甚至檔案型別都是可以用for語句來訪問的。我們最常用的一個數據結構list,它是用iterable作為引數來初始化一個list,其實執行了這樣的初始化函式

    class list(object):
    ...
    def __init__(self, iterable):
        for i in iterable:
            self.value.append(i)
    ...
  2. 很多函式的引數以及返回值都是iterable
    map(), filter() ,zip() ,range()
    dict.keys(), dict.items() 和 dict.values()

  3. for其實也是語法糖
    基於迭代的語法糖
    比如:

    for i in iterable:
    func(i)

    本質上是:

    z = iter(iterable)
    try:
    while True:
        func(next(z))
    except StopIteration:
    pass
  4. unpack也是語法糖
    比如:

    >>> a,b = b,a # 等價於 a,b = (b,a)
    >>> a,b,*_ = [1,2.3,4] # 僅適用於 Python 3
    >>> a,b,*_ = iter([1,2.3,4]) # 也可以用於迭代器
    >>> a
    1
    >>> b
    2.3
    >>> _
    [4]

    其實是賦值是這麼實現的:

    k = iter(iterable)
    a = next(k)
    b = next(k)
    _ = list(k)
  5. list comperhension也是語法糖
    上面雖然說generator experssion是生成器版本的list comperhension,這只是為了便於理解,其實先後順序應該顛倒過來。
    List Comprehension 也只是語法糖而已,甚至還可以寫出 tuple/set/dict comprehension(其實 set 就是所有 key 的 value 都為 None 的 dict)

    >>> [x*x for x in range(10)]
    [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
    >>> list(x*x for x in range(10))
    [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
    >>> tuple(x*x for x in range(10))
    (0, 1, 4, 9, 16, 25, 36, 49, 64, 81)
    >>> set(x*x for x in range(10))
    {0, 1, 64, 4, 36, 9, 16, 49, 81, 25}
    >>> dict((x,x*x) for x in range(10))
    {0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}

參考文獻