PEP 340 – 匿名區塊陳述式
- 作者:
- Guido van Rossum
- 狀態:
- 已否決 (Rejected)
- 類型:
- 標準軌跡 (Standards Track)
- 建立日期:
- 2005年4月27日
- 公告歷史:
簡介
本 PEP 提案了一種可用於資源管理目的的新型複合陳述式。這種新的陳述式類型暫定稱為「區塊陳述式」(block-statement),因為其將使用的關鍵字尚未選定。
本 PEP 與其他幾個 PEP 存在競爭關係:PEP 288(產生器屬性與例外;僅限第二部分)、PEP 310(可靠的取得/釋放對),以及 PEP 325(產生器的資源釋放支援)。
我應該澄清一點,使用產生器來「驅動」區塊陳述式實際上是一個可分離的提案;僅憑 PEP 中區塊陳述式的定義,您就可以使用類別來實作所有範例(類似於範例 6,它很容易轉換為範本)。但核心思想是使用產生器來驅動區塊陳述式;其餘都是細節闡述,所以我希望將這兩部分保留在一起。
(PEP 342,增強型迭代器,最初是本 PEP 的一部分;但這兩個提案實際上是獨立的,在 Steven Bethard 的幫助下,我已將其移至單獨的 PEP。)
拒絕通知
我拒絕了本 PEP,轉而支持 PEP 343。請參閱該 PEP 中的動機部分,以了解此拒絕的理由。GvR。
動機與總結
(感謝 Shane Hathaway – 嗨,Shane!)
優秀的程式設計師會將常用程式碼移至可重用的函式中。然而,有時模式出現在函式的結構中,而非實際的陳述式序列。例如,許多函式會取得鎖定、執行該函式特定的程式碼,然後無條件地釋放鎖定。在每個使用鎖定的函式中重複鎖定程式碼容易出錯,並使重構變得困難。
區塊陳述式提供了一種用於封裝結構模式的機制。區塊陳述式內的程式碼在一個稱為「區塊迭代器」的物件控制下執行。簡單的區塊迭代器會在區塊陳述式內的程式碼之前和之後執行程式碼。區塊迭代器也有機會多次執行受控程式碼(或根本不執行),捕獲例外,或從區塊陳述式主體接收資料。
編寫區塊迭代器的一種便捷方式是編寫產生器(PEP 255)。產生器很像 Python 函式,但它不是立即回傳一個值,而是在「yield」陳述式處暫停執行。當產生器用作區塊迭代器時,yield 陳述式會告訴 Python 解譯器暫停區塊迭代器,執行區塊陳述式主體,並在主體執行完畢後恢復區塊迭代器。
當 Python 解譯器遇到基於產生器的區塊陳述式時,其行為如下。首先,解譯器會實例化產生器並開始執行它。產生器會執行與其封裝模式相關的設定工作,例如取得鎖定、開啟檔案、啟動資料庫交易或開始一個迴圈。然後,產生器使用 yield 陳述式將執行權交給區塊陳述式的主體。當區塊陳述式主體完成、引發未捕獲的例外,或使用 continue 陳述式將資料傳回給產生器時,產生器會恢復執行。此時,產生器可以清理並停止,或再次 yield,導致區塊陳述式主體再次執行。當產生器完成時,解譯器會離開區塊陳述式。
使用案例
參閱末尾附近的“範例”章節。
規範:`__exit__()` 方法
提案為迭代器新增一個可選方法,稱為 __exit__()。它最多接受三個引數,這些引數對應於 raise 陳述式的三個「引數」:類型、值和追溯。如果所有三個引數皆為 None,則可以查詢 sys.exc_info() 以提供適當的預設值。
規範:匿名區塊陳述式
提案一個新陳述式,其語法為:
block EXPR1 as VAR1:
BLOCK1
在此,「block」和「as」是新的關鍵字;EXPR1 是一個任意表達式(但不是表達式列表),而 VAR1 是一個任意賦值目標(可以是逗號分隔的列表)。
「as VAR1」部分是可選的;如果省略,則下面翻譯中對 VAR1 的賦值也會省略(但賦值的表達式仍然會被求值!)。
「block」關鍵字的選擇備受爭議;許多替代方案已被提出,包括根本不使用關鍵字(我實際上很喜歡這一點)。PEP 310 使用「with」來表示類似的語義,但我希望將其保留給類似 Pascal 和 VB 中所見的 with 陳述式。(雖然我剛發現 C# 設計者不喜歡「with」[2],而且我同意他們的理由。)為了暫時迴避這個問題,我將使用「block」,直到我們能就正確的關鍵字(如果有的話)達成共識。
請注意,「as」關鍵字沒有爭議(它最終將被提升為正式的關鍵字狀態)。
請注意,由迭代器決定區塊陳述式是否表示一個具有多次迭代的迴圈;在最常見的使用案例中,BLOCK1 只會執行一次。然而,對剖析器來說,它始終是一個迴圈;`break` 和 `continue` 會將控制權轉移回區塊的迭代器(詳情請見下文)。
這種轉換與 for 迴圈有細微不同:iter() 不會被呼叫,因此 EXPR1 應該已經是一個迭代器(而不僅僅是可迭代的);並且保證當區塊陳述式被離開時,迭代器會收到通知,無論這是由於 `break`、`return` 還是例外所致。
itr = EXPR1 # The iterator
ret = False # True if a return statement is active
val = None # Return value, if ret == True
exc = None # sys.exc_info() tuple if an exception is active
while True:
try:
if exc:
ext = getattr(itr, "__exit__", None)
if ext is not None:
VAR1 = ext(*exc) # May re-raise *exc
else:
raise exc[0], exc[1], exc[2]
else:
VAR1 = itr.next() # May raise StopIteration
except StopIteration:
if ret:
return val
break
try:
ret = False
val = exc = None
BLOCK1
except:
exc = sys.exc_info()
(然而,變數「itr」等對使用者不可見,且所使用的內建模組名稱無法被使用者覆寫。)
在 BLOCK1 內部,適用以下特殊轉換:
- 「break」始終是合法的;它會被轉換為:
exc = (StopIteration, None, None) continue
- 「return EXPR3」僅在區塊陳述式包含在函式定義中時才合法;它會被轉換為:
exc = (StopIteration, None, None) ret = True val = EXPR3 continue
最終效果是 `break` 和 `return` 的行為與區塊陳述式作為 for 迴圈時大致相同,不同之處在於,迭代器在區塊陳述式被離開之前,可以透過可選的 __exit__() 方法進行資源清理。如果區塊陳述式透過引發例外而離開,迭代器也會有機會執行清理。如果迭代器沒有 __exit__() 方法,則與 for 迴圈沒有區別(除了 for 迴圈會對 EXPR1 呼叫 iter())。
請注意,區塊陳述式中的 yield 陳述式並未被特殊處理。它會暫停包含該區塊的函式,**而不會**通知區塊的迭代器。區塊的迭代器完全不知道這次 yield,因為本地控制流實際上並未離開區塊。換句話說,它**不像** `break` 或 `return` 陳述式。當由 yield 恢復的迴圈呼叫 next() 時,該區塊會在 yield 之後立即恢復。 (請參見下面的範例 7。)下面描述的產生器終結語義保證(在所有終結語義的限制內)該區塊最終將被恢復。
與 for 迴圈不同,區塊陳述式沒有 else 子句。我認為它會令人困惑,並強調區塊陳述式的「迴圈性」,而我希望強調它與 for 迴圈的**區別**。此外,else 子句有幾種可能的語義,並且只有非常弱的使用案例。
規範:產生器退出處理
產生器將實作新的 __exit__() 方法 API。
產生器將被允許在 `try-finally` 陳述式內部包含 yield 陳述式。
yield 陳述式的表達式引數將變為可選(預設為 None)。
當 __exit__() 被呼叫時,產生器會恢復執行,但在 yield 陳述式處,會引發由 __exit__ 引數所代表的例外。產生器可以重新引發此例外、引發另一個例外,或 yield 另一個值,除非傳遞給 __exit__() 的例外是 `StopIteration`,那麼它應該引發 `StopIteration`(否則效果將是 `break` 變為 `continue`,這至少是出乎意料的)。當恢復產生器的**初始**呼叫是 __exit__() 呼叫而非 next() 呼叫時,產生器的執行會被中止,且例外會被重新引發,而不會將控制權傳遞給產生器的主體。
當一個尚未終止的產生器被垃圾回收時(透過引用計數或循環式垃圾回收器),其 __exit__() 方法會被呼叫一次,並以 `StopIteration` 作為其第一個引數。再加上要求產生器在 __exit__() 被以 `StopIteration` 呼叫時應引發 `StopIteration`,這保證了當產生器最後一次暫停時任何活躍的 `finally` 子句最終都會被啟用。當然,在某些情況下,產生器可能永遠不會被垃圾回收。這與其他物件的終結器(__del__() 方法)所作的保證沒有什麼不同。
考量與捨棄的替代方案
- 許多「block」的替代方案已被提出。我還沒有看到比「block」更好的關鍵字提案。唉,「block」也不是一個好的選擇;它對於變數、引數和方法來說是一個相當受歡迎的名稱。或許「with」才是最好的選擇?
- 區塊陳述式可以不嘗試選擇理想的關鍵字,而只是採用這種形式:
EXPR1 as VAR1: BLOCK1
這最初很有吸引力,因為如果配合在
EXPR1中使用恰當的函式名稱(如下方「範例」部分所示),它讀起來很順暢,感覺就像一個「使用者定義的陳述式」。然而,它讓我(和許多其他人)感到不適;沒有關鍵字,語法會非常「平淡」,難以在手冊中查詢(請記住「as」是可選的),而且它會使 `break` 和 `continue` 在區塊陳述式中的意義更加令人困惑。 - Phillip Eby 提議讓區塊陳述式使用與 for 迴圈完全不同的 API,以區分兩者。產生器必須被裝飾器包裝起來才能支援區塊 API。我認為這會增加更多複雜性,而好處卻很少;而且我們無法否認區塊陳述式在概念上是一個迴圈——畢竟它支援 `break` 和 `continue`。
- 這個提案一直被提出:「block VAR1 = EXPR1」而不是「block EXPR1 as VAR1」。這會非常誤導人,因為 VAR1 **並不會**被賦予 EXPR1 的值;EXPR1 會產生一個產生器,該產生器被賦予一個內部變數,而 VAR1 是該迭代器連續呼叫
__next__()方法所回傳的值。 - 為什麼不將轉換改為應用
iter(EXPR1)呢?所有範例都會繼續運作。但這會使區塊陳述式**更像**一個 for 迴圈,而重點應該放在兩者之間的**區別**。不呼叫iter()可以避免許多誤解,例如將序列用作EXPR1。
與 Thunks (延遲計算函式) 的比較
為區塊陳述式提出的替代語義會將區塊轉換為一個 thunk (延遲計算函式)(一個融入包含範圍的匿名函式)。
我能看到 thunks 的主要優點是您可以將 thunk 儲存起來供以後使用,就像按鈕小工具的回呼一樣(thunk 隨後會成為一個閉包)。您無法為此使用基於 yield 的區塊(除了 Ruby,它使用 yield 語法和基於 thunk 的實作)。但我不得不說,我幾乎將其視為一個優點:我認為看到一個區塊卻不知道它是在正常控制流中執行還是稍後執行,我會感到有些不適。為此目的定義一個明確的巢狀函式對我來說沒有這個問題,因為我已經知道「def」關鍵字意味著其主體稍後執行。
thunks 的另一個問題是,一旦我們將它們視為匿名函式,我們就不得不說 thunk 中的 `return` 陳述式是從 thunk 而非從包含函式回傳。以任何其他方式執行,當 thunk 作為閉包在其包含函式之後仍然存在時,會導致嚴重的怪異行為(也許續接可以提供幫助,但我不想深入探討 :-)。
但這樣一來,我認為對於資源清理範本模式的一個重要使用案例就消失了。我通常會這樣寫程式碼:
def findSomething(self, key, default=None):
self.lock.acquire()
try:
for item in self.elements:
if item.matches(key):
return item
return default
finally:
self.lock.release()
如果我不能這樣寫,我會感到很沮喪:
def findSomething(self, key, default=None):
block locking(self.lock):
for item in self.elements:
if item.matches(key):
return item
return default
這個特定範例可以使用 `break` 重新編寫:
def findSomething(self, key, default=None):
block locking(self.lock):
for item in self.elements:
if item.matches(key):
break
else:
item = default
return item
但這看起來很勉強,而且轉換並非總是那麼容易;您會被迫以單一回傳風格重寫您的程式碼,這感覺過於限制。
另請注意 thunk 中 yield 的語義難題——唯一合理的解釋是這會將 thunk 轉換為產生器!
Greg Ewing 認為 thunks「會簡單得多,只做需要做的事情,而無需在例外和 `break`/`continue`/`return` 陳述式上玩任何花招。很容易解釋它的作用以及為什麼它有用。」
但是,為了在 thunk 和包含函式之間實現所需的區域變數共享,thunk 中使用或設定的每個區域變數都必須變成一個「cell」(我們在巢狀作用域之間共享變數的機制)。與常規區域變數相比,cells 會降低存取速度:存取涉及額外的 C 函式呼叫(PyCell_Get() 或 PyCell_Set())。
或許並非巧合,上面的最後一個範例(findSomething() 經重寫以避免在區塊內部回傳)顯示,與常規巢狀函式不同,我們會希望由 thunk **賦值**的變數也能與包含函式共享,即使這些變數在 thunk 外部並未被賦值。
Greg Ewing 再度提到:「產生器已被證明更為強大,因為您可以同時執行多個。在這裡這種能力有什麼用途嗎?」
我相信這絕對有其用途;幾位人士已經展示了如何使用產生器來實現非同步輕量級執行緒(例如 PEP 288 中引述的 David Mertz,以及 Fredrik Lundh [3])。
最後,Greg 說:「thunk 的實作有可能輕鬆處理多個區塊引數,如果能設計出合適的語法的話。很難想像這在產生器實作中如何以一般方式實現。」
然而,多個區塊的使用案例似乎難以捉摸。
(此後已經有人提出了改變 thunks 實作的提案,以消除大部分這些反對意見,但由此產生的語義相當複雜,難以解釋和實作,所以我認為這從一開始就違背了使用 thunks 的目的。)
範例
(其中幾個範例包含「yield None」。如果 PEP 342 被接受,這些當然可以改為僅「yield」。)
- 一個確保在區塊開頭獲取的鎖在離開區塊時被釋放的模板:
def locking(lock): lock.acquire() try: yield None finally: lock.release()
使用方式如下:
block locking(myLock): # Code here executes with myLock held. The lock is # guaranteed to be released when the block is left (even # if via return or by an uncaught exception).
- 一個用於開啟檔案的模板,確保在離開區塊時檔案被關閉:
def opening(filename, mode="r"): f = open(filename, mode) try: yield f finally: f.close()
使用方式如下:
block opening("/etc/passwd") as f: for line in f: print line.rstrip()
- 一個用於提交或回滾資料庫交易的模板:
def transactional(db): try: yield None except: db.rollback() raise else: db.commit()
- 一個嘗試某事最多 n 次的範本
def auto_retry(n=3, exc=Exception): for i in range(n): try: yield None return except exc, err: # perhaps log exception here continue raise # re-raise the exception we caught earlier
使用方式如下:
block auto_retry(3, IOError): f = urllib.urlopen("https://www.example.com/") print f.read()
- 可以巢狀區塊並結合範本
def locking_opening(lock, filename, mode="r"): block locking(lock): block opening(filename) as f: yield f
使用方式如下:
block locking_opening(myLock, "/etc/passwd") as f: for line in f: print line.rstrip()
(如果這個範例讓您感到困惑,請考慮它等同於在一個常規產生器中使用一個帶有 yield 的 for 迴圈,該產生器遞迴地呼叫另一個迭代器或產生器;例如,請參閱
os.walk()的原始碼。) - 可以編寫一個具有範例 1 語義的常規迭代器
class locking: def __init__(self, lock): self.lock = lock self.state = 0 def __next__(self, arg=None): # ignores arg if self.state: assert self.state == 1 self.lock.release() self.state += 1 raise StopIteration else: self.lock.acquire() self.state += 1 return None def __exit__(self, type, value=None, traceback=None): assert self.state in (0, 1, 2) if self.state == 1: self.lock.release() raise type, value, traceback
(這個範例可以輕易修改以實作其他範例;它顯示了產生器用於相同目的時是多麼簡單。)
- 暫時重新導向 stdout:
def redirecting_stdout(new_stdout): save_stdout = sys.stdout try: sys.stdout = new_stdout yield None finally: sys.stdout = save_stdout
使用方式如下:
block opening(filename, "w") as f: block redirecting_stdout(f): print "Hello world"
opening()的一個變體,它也回傳一個錯誤狀況def opening_w_error(filename, mode="r"): try: f = open(filename, mode) except IOError, err: yield None, err else: try: yield f, None finally: f.close()
使用方式如下:
block opening_w_error("/etc/passwd", "a") as f, err: if err: print "IOError:", err else: f.write("guido::0:0::/:/bin/sh\n")
致謝
沒有特定的順序:Alex Martelli, Barry Warsaw, Bob Ippolito, Brett Cannon, Brian Sabbey, Chris Ryland, Doug Landauer, Duncan Booth, Fredrik Lundh, Greg Ewing, Holger Krekel, Jason Diamond, Jim Jewett, Josiah Carlson, Ka-Ping Yee, Michael Chermside, Michael Hudson, Neil Schemenauer, Alyssa Coghlan, Paul Moore, Phillip Eby, Raymond Hettinger, Georg Brandl, Samuele Pedroni, Shannon Behrens, Skip Montanaro, Steven Bethard, Terry Reedy, Tim Delaney, Aahz, 以及其他各位。感謝所有寶貴的貢獻!
參考文獻
[1] https://mail.python.org/pipermail/python-dev/2005-April/052821.html
版權
此文件已歸入公有領域 (public domain)。
來源:https://github.com/python/peps/blob/main/peps/pep-0340.rst