全文共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
如需轉載,請後臺留言,遵守轉載規範