博客 / 詳情

返回

一篇夯實一個知識點系列--python生成器

寫在前面

本系列目的:一篇文章,不求鞭辟入裏,但使得心應手。
  • 迭代是數據處理的基石,在掃描內存無法裝載的數據集時,我們需要一種惰性獲取數據的能力(即一次獲取一部分數據到內存)。在Python中,具有這種能力的對象就是迭代器。生成器是迭代器的一種特殊表現形式。

    • 個人認為生成器是Python中最有用的高級特性之一(甚至沒有之一)。雖然初級編碼中使用寥寥,但隨着學習深入,會發現生成器是協程,異步等高級知識的基石。Python最有野心的asyncio庫,就是用協程砌造的。

      注:生成器和協程本質相同。PEP342(Python增強提案)增加了生成器的send()方法,使其變身為協程。如此之後,生成器生成數據,協程消費數據。雖然本質相同,但是由於從理念上説協程跟迭代沒有關係,並且糾纏生成器和協程的區別與聯繫會引爆自己的大腦,所以應該將這兩個概念區分。此處説本質相同意為:理解生成器原理之後,理解增加了send方法,但是實現方式幾乎相同的協程會更加輕鬆(這段話看不懂沒有關係,船到橋頭自然直,學到協程自然懂)。
  • Python的一致性是其最迷人的地方。瞭解了Python生成器,迭代器的實現。就會對Python的一致性設計有更加強烈的感知。本文讀完之後,遇到面試官提問為什麼列表可以迭代,字典可以迭代,甚至文本文件都可以迭代時,你就可以穩(huang)得一批。
  • 閲讀本文之前,如果你對Python的一致性有一些瞭解,如鴨子類型,或者Cpython的PyObject結構體,那真是太棒了。不過鑑於筆者深厚的文字功底,沒有這些知識也不打緊。

乾貨兒

  • 迭代器

    在學習生成器之前,先要了解迭代器。顧名思義,迭代器即具有迭代功能的對象。在Python中,可以認為迭代器可以通過不斷迭代,產生出一個又一個的對象。
    • 可迭代對象和迭代器

      Python的一致性是靠協議支撐的。一個對象只要遵循以下協議,它就是一個可迭代對象或迭代器。
      • Python中的一個對象,如果實現了iter方法,並且iter方法返回一個迭代器,那麼它就是可迭代對象。如果實現了iter和next方法,並且iter方法返回一個迭代器,那麼它就是迭代器(有點繞,按住不表,繼續學習)。

        注:如果對象實現了__getitem__方法,並且索引從0開始,那麼也是可迭代對象。此hack為兼容性考慮。只需切記,如果你要實現可迭代對象和可迭代器,那麼請遵循以上協議。
      • 可迭代對象的iter返回迭代器,迭代器的iter方法返回自身(也是迭代器),迭代器的next方法實現迭代功能,不斷返回下一個元素,或者在元素為空時raise一個StopIteration終止迭代。
    • 可迭代對象與迭代器的關係

      話不多説,上代碼。

      class Iterable:
          def __init__(self, *args):
              self.items = args
      
          def __iter__(self):
              return Iterator(self.items)       
      
      class Iterator:
          def __init__(self, items):
              self.items = items
              self.index = 0
      
          def __iter__(self):
              return self                       
      
          def __next__(self):                
              try:
                  item = self.items[self.index]
              except IndexError:
                  raise StopIteration()
              self.index += 1
              return item
      
      ins = Iterable(1,2,3,4,5)        # 1
      for i in ins:
          print(i)
      print('the end...')
      >>>                                              # 2
      1
      2
      3
      4
      5
      the end ...
      • 上述代碼中,實現了可迭代對象Iterable和迭代器Iterator。遵循協議規定,Iterable實現了iter方法,且iter方法返回迭代器Iterator實例,迭代器實現了iter方法和next方法,iter返回自身(即sel,迭代器本身f),next方法返回迭代器中的元素或者引發StopIteration異常。運行上述代碼,會看到#2處的輸出。
      • 通過上述代碼迭代一個對象顯得十分囉嗦。比如在Iterable中,iter必須要返回一個迭代器。為什麼不能直接用Iterator迭代元素呢?假設我們通過迭代器來迭代元素,將上述代碼中的#1處如下代碼:

        ins = Iterator([1,2,3,4,5])
        for i in ins:                                                        # 3
            print(i)
        for i in ins:                                                        # 4
            print(i)
        next(ins)                                                            # 5
        print('the end...')
        >>>                                                                      # 6
        1
        2
        3
        4
        5
        ...
        File "/home/disk/test/a.py", line 20, in __next__        # 7
            raise StopIteration()
        the end...

        運行上述代碼,會看到#6處的輸出。疑惑的是,#3和#4處運行了兩次for循環,結果只打印一遍所有元素。解釋如下:

        • 上述代碼中,ins是一個Iterator迭代器對象。那麼ins符合迭代器協議:每次調用next,會返回下一個元素,直到迭代器元素為空,raise一個StopIteration異常。
        • #3處第一次通過for循環迭代ins,相當於不斷調用ins的next方法,不斷返回下一個元素,輸出如#6所示。當元素為空時,迭代器raise了StopIterator。而這個異常會被for循環捕獲,不會暴露給用户,所以我們就認為數據迭代完成,並且沒有出現異常。
        • 迭代器ins內的元素已經被#3處的for循環消耗完,並且raise了StopIteration(只不過被for循環捕獲靜默處理,沒有暴露給用户)。此時ins已經是元素消耗殆盡的“空”狀態。在#4處第二次通過for循環迭代ins,因為ins內的元素為空,繼續調用ins的next方法,那麼還是會raise一個StopIteration,而且又被for循環靜默處理,所以沒有異常,也沒有輸出。
        • 接下來,#5處通過next方法獲取ins的下一個元素,同上,繼續raise一個StopIteration異常。由於此處通過next調用而不是for循環,異常不會被處理,所以拋出到用户層面,即#7輸出。
        • 重新編寫上述代碼中#3處for循環和#4處for循環,可以看到對應輸出驗證了我們的結論。第一次for循環在迭代到元素為2時跳出循環,第二次for循環繼續迭代同一個迭代器,那麼會繼續上次迭代器結束位置繼續迭代元素。代碼如下:

          ins = Iterator([1,2,3,4,5])
          print('the first for:')
          for i in ins:                                    # 3  the first for
            print(i)
            if i == 2:
                break
          print('the second for:')
          for i in ins:                                   # 4 the second for
                print(i)
          print('the end...')
          >>>                                                # the output
          the first for:
          1
          2
          the second for:
          3
          4
          5
          the end...

          所以我們可以得到如下結論:

          • 一個迭代器對象只能迭代一遍。多次迭代,相當於不停對一個空迭代器調用next方法,會不停raise StopIteration異常。
          • 由於迭代器實現了iter方法,並且iter方法返回了迭代器,那麼迭代器也是一個可迭代對象(廢話,不是可迭代對象,上述代碼中如何可以用for循環迭代呢)
          • 綜上來説,可迭代對象和迭代器明顯是一個多態的問題。迭代器是一個可迭代對象,可以迭代返回元素,由於iter返回self(即自身實例),所以只能迭代一遍,迭代到末尾就會拋出異常。而每次迭代可迭代對象,iter都會返回一個新的迭代器實例。所以可迭代對象是支持多次迭代的。比如l=[i for i in range(10)]生成的list對象就是一個可迭代對象,可以被多次迭代。l=(i for i in range(10))生成的是一個迭代器,只能被迭代一遍。
    • 迭代器支持

      引用流暢的Python中的原話,迭代器支持以下6個功能。由於篇幅所限,點到為止。大家只要理解了迭代器的原理,理解以下功能自然是水到渠成。
      • for循環

        上述代碼已經有舉例,可參考

      • 構建和擴展集合類型

        from collections improt abc
        
        class NewIterator(abc.Iterator):
            pass                                                    # 放飛自我,實現新的類型
      • 列表推導,字典推導和集合推導

        l = [i for i in range(10)]            # list
        d = {i:i for i in range(10)}      # dict
        s = {i for i in range(10)}           # set
      • 遍歷文本文件

        with open ('a.txt') as f:
            for line in f:
                print(line)
      • 元祖拆包

        for i, j in [(1, 2), (3, 4)]:
            print(i,  j)
        >>>
        1 2
        3 4
      • 調用函數時,使用*拆包實參

        def func(a, b, c):
            print(a, b, c)
        
        func(*[1, 2, 3])  # 會將[1, 2, 3]這個list拆開成三個實參,對應a, b, c三個形參傳給func函數
  • 生成器

    Python之禪曾經説過,simple is better than complex。鑑於以上代碼中迭代器複雜的實現方式。Python提供了一個更加pythonic的實現方式——生成器。生成器函數就是含有yield關鍵字的函數(目前這種説法是正確的,之後會學到yield from等句法,那麼這個説法就就需要更正了),生成器對象就是調用生成器函數返回的對象。
    • 生成器的實現

      將上述代碼修改為生成器實現,如下:
      class Iterable:
          def __init__(self, *args):
              self.items = args
      
          def __iter__(self):                            # 8
              for item in self.items:
                  yield item
      
      ins = Iterable(1, 2, 3, 4, 5)
      print('the first for')
      for i in ins:
          print(i)
      print('the second for')
      for i in ins:
          print(i)
      print('the end...')
      
      >>>                                                            # 9                            
      the first for
      1
      2
      3
      4
      5
      the second for
      1
      2
      3
      4
      5
      the end...

      上述代碼中,可迭代對象的iter方法並沒有只用了短短數行,就完成了之前Iterator迭代器功能,點贊!

    • yield關鍵字

      要理解以上代碼,就需要理解yield關鍵字,先來看以下最簡單的生成器函數實現

      def func():
          yield 1                                                                
          yield 2
          yield 3
      
      ins1 = func()
      ins2 = func()
      print(func)
      print(ins1)
      print(ins2)
      
      for i in ins1:
          print(i)
      for i in ins1:
          print(i)
      
      print(next(ins2))
      print(next(ins2))
      print(next(ins2))
      print(next(ins2))
      
      >>> 
      <function func at 0x7fcb1e4bde18>
      <generator object func at 0x7fcb1cc7c0a0>
      <generator object func at 0x7fcb1cc7c0f8>
      1
      2
      3
      1
      2
      3
        File "/home/disk/test/a.py", line 18, in <module>
          print(next(ins2))
      StopIteration

      從以上代碼可以看出:

      • func是一個函數,但是調用func會返回一個生成器對象,並且通過打印的地址看,每次調用生成器函數會返回一個新的生成器對象。
      • 生成器對象和迭代器對象相似,都可以被for循環迭代,都只能被迭代一遍,通過next調用,都會在生成器元素為空時raise一個StopIteration異常。
    那麼含有yield關鍵字的生成器函數體是如何執行的呢?請看如下代碼:
    
    ```python
    def f_gen():                            # 10
        print('start')
        yield 1                                    # 11
        print('stop')
        yield 2                                    # 12
        print('next')
        yield 3                                    # 13
        print('end')
    
    for i in f_gen():                    # 14
        print(i)
    
    >>>
    start
    1
    stop
    2
    next
    3
    end
    ```
    從上述代碼及其打印結果,我們可以得出如下結論:

    -   \#10處代碼表明,生成器函數定義與普通函數無二,只是需要包含有yield關鍵字
    -   \#14for 循環隱形調用next的時候,會執行到#11處,打印start,然後產出值 1返回給for循環,打印
    -   for 循環繼續調用next,**從#11處執行到#12處**#,打印stop,然後產出值 2返回給for循環,打印
    -   for 循環繼續調用next,**從#12處執行到#13處**#,打印next,然後產出值 3返回給for循環,打印
    -   for 循環繼續調用next,**從#13處執行到函數尾**#,打印end,然後raise一個StopIteration,由於for循環捕獲異常,程序正常執行
    -   **綜上所述,yield具有暫停的功能,每次迭代生成器,生成器函數體都會前進到yield語句處,並將yield之後的值拋出(無值拋None)。生成器函數作為一個工廠函數,實現了可迭代對象中iter函數的功能,可以每次產出一個新的迭代器實例。由於使用了特殊的yield關鍵字,它擁有與區別於迭代器的新名字——生成器,它其實與迭代器並無二致**
  • 生成器表達式

    將列表推導式中的[]改為(),即為生成器表達式。返回的是一個生成器對象。一般用户列表推導但是又不需要立馬產生所有值的情景中。
    gen = (i for i in range(10))
    
    for i in gen:
        print(i)
    
    for i in gen:                        # 只能被消費一遍,第二遍無輸出
        print(i)
    print('the end...')
    
    >>> 
    0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    the end...
  • itertools

    python的內置模塊itertools提供了對生成器的諸多支持。這裏列舉一個,其它支持請看文檔
    gen = itertools.count(1, 2)    # 從1開始,步長為2,不斷產生數值
    
    >>> next(gen)
    1
    >>> next(gen)
    3
    >>> next(gen)
    5
    >>> next(gen)
    7
    >>> next(gen)
    9
    >>> next(gen)
    11
  • yield from 關鍵字

    yield from 是python3.3中出現的新句法。yield from句法可以實現委派生成器。
    def func():
        yield from (i for i in range(5))
    
    gen = func()
    
    for i in gen:
        print(i)
        
    >>>
    0
    1
    2
    3
    4

    如上所示,yield from把func作為了一個委派生成器。for循環可以通過委派生成器func直接迭代子生成器(i for i in range(5))。不過只是這個取巧遠遠不足以將yield from作為一個新句法加入到Python中。比起上述代碼的迭代內層循環,新句法更加重要的功能是委派生成器為調用者和子生成器建立了一個管道。通過生成器的send方法就可以在管道中為兩端傳遞消息。如果使用此方法在程序層面控制線程行為,就會迸發出強大的能量,它叫做協程。

寫在最後


  • 注意事項

    迭代器與生成器功能強大,不過使用中還是有幾點要注意:
    • 迭代器應該實現iter方法,雖然很多時候不實現此方法頁不會影響代碼運行。實現此方法的最主要原因有二:

      • 迭代器協議規定需要實現此方法
      • 可以通過issubclass檢查對象是否是迭代器
    • 不要把可迭代對象變為迭代器。原因有二:

      • 這不符合迭代器協議規定,造就了一個四不像。
      • 可迭代對象應該是可以重複遍歷的,如果變為了迭代器,那麼只能遍歷一次。
  • tips

    個人覺得迭代器有趣的點
    • os.walk

      os.walk迭代器可以深度遍歷目錄,是個大殺器,你值得擁有,快去試試吧。

    • iter

      iter可以接受兩個位置參數:callable和flag。callable()可以不斷產出值,如果等於flag,則終止。如下是一個小例子

      gen = (i for i in range(10))
      for i in iter(lambda: next(gen), 4):                # 執行ntext(gen), 不斷返回生成器中的值,等於4則停止
          print(i)
      
      >>> 
      0
      1
      2
      3
      the end...
    • yield可以接收值

      yield可以接收send發送的值。如下代碼中,#16處send的值,會傳給#15中的yield,然後賦值給res。

      def func():
          res = yield 1                #15
          print(res)            
      
      f = func()
      f.send(None)              # 預激
      f.send(5)                        # 16

希望大家可以通過本文掌握裝飾器這個殺手級特性。歡迎關注個人博客:藥少敏的博客

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.