Python生成器函數概述:運用實例分解說明機制

2021-01-08 讀芯術

全文共5930字,預計學習時長12分鐘

Python的生成器函數為數據和計算資源管理提供了強大的機制,但對於Python初學者而言,理解它們並非易事。這篇文章會分解生成器的機制,同時介紹一個管理和分類S3文件資源的小類的例子,希望能對你有所啟發。

鑑於Python入門並不難,非常容易就可寫出真正能夠運作的代碼(比如迭代一列數值,計算以及/或列印一些數值),一些Python初學者和粗心的程式設計師可能沒有意識到該語言建立在procrastination,也即延遲計算的概念之上。對使用過編譯語言(如C++)的人而言,這種根植於該語言本身的鬆散性,或者說惰性,可能有點陌生。

很多程式設計師都學過「惰性計算」以及如何寫代碼來實現這一操作。但Python語言本身就支持這種計算(只需一個關鍵詞就可輕易實現),這種有效性和表達性在其他程序語言中非常罕見。所以,惰性計算這個概念被引入「拉達姆演算」,而Python儘管並非專門的功能語言(例如Lisp),也體現出功能編程的特性,也就不足為奇了,Python使用閉包函數也是拉達姆演算特性的一部分。

2001年,「PEP 255 — Simple Generators」(https://www.python.org/dev/peps/pep-0255/)介紹了生成器,提出動機是對惰性計算更加直接的表達:

當一個生產函數遇到需要保持在產出值之間的狀態,面對這一難題,很多程序語言無法提供令人滿意的有效解決方案……

機制

Python生成器函數是一個很強大的概念,但不同於函數裝飾器(function decorators)複雜的構架,它們運行和表達機制相當簡單,只需「yield」語句(yield這一新的關鍵詞在PEP 255中被加入Python)。

作為及物動詞,yield表示產出。作為不及物動詞,它表示讓步或撤回。這個單詞的兩種含義都會在Python生成器函數中出現。

人們通常認為,函數在返回單個值、以列表或詞庫形式返回多個值、或用戶定義的對象時,會通過返回語句產生結果。返回語句是函數結束控制並將控制和結果讓渡給調用者的方式。返回語句後,運行環境(解釋器)將給定函數的堆棧幀從調用堆棧中移除,給定函數的「環境」就會消失(直到下一次調用該函數)。

Python的yield語句則完全改變了上述操作。下面來看看一個非常簡單的人為設計的生成器例子,附有額外代碼以證明它的效用(代碼來自iPython解釋器交互對話):

In [8]: def gen(x):

...: yield x

In [9]: g = gen(10)

In [10]: g

Out[10]: <generator object gen at 0x10d26ed00>

In [11]: next(g)

Out[11]: 10

In [12]: g = gen(10)

In [13]: g

Out[13]: <generator object gen at 0x10d41d1a8>

In [14]: next(g)

Out[14]: 10

In [15]: next(g)

StopIteration Traceback (most recent call last)

<ipython-input-15-e734f8aca5ac> in <module>()

----> 1 next(g)

StopIteration:

In [16]:

這個函數只會「產出」作為參數傳遞的值。但是,僅像「普通」函數那樣調用該函數不會產生返回值。生成器函數會通過參數實例化並存於變量g之中。那麼,當 next()明確調用生產器對象時,生產器就必須進行迭代以產出值。而且,一旦產出(單個)值,生產器就會停止運作,此時繼續調用next()會導致 「StopIteration」異常。但如果在for循環中迭代生成器函數,for中包含的底層迭代機制就會巧妙地處理StopIteration異常。

很多Python文本都會通過循環語句引入generators,如以下代碼:

In [19]: def countdown_gen(x):

...: count = x

...: while count > 0:

...: yield count

...: count -= 1

...:

In [20]: g = countdown_gen(5)

In [21]: for item in g:

...: print(item)

...:

5

4

3

2

1

但這可能會混淆控制權的流動和轉移。必須明白,for循環進行迭代時,生成器在客戶端發出請求前,不會產出任何值。在for循環中,Python隱式地在從生成器對象中獲取的迭代器中調用next()。也就是說,在for循環中,Python隱式地執行以下操作:

In [32]: g = countdown_gen(5)

In [33]: g_iter = iter(g)

In [34]: next(g_iter)

Out[34]: 5

In [35]: next(g_iter)

Out[35]: 4

In [36]: next(g_iter)

Out[36]: 3

In [37]: next(g_iter)

Out[37]: 2

In [38]: next(g_iter)

Out[38]: 1

In [39]: next(g_iter)

StopIteration Traceback (most recent call last)

<ipython-input-39-fe4ec6cc82e2> in <module>()

----> 1 next(g_iter)

StopIteration:

當然,在生成器的迭代器上也可以顯式地調用next(),並且在Python解釋器控制臺中,手動強制迭代生成器是有幫助的。

下面的圖表或許有助於解釋這些步驟。

通過這種方式,與使用閉包函數一樣,Python生成器函數在連續調用期間保持相同狀態。或者,正如PEP 255所說:

如果遇到yield語句,函數的狀態將被凍結,expression_list的值將返回給.next()的調用者。「凍結」意味著保留所有本地狀態,包括當前的局部變量綁定、指令指針和內部評價棧。保存足夠的信息,這樣下次調用. next()時,該函數就可以直接開始運作,這時yield語句就好比是另一個外部調用。

上述的狀態保留和惰性產出值很難用這樣一個小而瑣碎的例子解釋清楚,所以本文嘗試通過編寫一個可能有用的生成器函數做出更加具體的解釋。

例子講解——S3

Amazon的S3存儲服務提供了一種相當簡單且可延展的方法,可在非層級結構中遠程存儲數據。本文不會全面討論S3, 但會進行簡單介紹,然後再探討是否可以將一些有用的S3資源訪問功能封裝到生成器函數中。

boto3 Python庫提供了訪問S3會話、資源和文件對象的API調用。此前筆者使用了download_file() API調用,但正如預計的那樣,它會將整個遠程文件下載到當前工作目錄中。如果在Docker運容器和EC2實例上運行Python腳本,這種方式是沒問題的。但要在MacBook Air上運行腳本,就得找到一種方法避免使用本地存儲,同時仍然能夠訪問遠程文件。

幸運的是,boto3庫允許通過Object API訪問文件資源的「流體」。這似乎是生成器函數的理想候選,因為文件對象應該只根據需求移動——即延遲模式。

當然,可直接使用這些API調用並直接迭代文件流。但整合訪問文件流所需的S3清理可能會更加簡便。雖然生成器在調用之間會保持狀態,但建議在類中組合生成器函數,管理S3會話狀態。這樣,通過重載類中的 __iter__方法就可以使類進行迭代,從而使S3類像Python標準庫中的文件對象一樣運行。

對該類的代碼如下所示:

import boto3

class S3FileReader:

"""

class S3FileReader:

Class to encapsulate boto3 calls to access an S3 resource

and allow clients to stream the contents of the file iteratively,

via a generator function: __iter__()

"""

def __init__(self, cfg, resource_key, bucket=None):

"""

__init__(self, cfg, bucket, resource_name):

S3FileReader constructor initializes the S3 Session,

gets the resource for a given bucket and key,

obtains the resource's object, and obtains a handle to the object.

Params:

cfg: config.py file containing S3 crexentials

bucket: name of the S3 bucket to access

resource_key: key of the S3 resource (file name)

"""

try:

if not bucket:

bucket = cfg.bucket

self._session = boto3.Session(

aws_access_key_id=cfg.aws_access_key_id,

aws_secret_access_key=cfg.aws_secret_access_key)

self._resource = self._session.resource('s3')

self._object = self._resource.Object(bucket, resource_key)

self._handle = self._object.get()

except Exception:

raise S3FileReaderException('Failed to initialize S3 resources!')

def __iter__(self):

"""

__iter__(self):

Provide iteration interface to clients. Get the stream of our

S3 object handle and produce results lazily for our clients

from a generator function.

yield statement yields a single line from the file.

Returns: nothing. A StopIteration exception is implicitly

raised following the completion of the for loop.

"""

if not self._handle:

raise S3FileReaderException('No S3 object handle!')

stream = self._handle['Body']

for line in stream:

yield line

def __enter__(self):

"""

__enter__(self):

Implement Python's context management protocol

so this class can be used in a "with" statement.

"""

return self

def __exit__(self, exc_type, exc_value, exc_tb):

"""

__exit__(self, exc_type, exc)_value, exc_tb):

Implement Python's context management protocol

so this class can be used in a "with" statement.

If exc_type is not None, then we are handling an

exception and for safety should delete our resources

"""

if exc_type is not None:

del self._session

del self._resource

del self._object

del self._handle

return False

else: # normal exit flow

return True

class S3FileReaderException(Exception):

"""

class S3FileReaderException(Exception):

Simple exception class to use if we can't get an S3

File handle, or otherwise have an exception when

dealing with S3.

"""

def __init(self, msg):

self.msg = msg

誠然,與它需要提供的有限功能相比,這個類的代碼更複雜。但它會提供一些異常處理機制,運行Python上下文管理界面,類在使用時就能像標準庫的文件對象一樣。這樣就無需更詳細的try/except塊。__exit__函數使用了不必要的對象刪除,這有違筆者以前堅持最優類析構函數的C++ 習慣,但它也明確了要在對象清理時釋放所有S3資源,包括會話、資源和對象。boto3庫似乎不支持close()方法。

在構造函數中執行了必要的S3清理之後,該類提供了一個很好的接口,通過_iter__方法迭代文件流。客戶端代碼可能希望在迭代流時執行額外的處理或邏輯。增強小類的一個好方法是添加過濾謂詞。那麼,如果用戶只關注數據的一個子集, _iter_方法就無需發出大文件的每一行。同時,標準庫的itertools.dropwhile函數在此處也能很好地運作。

主要優點在於S3FileReader類的客戶端無需擔心S3的清理和維護,只需指示感興趣的資源。雖然類會迭代文件流,從而通過 __iter__ 方法生成行,但控制迭代和數據生成的是類客戶端。

總之,Python生成器函數廣泛應用於標準庫中,為程式設計師提供了一個強大的延遲計算工具,節省了時間和空間。

留言 點讚 關注

我們一起分享AI學習與發展的乾貨

編譯組:楊敏迎、梁晶晶

相關連結:

https://medium.com/better-programming/an-introduction-to-python-generator-functions-cd9662b1d797

如需轉載,請後臺留言,遵守轉載規範

相關焦點

  • Python3中的生成器函數
    下面介紹的函數生成器就是這種工具之一。 1:生成器函數的定義 定義:使用常規的def語句進行編寫,但是使用yield語句一次返回一個結果集,在每次結果產生之間掛起和恢復它們的狀態。
  • python迭代器和生成器總結——新的斐波那契數列
    for循環遍歷結果事實上,python中,提供了另一個工具來幫助遍歷,該工具稱之為迭代器。可以通過iter函數來獲取一個迭代器,通過next函數來獲取下一個元素。如下:事實上,當將__iter__方法刪除後,還是會得到一個TypeError,這說明,__iter__方法是重要的。demo裡,__next__方法簡單返回了一個1,事實上,在真正使用時,這個方法可能需要更加豐富的邏輯。比如說,返回一個從1開始不斷遞增的數字序列,並且當大於10時,停止循環 。代碼如下:
  • Python數據讀取之生成器(generator)
    Python生成器是創建迭代器的簡單方法。簡單來說,生成器是一個函數,它返回一個我們可以迭代的對象(迭代器),迭代器一次返回一個值較使用列表將所有數據都加載到內存中,生成器節省了大量內存空間。深度學習的數據讀取部分一般都需要使用迭代器。
  • python高階函數:map、filter、reduce的替代品
    什麼是高階函數?高階函數是一種將函數作為參數,或者把函數作為結果返回的函數,map函數、sorted函數就是高階函數的典型例子。map函數在小編以前的文章中做過相應的知識分享。sorted函數是python的內置函數,它的可選參數key用於提供一個函數,它可以將函數應用到各個元素上進行排序。
  • Python實現一個類似range函數的浮點數生成器 - python高手養成
    今天,帶大家做一個浮點數生成器,主要用到的知識點有:1.實現自定義類的__iter__()和__reversed()__方法2.Decimal對於十進位數據的精度控制Python實現浮點數生成器通常,我們使用range()函數來生成一個整形數列表,函數中我們可以自定義起始值、結束值和步進範圍。
  • python中常見的對象辨析(可迭代對象、迭代器對象、生成器對象)
    2.數值型可迭代對象即序列、字典、集合對應的可迭代對象二 迭代器對象IteratorA.定義可作用於next()函數並能不斷調用的對象叫迭代 器對象B.包括1.iter(具有__iter__方法的對象)的返回值2.生成器C.訪問迭代器1.循環遍歷訪問
  • python動態添加類對象成員:變量、函數
    支持為已創建好的對象動態增加實例變量而不會改變類的原始定義!) 運行結果為:男上述代碼為 p 對象動態新增了一個 sex實例變量,即直接對p對象的sex實例變量賦值就能新增一個實例變量。python也支持動態刪除實例變量,使用del語句即可刪除,如刪除類對象p的name成員變量:# 刪除p對象的name實例變量 del p.name# 再次訪問p的name實例變量
  • Python每日3題-為什麼函數名字可以當做參數用?
    [Normal] 函數裝飾器有什麼作用?裝飾器的返回值也是一個函數的對象,它經常用於有切面需求的場景。比如:插入日誌,性能測試,事務處理,緩存。權限的校驗等場景,有了裝飾器就可以抽離出大量的與函數功能本身無關的雷同代碼並發並繼續使用。 [Hard] 生成器,迭代器的區別? 思考30秒再往下翻...
  • Python視頻教程網課編程零基礎入門數據分析網絡爬蟲全套Python...
    變量的計算和輸入輸出 08 變量的命名 09 if判斷語句 10 石頭剪刀布 11 while循環基本使用 12循環嵌套 13 函數的基本使用 14 函數的參數和返回值 15函數的嵌套調用 16模塊的簡介 17列表 18元祖 19字典
  • python機器學習:常用庫的介紹及安裝
    用Anacond安裝Numpy庫3、代碼編寫是python版本的選擇,python是一個比較特殊的群體,一般語言的各大版本都是向下兼容的,但是python大半本為python3.x的是不兼容python2.x得,所以在寫代碼之前要確認後面的代碼要使用
  • python測試函數模塊unittest
    return full_name.title()test_name_function.py 模塊名import unittest unittest是python標準庫中的函數不可更改但可以用as重新命名調用函數unittest模塊from name_function import get_formatted_name
  • Python學習第40課-Lambda表達式創建匿名函數
    【每天幾分鐘,從零入門python編程的世界!】之前我們學習了使用def關鍵字來創建自定義函數,這種方式創建的函數都是有函數名的,我們也可以創建沒有函數名的函數,這種函數叫做匿名函數。Python中可以使用Lambda表達式來創建匿名函數。
  • Python面試題庫,據說網易騰訊谷歌都在用!
    (1分)它是一個經常用作內聯函數的單個表達式匿名函數。8、為什麼python中的lambda表單沒有語句?(1分)python中的lambda表單沒有語句,因為它用於創建新的函數對象,然後在運行時返回它們。9、Python中的pass是什麼意思?
  • python中函數的運用(1)
    世界一切皆函數! 提到函數,定義我們就不說了。 大白話就是提供某個功能的黑盒子。 你提供一個數值,或者一個字符串,或者提供一個功能,或者子函數,然後函數輸出一個變量或者其他應該輸出的內容 我們先從最簡單的開始,就先計算加,減,乘,除中的一個。 我們先來計算加法。 比如我們計算1.5+2。
  • Python利用openpyxl處理Excel文件(單元格及行列具體操作)
    用ws表示工作表實例,後面不再說明。(一)獲取單元格總結獲取單元格有兩種方式:1.ws[行列名] # 列名使用英文字母2.ws.cell(row, column) # 列名使用數字列字母數字轉化函數(ws.rows和ws.columns),這兩個生成器裡面存儲了每一行(列)的數據,每一行由一個tuple包裹,便於對行列進行遍。
  • 如何使用python語言代碼實現判斷是否為回文
    工具Visual Studio 2019python運行環境技術python回文回文,是按照中心對稱,從左到右或從右到左,字符串都一樣的。如果想要python語言代碼實現回文判斷,若為回文,列印回文,否則列印不是回文。
  • 雲計算開發學習實例:Python3 計算 n 個自然數的立方和
    225公式 : 13 + 23 + 33 + 43 + 53 = 225輸入 : n = 7輸入 : 784公式 : 13 + 23 + 33 + 43 + 53 + 63 + 73 = 784以上實例輸出結果為
  • Python 爬蟲面試題 170 道:2019 版
    44.我們知道對於列表可以使用切片操作進行部分元素的選擇,那麼如何對生成器類型的對象實現相同的功能呢?45.請將[i for i in range(3)]改成生成器46.a="hello"和 b="你好"編碼成 bytes 類型47.下面的代碼輸出結果是什麼?
  • python:pop函數詳解 - 二進位01
    pop函數詳解今天我為大家講解python中pop函數的使用。#python#簡介——pop()函數是python解釋器的內置方法,可作用於列表,字典。用法說明——在builtins.py中找到pop函數。列表:L.pop([index]) -> item -- remove and return item at index (default last).