PEP 325 – 產生器資源釋放支援
- 作者:
- Samuele Pedroni <pedronis at python.org>
- 狀態:
- 已否決 (Rejected)
- 類型:
- 標準軌跡 (Standards Track)
- 建立日期:
- 2003年8月25日
- Python 版本:
- 2.4
- 公告歷史:
摘要
產生器允許以自然的方式對數據遍歷進行編碼與抽象。目前,如果涉及需要適時釋放的外部資源,產生器遺憾地並不適用。適時釋放資源的典型慣用法不被支援,因為產生器內部的 try-finally 語句中的 try 子句不允許使用 yield。此外,finally 子句的執行既無法保證,也無法強制執行。
本 PEP 提議內建的產生器類型應實作 close 方法與銷毀語義,以便解除對 yield 放置位置的限制,進而擴展產生器的適用性。
宣告
已被否決,改為支持 PEP 342,後者以更完善的形式包含實質上所有要求的行為。
原理
Python 產生器允許對多種數據遍歷場景進行自然編碼。它們的實例化會產生迭代器,即抽象化遍歷的一等公民對象(具有一等公民對象的所有優點)。在這方面,它們的效能與使用接受(類 smalltalk 風格)代碼塊的迭代器方法相比,具有相當的實力且提供了一些優勢。另一方面,考慮到目前的限制(產生器內部 try-finally 的 try 子句不允許 yield),後者的方法似乎更適合不僅封裝遍歷,還封裝異常處理以及正確的資源獲取與釋放。
讓我們考慮一個例子(為簡單起見,使用讀取模式的文件)
def all_lines(index_path):
for path in file(index_path, "r"):
for line in file(path.strip(), "r"):
yield line
這段代碼簡潔明瞭,但無法加入用於適時關閉文件的 try-finally。(雖然可以傳遞文件對象而非路徑作為參數,並由調用者負責關閉,但對於根據索引內容打開的文件,這種方法並不適用)。
如果我們想要適時釋放資源,就必須犧牲純產生器方法的簡單與直接性:(例如)
class AllLines:
def __init__(self, index_path):
self.index_path = index_path
self.index = None
self.document = None
def __iter__(self):
self.index = file(self.index_path, "r")
for path in self.index:
self.document = file(path.strip(), "r")
for line in self.document:
yield line
self.document.close()
self.document = None
def close(self):
if self.index:
self.index.close()
if self.document:
self.document.close()
使用方式如下
all_lines = AllLines("index.txt")
try:
for line in all_lines:
...
finally:
all_lines.close()
這種實作適時釋放的較為複雜的解決方案,似乎提供了一個寶貴的提示。我們所做的是將遍歷封裝在一個具有 close 方法的對象(迭代器)中。
本 PEP 提議產生器應發展出這樣的 close 方法,並具備相應的語義,使得該範例可以重寫為
# Today this is not valid Python: yield is not allowed between
# try and finally, and generator type instances support no
# close method.
def all_lines(index_path):
index = file(index_path, "r")
try:
for path in index:
document = file(path.strip(), "r")
try:
for line in document:
yield line
finally:
document.close()
finally:
index.close()
all = all_lines("index.txt")
try:
for line in all:
...
finally:
all.close() # close on generator
目前 PEP 255 不允許在 try-finally 語句的 try 子句中使用 yield,因為無法按照 try-finally 語義的要求來保證 finally 子句的執行。
提議的 close 方法語義應為:雖然 finally 子句的執行仍無法絕對保證,但在需要時可以強制執行。具體來說,close 方法的行為應觸發產生器內部 finally 子句的執行,方式可以是強制在產生器框架中回傳(return),或者是向其拋出一個異常。在需要適時釋放資源的情況下,可以明確調用 close。
另一方面,應擴展產生器銷毀的語義,以便在一般情況下實作「盡力而為」(best-effort)策略。具體來說,銷毀應觸發 close()。「盡力而為」的局限性源於析構函數(destructor)的執行本身就不是絕對保證的。
這似乎是一個合理的妥協,最終的全域行為將類似於文件的處理與關閉。
可能的語義
內建的產生器類型應實作 close 方法,調用方式如下:
gen.close()
其中 gen 是內建產生器類型的實例。產生器的銷毀也應調用 close 方法行為。
如果產生器已經終止,close 應為無操作(no-op)。
否則,有兩種替代方案:Return 語義或 Exception 語義。
A - Return 語義:產生器應恢復執行,且執行過程應如同在重新進入點執行的指令是 return 一樣。因此,如果允許 try-yield-finally 模式,圍繞重新進入點的 finally 子句將會被執行。
問題:區分由 close 引起的強制終止、正常終止、來自產生器或產生器調用代碼的異常傳遞是否重要?在正常情況下似乎不重要,finally 子句在所有這些情況下的運作應該是一樣的,但這種語義可能會使這類區分變得困難。
與正常 return 一樣,Except 子句不會被執行。遺留產生器中的這類子句預期用於執行由產生器或其調用代碼引發的異常。在 close 的情況下不執行它們似乎是正確的。
B - Exception 語義:產生器應恢復執行,且執行過程應如同在重新進入點引發了一個特殊用途的異常(例如 CloseGenerator)。close 的實作應消耗此異常而不進一步向上傳遞。
問題:是否應為此目的重複使用 StopIteration?可能不應該。我們希望 close 對於遺留產生器是一個無害的操作,因為它們可能包含捕獲 StopIteration 以處理其他產生器/迭代器的代碼。
一般而言,在 Exception 語義下,如果產生器沒有終止,或者我們沒有收到傳回的特殊異常,目前尚不清楚該如何處理。其他不同的異常可能應該被傳遞,但考慮這段可能的遺留產生器代碼
try:
...
yield ...
...
except: # or except Exception:, etc
raise Exception("boom")
如果在 yield 之後產生器掛起時調用 close,except 子句會捕獲我們的特殊用途異常,因此我們會得到一個傳回的不同異常。在這種情況下,該異常理應被合理地消耗與忽略,但通常情況下應該傳遞,區分這些場景似乎很困難。
異常方法的優點在於讓產生器能夠區分終止情況並擁有更多控制權。另一方面,明確的語義似乎更難定義。
備註
如果此提案被接受,記錄產生器是否獲取資源以便調用其 close 方法應成為通用慣例。如果產生器不再使用,調用 close 應該是無害的。
另一方面,在典型場景中,實例化產生器的代碼應在需要時負責調用 close。處理別處實例化的迭代器/產生器的通用代碼,通常不應充斥著 close 調用。
極少數情況下,代碼獲得了所有迭代器、產生器以及獲取需及時釋放資源之產生器的所有權,且需要妥善處理它們,這可以很容易地解決:
if hasattr(iterator, 'close'):
iterator.close()
待決問題
應選擇最終語義。目前 Guido 傾向於 Exception 語義。如果產生器 yield 了一個值而不是終止或傳回特殊異常,則應在產生器端再次引發一個特殊異常。
目前尚不清楚非故意轉換的特殊異常(如「可能的語義」中所討論的)是否是一個問題,以及對此該如何處理。
應探索實作問題。
替代方案構想
應移除 yield 放置限制以及產生器銷毀應觸發 finally 子句執行的想法已被提議多次。單憑這一點無法保證可以強制及時釋放產生器所獲取的資源。
PEP 288 提出了一個更通用的解決方案,允許向產生器傳遞自定義異常。本 PEP 中的提案更直接地解決了資源釋放問題。如果實作了 PEP 288,close 的 Exception 語義可以建立在它的基礎之上;另一方面,PEP 288 應為更通用的功能提供獨立的理據。
版權
此文件已歸入公有領域 (public domain)。
來源:https://github.com/python/peps/blob/main/peps/pep-0325.rst
最後修改日期:2025-02-01 08:59:27 GMT