「Python 系列」 Python 生成器函數詳解


「Python 系列」 Python 生成器函數詳解


Python的生成器函數提供了一種強大的機制來管理數據和計算資源,但是對於Python的新手來說,它們不一定直觀。在本文中,我將分解生成器的機制,同時還介紹我希望是一個有啟發性的示例:用於管理和流傳輸S3文件資源的小類。

簡介與歷史

鑑於使用Python入門和編寫實際完成某件事的代碼非常容易(例如,遍歷值列表,計算和/或打印這些值),對於新手或不經意的Python程序員來說,使用該語言可能並不陌生建立在拖延或延遲計算的概念中。語言本身固有的這種鬆散(或懶惰)對於那些使用編譯語言(例如C ++)的人來說似乎是陌生的。

大多數程序員學習“ 惰性評估 ”,並被教導編寫代碼以實現這種做法。但是Python在語言中對此的原生支持(通過單個關鍵字簡單而優雅地實現)引入了功能和表達能力,這在其他編程語言中似乎很少見。毫不奇怪的是,作為“ lambda演算 ”的一部分引入了惰性評估作為概念,並且Python(儘管不是唯一的功能語言(例如Lisp))體現了這種功能編程DNA。(我之前寫過關於Python函數裝飾器的文章,Python對閉包的使用也是lambda演算的遺產的一部分)。

“ PEP 255-Simple Generators ”在2001年引入了generators,將懶惰評估的一種稍微傾斜的表達作為動機:

當生產者職能部門的工作非常艱苦,需要維持所產生的值之間的狀態時,大多數編程語言都無法提供令人滿意且有效的解決方案……。


機械學

Python生成器函數是一個功能強大的概念,但是與複雜的函數裝飾器框架不同,它們是通過非常簡單的機制來實現或表示的,即“ yield”語句(yield是PEP 255下添加到Python的新關鍵字)。

作為及物動詞,yield表示生產;作為不及物動詞,它表示讓步或放棄。單詞的兩種含義都在Python的生成器函數中起作用。

傳統上,我們認為函數在返回單個值,列表或字典形式的多個值或用戶定義的對象時通過它們的return語句產生結果。我們認為return語句是函數結束控制並將控制和結果割讓回其調用方的一種方式。在return語句之後,運行時環境(解釋器)將給定函數的堆棧框架彈出調用堆棧,並且給定函數的“環境”(如其存在)將不復存在(直到下一次調用該函數)。

Python的yield語句完全改變了這種行為。讓我們看一下生成器的一個非常簡單且人為的示例-帶有一些其他代碼來演示其用法(代碼來自iPython解釋器交互式會話):

In [8]: def gen(x):
...: yield x

In [9]: g = gen(10)
In [10]: g
Out[10]: <generator>

In [11]: next(g)
Out[11]: 10

In [12]: g = gen(10)

In [13]: g
Out[13]: <generator>

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]: /<module>/<ipython-input-15-e734f8aca5ac>/<generator>/<generator>

該函數除了“屈服”作為參數傳遞的值外,什麼也不做。但是,僅像“正常”函數那樣調用函數不會產生返回值。如果可能,將使用一個參數實例化生成器函數,並將其保存在變量中g。現在,隨著next()對生成器對象的調用清楚了,必須迭代生成器以生成值。並且,一旦產生其(單個)值,生成器便會耗盡-隨後的調用next()會引發“ StopIteration”異常。如果我們在一個for循環中迭代了此生成器函數,則包含在中的底層迭代機制for將StopIteration優雅地處理該異常。

大多數Python文本使用循環語句介紹生成器,類似於以下代碼:

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:

In [40]: /<module>/<ipython-input-39-fe4ec6cc82e2>

當然,next()可以在生成器函數的迭代器上顯式調用它,這有助於在Python解釋器控制檯上手動強制生成器函數進行迭代。

下圖也可以幫助解釋這些步驟。


「Python 系列」 Python 生成器函數詳解

這樣,與閉包一樣,Python的生成器函數在連續調用之間保持狀態。或者,如PEP 255所述:

如果遇到yield語句,則函數的狀態將被凍結,並且expression_list的值返回給.next()的調用方。“凍結”是指保留所有局部狀態,包括局部變量的當前綁定,指令指針和內部評估堆棧:保存了足夠的信息,以便下次調用.next()時,該函數可以就像yield語句只是另一個外部調用一樣繼續進行。

這種狀態保留和值的懶散生成很難用這麼小的瑣碎示例來概念化,因此我嘗試通過編碼我認為可能有用的生成器函數使其更加具體。


用例— S3

Amazon的S3存儲服務提供了一種相當簡單且可擴展的方式,可以以非分層結構遠程存儲數據。關於S3的完整討論不在本文的討論範圍之內,但是在吸引我探索是否可以在生成器函數中封裝一些有用的S3資源訪問功能之前,已經對S3進行了一些工作。

該boto3 Python庫提供的API調用訪問S3會話,資源和文件對象。以前我曾使用過download_file()API調用,但是正如預期的那樣,這會將整個遠程文件下載到一個人的當前工作目錄中。如果您要在EC2實例上的Docker容器中運行Python腳本,那就很好,但是對於我目前的工作,我在MacBook Air上運行腳本,因此我很想找到一種避免使用本地存儲的方法,仍然能夠訪問遠程文件。

幸運的是,boto3庫允許通過對象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生成器函數在標準庫中得到了廣泛使用,併為程序員提供了用於推遲計算,節省時間和空間的強大工具。


分享到:


相關文章: