PEP 340 – 匿名块语句
- 作者:
- Guido van Rossum
- 状态:
- 已拒绝
- 类型:
- 标准跟踪
- 创建:
- 2005-04-27
- 更新历史:
简介
本 PEP 提出了一种新的复合语句类型,可用于资源管理目的。这种新的语句类型暂称为块语句,因为尚未选择要使用的关键字。
本 PEP 与其他几个 PEP 竞争:PEP 288(生成器属性和异常;仅第二部分)、PEP 310(可靠的获取/释放对)和 PEP 325(生成器的资源释放支持)。
我应该澄清一下,使用生成器来“驱动”块语句实际上是一个可分离的提案;仅使用 PEP 中的块语句定义,就可以使用类实现所有示例(类似于示例 6,它很容易转换为模板)。但关键思想是使用生成器来驱动块语句;其余部分是阐述,所以我想将这两个部分放在一起。
(PEP 342,增强迭代器,最初是本 PEP 的一部分;但这两个提案实际上是独立的,在 Steven Bethard 的帮助下,我已经将其移到了一个单独的 PEP 中。)
拒绝通知
我拒绝了这个 PEP,转而支持 PEP 343。请参阅该 PEP 中的动机部分,了解拒绝该 PEP 的原因。GvR。
动机和摘要
(感谢 Shane Hathaway - Hi 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 的要求,这保证了在生成器上次挂起时处于活动状态的任何 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)
?所有的示例都会继续工作。但这会使block语句**更像**一个for循环,而重点应该放在**两者之间的区别**上。不调用iter()
可以避免很多误解,比如使用序列作为EXPR1
。
与 thunk 的比较
针对block语句提出的替代语义将block变成一个thunk(一个匿名函数,融入到包含作用域中)。
我认为thunk的主要优势在于你可以将thunk保存以备后用,就像按钮小部件的回调函数一样(thunk 此时变成了闭包)。你无法将基于yield的block用于此(除了 Ruby,它使用yield语法,但实现基于thunk)。但我不得不说,我几乎认为这是一个优势:我认为看到一个block,却不知道它是在正常的控制流中执行还是稍后执行,会让我有点不舒服。为此目的定义一个显式嵌套函数不会让我有这种感觉,因为我已经知道“def”关键字意味着它的主体会在稍后执行。
thunk的另一个问题是,一旦我们将它们视为匿名函数,我们几乎被迫说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 认为 thunk “会简单很多,只做必要的事情,而无需对异常和break/continue/return 语句进行任何花招。解释它做什么以及为什么有用会很容易。”
但是,为了获得 thunk 和包含函数之间所需的局部变量共享,在 thunk 中使用或设置的每个局部变量都必须成为一个“单元格”(我们用于在嵌套作用域之间共享变量的机制)。与普通局部变量相比,单元格会减慢访问速度:访问涉及一个额外的 C 函数调用(PyCell_Get()
或 PyCell_Set()
)。
也许并非完全巧合的是,上面的最后一个例子(findSomething()
重写以避免在 block 中使用 return)表明,与普通嵌套函数不同,我们希望 thunk **赋值**的变量也与包含函数共享,即使它们没有在 thunk 外部赋值。
Greg Ewing 又说:“生成器已经证明更强大,因为你可以同时使用多个生成器。这里是否有这种能力的用武之地?”
我相信确实有这种用武之地;很多人已经展示了如何使用生成器来实现异步轻量级线程(例如,David Mertz 在PEP 288中引用过,Fredrik Lundh [3]也是)。
最后,Greg 说:“thunk 实现有潜力轻松地处理多个 block 参数,如果能找到合适的语法。很难想象如何用生成器实现以通用方式做到这一点。”
然而,多个 block 的用例似乎很模糊。
(后来有人提出要改变 thunk 的实现,以消除大多数这些反对意见,但产生的语义解释起来和实现起来都相当复杂,因此我认为这违背了使用 thunk 的初衷。)
示例
(这些示例中的几个包含“yield None”。如果PEP 342被接受,当然可以将其更改为“yield”。)
- 用于确保在 block 开始时获取的锁在 block 退出时释放的模板
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).
- 用于打开文件的模板,确保在 block 退出时关闭文件
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()
- 可以嵌套 block 并组合模板
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
版权
本文件已置于公有领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0340.rst