<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Python進階——如何正確使用yield?

          共 10865字,需瀏覽 22分鐘

           ·

          2021-03-15 09:35

          在 Python 開發(fā)中,yield 關鍵字的使用其實較為頻繁,例如大集合的生成,簡化代碼結(jié)構、協(xié)程與并發(fā)都會用到它。

          但是,你是否真正了解 yield 的運行過程呢?

          這篇文章,我們就來看一下 yield 的運行流程,以及在開發(fā)中哪些場景適合使用 yield。

          生成器

          如果在一個方法內(nèi),包含了 yield 關鍵字,那么這個函數(shù)就是一個「生成器」。

          生成器其實就是一個特殊的迭代器,它可以像迭代器那樣,迭代輸出方法內(nèi)的每個元素。

          如果你還不清楚「迭代器」是什么,可以參考我寫的這篇文章:Python進階——迭代器和可迭代對象有什么區(qū)別?

          我們來看一個包含 yield 關鍵字的方法:

          # coding: utf8

          # 生成器
          def gen(n):
              for i in range(n):
                  yield i

          g = gen(5)      # 創(chuàng)建一個生成器
          print(g)        # <generator object gen at 0x10bb46f50>
          print(type(g))  # <type 'generator'>

          # 迭代生成器中的數(shù)據(jù)
          for i in g:
              print(i)
              
          # Output:
          # 0 1 2 3 4

          注意,在這個例子中,當我們執(zhí)行 g = gen(5) 時,gen 中的代碼其實并沒有執(zhí)行,此時我們只是創(chuàng)建了一個「生成器對象」,它的類型是 generator。

          然后,當我們執(zhí)行 for i in g,每執(zhí)行一次循環(huán),就會執(zhí)行到 yield 處,返回一次 yield 后面的值。

          這個迭代過程是和迭代器最大的區(qū)別。

          換句話說,如果我們想輸出 5 個元素,在創(chuàng)建生成器時,這個 5 個元素其實還并沒有產(chǎn)生,什么時候產(chǎn)生呢?只有在執(zhí)行 for 循環(huán)遇到 yield 時,才會依次生成每個元素。

          此外,生成器除了和迭代器一樣實現(xiàn)迭代數(shù)據(jù)之外,還包含了其他方法:

          • generator.__next__():執(zhí)行 for 時調(diào)用此方法,每次執(zhí)行到 yield 就會停止,然后返回 yield 后面的值,如果沒有數(shù)據(jù)可迭代,拋出 StopIterator 異常,for 循環(huán)結(jié)束
          • generator.send(value):外部傳入一個值到生成器內(nèi)部,改變 yield 前面的值
          • generator.throw(type[, value[, traceback]]):外部向生成器拋出一個異常
          • generator.close():關閉生成器

          通過使用生成器的這些方法,我們可以完成很多有意思的功能。

          __next__

          先來看生成器的 __next__ 方法,我們看下面這個例子。

          # coding: utf8

          def gen(n):
              for i in range(n):
                  print('yield before')
                  yield i
                  print('yield after')

          g = gen(3)      # 創(chuàng)建一個生成器
          print(g.__next__())  # 0
          print('----')
          print(g.__next__())  # 1
          print('----')
          print(g.__next__())  # 2
          print('----')
          print(g.__next__())  # StopIteration

          # Output:
          # yield before
          # 0
          # ----
          # yield after
          # yield before
          # 1
          # ----
          # yield after
          # yield before
          # 2
          # ----
          # yield after
          # Traceback (most recent call last):
          #   File "gen.py", line 16, in <module>
          #     print(g.__next__())  # StopIteration
          # StopIteration

          在這個例子中,我們定義了 gen 方法,這個方法包含了 yield 關鍵字。然后我們執(zhí)行 g = gen(3) 創(chuàng)建一個生成器,但是這次沒有執(zhí)行 for 去迭代它,而是多次調(diào)用 g.__next__() 去輸出生成器中的元素。

          我們看到,當執(zhí)行 g.__next__()時,代碼就會執(zhí)行到 yield 處,然后返回 yield 后面的值,如果繼續(xù)調(diào)用 g.__next__(),注意,你會發(fā)現(xiàn),這次執(zhí)行的開始位置,是上次 yield 結(jié)束的地方,并且它還保留了上一次執(zhí)行的上下文,繼續(xù)向后迭代。

          這就是使用 yield 的作用,在迭代生成器時,每一次執(zhí)行都可以保留上一次的狀態(tài),而不是像普通方法那樣,遇到 return 就返回結(jié)果,下一次執(zhí)行只能再次重復上一次的流程。

          生成器除了能保存狀態(tài)之外,我們還可以通過其他方式,改變其內(nèi)部的狀態(tài),這就是下面要講的 send 和 throw 方法。

          send

          上面的例子中,我們只展示了在 yield 后有值的情況,其實還可以使用 j = yield i 這種語法,我們看下面的代碼:

          # coding: utf8

          def gen():
              i = 1
              while True:
                  j = yield i
                  i *= 2
                  if j == -1:
                      break

          此時如果我們執(zhí)行下面的代碼:

          for i in gen():
              print(i)
              time.sleep(1)

          輸出結(jié)果會是 1 2 4 8 16 32 64 ... 一直循環(huán)下去, 直到我們殺死這個進程才能停止。

          這段代碼一直循環(huán)的原因在于,它無法執(zhí)行到 j == -1 這個分支里 break 出來,如果我們想讓代碼執(zhí)行到這個地方,如何做呢?

          這里就要用到生成器的 send 方法了,send 方法可以把外部的值傳入生成器內(nèi)部,從而改變生成器的狀態(tài)。

          代碼可以像下面這樣寫:

          g = gen()   # 創(chuàng)建一個生成器
          print(g.__next__())  # 1
          print(g.__next__())  # 2
          print(g.__next__())  # 4
          # send 把 -1 傳入生成器內(nèi)部 走到了 j = -1 這個分支
          print(g.send(-1))   # StopIteration 迭代停止

          當我們執(zhí)行 g.send(-1) 時,相當于把 -1 傳入到了生成器內(nèi)部,然后賦值給了 yield 前面的 j,此時 j = -1,然后這個方法就會 break 出來,不會繼續(xù)迭代下去。

          throw

          外部除了可以向生成器內(nèi)部傳入一個值外,還可以傳入一個異常,也就是調(diào)用 throw 方法:

          # coding: utf8

          def gen():
              try:
                  yield 1
              except ValueError:
                  yield 'ValueError'
              finally:
                  print('finally')

          g = gen()   # 創(chuàng)建一個生成器
          print(g.__next__()) # 1
          # 向生成器內(nèi)部傳入異常 返回ValueError
          print(g.throw(ValueError))

          # Output:
          # 1
          # ValueError
          # finally

          這個例子創(chuàng)建好生成器后,使用 g.throw(ValueError) 的方式,向生成器內(nèi)部傳入了一個異常,走到了生成器異常處理的分支邏輯。

          close

          生成器的 close 方法也比較簡單,就是手動關閉這個生成器,關閉后的生成器無法再進行操作。

          >>> g = gen()
          >>> g.close() # 關閉生成器
          >>> g.__next__() # 無法迭代數(shù)據(jù)
          Traceback (most recent call last):
            File "<stdin>", line 1in <module>
          StopIteration

          close 方法我們在開發(fā)中使用得比較少,了解一下就好。

          使用場景

          了解了 yield 和生成器的使用方式,那么 yield 和生成器一般用在哪些業(yè)務場景中呢?

          下面我介紹幾個例子,分別是大集合的生成、簡化代碼結(jié)構、協(xié)程與并發(fā),你可以參考這些使用場景來使用 yield。

          大集合的生成

          如果你想生成一個非常大的集合,如果使用 list 創(chuàng)建一個集合,這會導致在內(nèi)存中申請一個很大的存儲空間,例如想下面這樣:

          # coding: utf8

          def big_list():
              result = []
              for i in range(10000000000):
                  result.append(i)
              return result

          # 一次性在內(nèi)存中生成大集合 內(nèi)存占用非常大
          for i in big_list():
              print(i)

          這種場景,我們使用生成器就能很好地解決這個問題。

          因為生成器只有在執(zhí)行到 yield 時才會迭代數(shù)據(jù),這時只會申請需要返回元素的內(nèi)存空間,代碼可以這樣寫:

          # coding: utf8

          def big_list():
              for i in range(10000000000):
                  yield i

          # 只有在迭代時 才依次生成元素 減少內(nèi)存占用
          for i in big_list():
              print(i)

          簡化代碼結(jié)構

          我們在開發(fā)時還經(jīng)常遇到這樣一種場景,如果一個方法要返回一個 list,但這個 list 是多個邏輯塊組合后才能產(chǎn)生的,這就會導致我們的代碼結(jié)構變得很復雜:

          # coding: utf8

          def gen_list():
              # 多個邏輯塊 組成生成一個列表
              result = []
              for i in range(10):
                  result.append(i)
              for j in range(5):
                  result.append(j * j)
              for k in [100200300]:
                  result.append(k)
              return result
              
          for item in gen_list():
              print(item)

          這種情況下,我們只能在每個邏輯塊內(nèi)使用 append 向 list 中追加元素,代碼寫起來比較啰嗦。

          此時如果使用 yield 來生成這個 list,代碼就簡潔很多:

          # coding: utf8

          def gen_list():
              # 多個邏輯塊 使用yield 生成一個列表
              for i in range(10):
                  yield i
              for j in range(5):
                  yield j * j
              for k in [100200300]:
                  yield k
                  
          for item in gen_list():
              print(i)

          使用 yield 后,就不再需要定義 list 類型的變量,只需在每個邏輯塊直接 yield 返回元素即可,可以達到和前面例子一樣的功能。

          我們看到,使用 yield 的代碼更加簡潔,結(jié)構也更清晰,另外的好處是只有在迭代元素時才申請內(nèi)存空間,降低了內(nèi)存資源的消耗。

          協(xié)程與并發(fā)

          還有一種場景是 yield 使用非常多的,那就是「協(xié)程與并發(fā)」。

          如果我們想提高程序的執(zhí)行效率,通常會使用多進程、多線程的方式編寫程序代碼,最常用的編程模型就是「生產(chǎn)者-消費者」模型,即一個進程 / 線程生產(chǎn)數(shù)據(jù),其他進程 / 線程消費數(shù)據(jù)。

          在開發(fā)多進程、多線程程序時,為了防止共享資源被篡改,我們通常還需要加鎖進行保護,這樣就增加了編程的復雜度。

          在 Python 中,除了使用進程和線程之外,我們還可以使用「協(xié)程」來提高代碼的運行效率。

          什么是協(xié)程?

          簡單來說,由多個程序塊組合協(xié)作執(zhí)行的程序,稱之為「協(xié)程」。

          而在 Python 中使用「協(xié)程」,就需要用到 yield 關鍵字來配合。

          可能這么說還是太好理解,我們用 yield 實現(xiàn)一個協(xié)程生產(chǎn)者、消費者的例子:

          # coding: utf8

          def consumer():
              i = None
              while True:
                  # 拿到 producer 發(fā)來的數(shù)據(jù)
                  j = yield i 
                  print('consume %s' % j)

          def producer(c):
              c.__next__()
              for i in range(5):
                  print('produce %s' % i)
                  # 發(fā)數(shù)據(jù)給 consumer
                  c.send(i)
              c.close()

          c = consumer()
          producer(c)

          # Output:
          # produce 0
          # consume 0
          # produce 1
          # consume 1
          # produce 2
          # consume 2
          # produce 3
          # consume 3
          ...

          這個程序的執(zhí)行流程如下:

          1. c = consumer() 創(chuàng)建一個生成器對象
          2. producer(c) 開始執(zhí)行,c.__next()__ 會啟動生成器 consumer 直到代碼運行到 j = yield i 處,此時 consumer 第一次執(zhí)行完畢,返回
          3. producer 函數(shù)繼續(xù)向下執(zhí)行,直到 c.send(i) 處,這里利用生成器的 send 方法,向 consumer 發(fā)送數(shù)據(jù)
          4. consumer 函數(shù)被喚醒,從 j = yield i 處繼續(xù)開始執(zhí)行,并且接收到 producer 傳來的數(shù)據(jù)賦值給 j,然后打印輸出,直到再次執(zhí)行到 yield 處,返回
          5. producer 繼續(xù)循環(huán)執(zhí)行上面的過程,依次發(fā)送數(shù)據(jù)給 cosnumer,直到循環(huán)結(jié)束
          6. 最終 c.close() 關閉 consumer 生成器,程序退出

          在這個例子中我們發(fā)現(xiàn),程序在 producer 和 consumer 這 2 個函數(shù)之間來回切換執(zhí)行,相互協(xié)作,完成了生產(chǎn)任務、消費任務的業(yè)務場景,最重要的是,整個程序是在單進程單線程下完成的。

          這個例子用到了上面講到的 yield、生成器的 __next__、sendclose 方法。如果不好理解,你可以多看幾遍這個例子,最好自己測試一下。

          我們使用協(xié)程編寫生產(chǎn)者、消費者的程序時,它的好處是:

          • 整個程序運行過程中無鎖,不用考慮共享變量的保護問題,降低了編程復雜度
          • 程序在函數(shù)之間來回切換,這個過程是用戶態(tài)下進行的,不像進程 / 線程那樣,會陷入到內(nèi)核態(tài),這就減少了內(nèi)核態(tài)上下文切換的消耗,執(zhí)行效率更高

          所以,Python 的 yield 和生成器實現(xiàn)了協(xié)程的編程方式,為程序的并發(fā)執(zhí)行提供了編程基礎。

          Python 中的很多第三方庫,都是基于這一特性進行封裝的,例如 gevent、tornado,它們都大大提高了程序的運行效率。

          總結(jié)

          總結(jié)一下,這篇文章我們主要講了 yield 的使用方式,以及生成器的各種特性。

          生成器是一種特殊的迭代器,它除了可以迭代數(shù)據(jù)之外,在執(zhí)行時還可以保存方法中的狀態(tài),除此之外,它還提供了外部改變內(nèi)部狀態(tài)的方式,把外部的值傳入到生成器內(nèi)部。

          利用 yield 和生成器的特性,我們在開發(fā)中可以用在大集成的生成、簡化代碼結(jié)構、協(xié)程與并發(fā)的業(yè)務場景中。

          Python 的 yield 也是實現(xiàn)協(xié)程和并發(fā)的基礎,它提供了協(xié)程這種用戶態(tài)的編程模式,提高了程序運行的效率。

          更多閱讀



          2020 年最佳流行 Python 庫 Top 10


          2020 Python中文社區(qū)熱門文章 Top 10


          5分鐘快速掌握 Python 定時任務框架

          特別推薦




          點擊下方閱讀原文加入社區(qū)會員

          瀏覽 70
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  又色又爽的网站 | 污网站在线免费看 | 精品无码第一页在线观看 | 91一区二区三 | 极品3p肏屄 |