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

寫在前面

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

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

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

乾貨兒

  • 迭代器github

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

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

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

      話很少說,上代碼。函數

      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

但願你們能夠經過本文掌握裝飾器這個殺手級特性。歡迎關注我的博客:藥少敏的博客

相關文章
相關標籤/搜索