PEP 346 – 用户定义的(”with
”)语句
- 作者:
- Alyssa Coghlan <ncoghlan at gmail.com>
- 状态:
- 已撤回
- 类型:
- 标准跟踪
- 创建:
- 2005年5月6日
- Python 版本:
- 2.5
- 发布历史:
摘要
本 PEP 结合了 PEP 310 的“可靠的获取/释放对”和 Guido 的 PEP 340 的“匿名块语句”。本 PEP 旨在提取 PEP 340 的优点,将其与 PEP 310 的部分内容混合,并将其重新整理成一个优雅的整体。它借鉴了其他几个 PEP,以描绘一幅完整的画面,并旨在独立存在。
引言
本 PEP 建议通过引入新的 with
语句来增强 Python 可靠地管理资源的能力,该语句允许分解任意 try
/finally
和一些 try
/except
/else
样板代码。新结构称为“用户定义语句”,相关的类定义称为“语句模板”。
以上是 PEP 的主要内容。但是,如果这只是它说的一切,那么 PEP 310 就足够了,本 PEP 基本上就是多余的。相反,本 PEP 建议进行其他增强,使使用适当装饰的生成器编写这些语句模板变得自然。这些增强的一个副作用是,正确处理生成器内部的资源管理变得很重要。
这与 PEP 343 非常相似,但是发生的异常在生成器的框架内重新引发,因此需要解决生成器结束的问题。本 PEP 建议的模板生成器装饰器还创建可重用模板,而不是 PEP 340 的一次性模板。
与 PEP 340 相比,本 PEP 消除了抑制异常的能力,并将用户定义语句设为非循环结构。另一个主要区别是使用装饰器将生成器转换为语句模板,以及纳入解决迭代器结束的思想。
如果所有这些看起来像是一项雄心勃勃的操作……好吧,当 Guido 编写 PEP 340 时,是他将标准提高到如此之高 :)
与其他 PEP 的关系
本 PEP 与 PEP 310、PEP 340 和 PEP 343 直接竞争,因为这些 PEP 都描述了处理确定性资源管理的替代机制。
它不与 PEP 342 竞争,后者将 PEP 340 与将数据传递到迭代器相关的增强功能分离开来。对 for
循环语义的相关更改将与本 PEP 中建议的迭代器结束更改相结合。用户定义语句将不受影响。
本 PEP 也不与 PEP 288 中描述的生成器增强功能竞争。虽然本 PEP 提出了将异常注入生成器框架的能力,但这只是一个内部实现细节,不需要向 Python 代码公开此能力。PEP 288 部分是为了使该实现细节易于访问。
但是,本 PEP 将使 PEP 325 中描述的生成器资源释放支持变得多余——需要结束的迭代器应提供语句模板协议的适当实现。
用户定义语句
为了从 PEP 310 中窃取激励示例,同步锁的正确处理目前如下所示
the_lock.acquire()
try:
# Code here executes with the lock held
finally:
the_lock.release()
与 PEP 310 一样,本 PEP 建议能够将此类代码编写为
with the_lock:
# Code here executes with the lock held
这些用户定义语句主要旨在允许轻松分解不容易转换为函数的 try
块。当异常处理模式一致但 try
块的主体发生变化时,这种情况最常见。使用用户定义语句,可以轻松地将异常处理分解到语句模板中,并在用户代码中内联提供 try
子句的主体。
术语“用户定义语句”反映了一个事实,即 with
语句的含义主要由使用的语句模板决定,程序员可以自由创建自己的语句模板,就像他们可以自由创建自己的迭代器在 for
循环中使用一样。
用户定义语句的使用语法
建议的语法很简单
with EXPR1 [as VAR1]:
BLOCK1
用户定义语句的语义
the_stmt = EXPR1
stmt_enter = getattr(the_stmt, "__enter__", None)
stmt_exit = getattr(the_stmt, "__exit__", None)
if stmt_enter is None or stmt_exit is None:
raise TypeError("Statement template required")
VAR1 = stmt_enter() # Omit 'VAR1 =' if no 'as' clause
exc = (None, None, None)
try:
try:
BLOCK1
except:
exc = sys.exc_info()
raise
finally:
stmt_exit(*exc)
除了 VAR1
之外,上面显示的局部变量都不会对用户代码可见。像 for
循环中的迭代变量一样,VAR1
在 BLOCK1
和用户定义语句之后的代码中都是可见的。
请注意,语句模板只能对异常做出反应,不能抑制它们。有关原因,请参阅 已拒绝的选项。
语句模板协议:__enter__
__enter__()
方法不接受任何参数,如果它引发异常,则永远不会执行 BLOCK1
。如果发生这种情况,则不会调用 __exit__()
方法。如果使用了 as
子句,则此方法返回的值将分配给 VAR1。没有其他返回值的对象通常应返回 self
而不是 None
,以允许在 with
语句中就地创建。
语句模板应使用此方法来设置语句执行期间存在的条件(例如,获取同步锁)。
并非始终可用的语句模板(例如已关闭的文件对象)如果在模板处于无效状态时尝试调用__enter__()
,则应引发RuntimeError
。
语句模板协议:__exit__
__exit__()
方法接受三个参数,分别对应于raise
语句的三个“参数”:类型、值和回溯。始终提供所有参数,如果未发生异常,则将其设置为None
。如果__enter__()
方法成功完成,则此方法将由with
语句机制准确调用一次。
语句模板在此方法中执行其异常处理。如果第一个参数为None
,则表示BLOCK1
的非异常完成 - 执行要么到达块的末尾,要么使用return
、break
或continue
语句强制提前完成。否则,这三个参数反映了终止BLOCK1
的异常。
__exit__()
方法引发的任何异常都将传播到包含with
语句的作用域。如果BLOCK1
中的用户代码也引发了异常,则该异常将丢失,并被__exit__()
方法引发的异常替换。
分解任意异常处理
考虑以下异常处理安排
SETUP_BLOCK
try:
try:
TRY_BLOCK
except exc_type1, exc:
EXCEPT_BLOCK1
except exc_type2, exc:
EXCEPT_BLOCK2
except:
EXCEPT_BLOCK3
else:
ELSE_BLOCK
finally:
FINALLY_BLOCK
它可以大致转换为以下语句模板:
class my_template(object):
def __init__(self, *args):
# Any required arguments (e.g. a file name)
# get stored in member variables
# The various BLOCK's will need updating to reflect
# that.
def __enter__(self):
SETUP_BLOCK
def __exit__(self, exc_type, value, traceback):
try:
try:
if exc_type is not None:
raise exc_type, value, traceback
except exc_type1, exc:
EXCEPT_BLOCK1
except exc_type2, exc:
EXCEPT_BLOCK2
except:
EXCEPT_BLOCK3
else:
ELSE_BLOCK
finally:
FINALLY_BLOCK
然后可以将其用作
with my_template(*args):
TRY_BLOCK
但是,此代码与原始try
语句之间存在两个重要的语义差异。
首先,在原始的try
语句中,如果在TRY_BLOCK
中遇到break
、return
或continue
语句,则仅执行FINALLY_BLOCK
,因为语句已完成。使用语句模板,ELSE_BLOCK
也将执行,因为这些语句被视为任何其他非异常块终止。对于有关系例,这很可能是一件好事(请参阅示例中的transaction
),因为在编写异常处理程序时,很容易忘记这个既不执行except
也不执行else
子句的漏洞。
其次,语句模板不会抑制任何异常。例如,如果原始代码抑制了exc_type1
和exc_type2
异常,那么仍然需要在用户代码中内联执行此操作
try:
with my_template(*args):
TRY_BLOCK
except (exc_type1, exc_type2):
pass
但是,即使在需要明确抑制异常的这些情况下,在调用站点重复的样板代码量也大大减少了(有关此行为的进一步讨论,请参阅已拒绝的选项)。
通常,不需要所有子句。对于资源处理(如文件或同步锁),可以简单地在__exit__()
方法中执行本应作为FINALLY_BLOCK
一部分的代码。这可以在以下实现中看到,该实现将同步锁转换为语句模板,如本节开头所述
# New methods of synchronisation lock objects
def __enter__(self):
self.acquire()
return self
def __exit__(self, *exc_info):
self.release()
生成器
生成器能够暂停执行并将控制权返回给调用帧,因此是编写语句模板的自然候选者。向语言添加用户定义的语句*不需要*本节中描述的生成器更改,因此使本PEP成为分阶段实施的明显候选者(第1阶段中的with
语句,第2阶段中的生成器集成)。建议的生成器更新允许将任意异常处理分解如下
@statement_template
def my_template(*arguments):
SETUP_BLOCK
try:
try:
yield
except exc_type1, exc:
EXCEPT_BLOCK1
except exc_type2, exc:
EXCEPT_BLOCK2
except:
EXCEPT_BLOCK3
else:
ELSE_BLOCK
finally:
FINALLY_BLOCK
请注意,与基于类的版本不同,不需要修改任何块,因为共享值是生成器内部帧的局部变量,包括调用代码传入的参数。前面提到的语义差异(所有非异常块终止都触发else
子句,并且模板无法抑制异常)仍然适用。
yield
的默认值
使用生成器创建语句模板时,yield
语句通常仅用于将控制权返回给用户定义语句的主体,而不是返回有用的值。
因此,如果接受本PEP,则yield
与return
一样,将提供None
的默认值(即yield
和yield None
将成为等效语句)。
在PEP 342中也建议进行此更改。显然,如果两个PEP都被接受,则只需要实现一次:)。
模板生成器装饰器:statement_template
与PEP 343一样,建议使用一个新的装饰器将生成器包装在一个具有适当语句模板语义的对象中。与PEP 343不同,此处建议的模板是可重用的,因为生成器在每次调用__enter__()
时都会重新实例化。此外,BLOCK1
中发生的任何异常都会在生成器的内部帧中重新引发
class template_generator_wrapper(object):
def __init__(self, func, func_args, func_kwds):
self.func = func
self.args = func_args
self.kwds = func_kwds
self.gen = None
def __enter__(self):
if self.gen is not None:
raise RuntimeError("Enter called without exit!")
self.gen = self.func(*self.args, **self.kwds)
try:
return self.gen.next()
except StopIteration:
raise RuntimeError("Generator didn't yield")
def __exit__(self, *exc_info):
if self.gen is None:
raise RuntimeError("Exit called without enter!")
try:
try:
if exc_info[0] is not None:
self.gen._inject_exception(*exc_info)
else:
self.gen.next()
except StopIteration:
pass
else:
raise RuntimeError("Generator didn't stop")
finally:
self.gen = None
def statement_template(func):
def factory(*args, **kwds):
return template_generator_wrapper(func, args, kwds)
return factory
模板生成器包装器:__enter__()
方法
模板生成器包装器具有一个__enter__()
方法,该方法创建包含生成器的新实例,然后调用next()
一次。如果最后一个生成器实例尚未清理,或者生成器终止而不是生成值,它将引发RuntimeError
。
模板生成器包装器:__exit__()
方法
模板生成器包装器具有一个__exit__()
方法,如果未传入异常,则该方法只需在生成器上调用next()
。如果传入异常,则在最后一个yield
语句处在包含的生成器中重新引发该异常。
在这两种情况下,如果内部帧没有因操作而终止,则生成器包装器将引发RuntimeError。__exit__()
方法将始终清理对已用生成器实例的引用,允许再次调用__enter__()
。
用户定义语句的主体引发的StopIteration
可能会在__exit__()
方法内部意外抑制,但这并不重要,因为最初引发的异常仍然正确传播。
将异常注入生成器
要实现模板生成器包装器的__exit__()
方法,需要将异常注入到生成器的内部帧中。这是没有当前Python等效项的新实现级行为。
注入机制(在本PEP中称为_inject_exception
)在生成器的帧中引发异常,并使用指定的类型、值和回溯信息。这意味着如果允许异常传播,则该异常看起来与原始异常相同。
出于本PEP的目的,无需在Python实现代码之外提供此功能。
生成器结束
为了支持模板生成器中的资源管理,本PEP将消除对try
/finally
语句的try
块内的yield
语句的限制。因此,需要使用文件或某些对象的生成器可以通过使用try
/finally
或with
语句确保对象得到正确管理。
此限制可能需要在全局范围内解除 - 将其限制为仅允许在用作语句模板的生成器内部使用将很困难。因此,本PEP包含旨在确保不用作语句模板的生成器仍能适当地完成最终化的建议。
生成器结束:TerminateIteration
异常
提出了一种新的异常
class TerminateIteration(Exception): pass
将新异常注入生成器以请求最终化。良好行为的代码不应抑制它。
生成器结束:__del__()
方法
为了确保生成器最终完成(在Python垃圾回收的限制范围内),生成器将获得具有以下语义的__del__()
方法
def __del__(self):
try:
self._inject_exception(TerminateIteration, None, None)
except TerminateIteration:
pass
确定性生成器结束
有一种简单的方法可以提供生成器的确定性最终化 - 为它们提供适当的__enter__()
和__exit__()
方法
def __enter__(self):
return self
def __exit__(self, *exc_info):
try:
self._inject_exception(TerminateIteration, None, None)
except TerminateIteration:
pass
然后,可以通过将相关for
循环包装在with
语句中来立即完成任何生成器
with all_lines(filenames) as lines:
for line in lines:
print lines
(有关all_lines
的定义及其需要立即完成最终化的原因,请参阅示例)
将上述示例与文件对象的用法进行比较
with open(filename) as f:
for line in f:
print f
生成器作为用户定义语句模板
当用于实现用户定义语句时,生成器在给定的控制路径上应该只产生一次 yield。该 yield 的结果将作为生成器的 __enter__()
方法的结果提供。在每个控制路径上只有一个 yield
可以确保当生成器的 __exit__()
方法被调用时,内部框架将终止。在单个控制路径上使用多个 yield
语句将导致 __exit__()
方法在内部框架无法正确终止时引发 RuntimeError
。此类错误表示语句模板中的错误。
为了响应异常或清理资源,只需将 yield
语句包装在一个构造适当的 try
语句中即可。如果执行在没有异常的情况下从 yield
之后恢复,则生成器知道 do
语句的主体已顺利完成。
示例
- 一个确保在块开始时获取的锁在离开块时释放的模板
# New methods on synchronisation locks def __enter__(self): self.acquire() return self def __exit__(self, *exc_info): lock.release()
用法如下
with 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).
- 一个打开文件的模板,确保在离开块时关闭文件
# New methods on file objects def __enter__(self): if self.closed: raise RuntimeError, "Cannot reopen closed file handle" return self def __exit__(self, *args): self.close()
用法如下
with open("/etc/passwd") as f: for line in f: print line.rstrip()
- 一个提交或回滚数据库事务的模板
def transaction(db): try: yield except: db.rollback() else: db.commit()
用法如下
with transaction(the_db): make_table(the_db) add_data(the_db) # Getting to here automatically triggers a commit # Any exception automatically triggers a rollback
- 可以嵌套块并组合模板
@statement_template def lock_opening(lock, filename, mode="r"): with lock: with open(filename, mode) as f: yield f
用法如下
with lock_opening(myLock, "/etc/passwd") as f: for line in f: print line.rstrip()
- 临时重定向 stdout
@statement_template def redirected_stdout(new_stdout): save_stdout = sys.stdout try: sys.stdout = new_stdout yield finally: sys.stdout = save_stdout
用法如下
with open(filename, "w") as f: with redirected_stdout(f): print "Hello world"
- 一个
open()
的变体,它也返回错误条件@statement_template def open_w_error(filename, mode="r"): try: f = open(filename, mode) except IOError, err: yield None, err else: try: yield f, None finally: f.close()
用法如下
do open_w_error("/etc/passwd", "a") as f, err: if err: print "IOError:", err else: f.write("guido::0:0::/:/bin/sh\n")
- 查找具有特定头的第一个文件
for name in filenames: with open(name) as f: if f.read(2) == 0xFEB0: break
- 查找您可以处理的第一个项目,为整个循环或每次迭代保持锁
with lock: for item in items: if handle(item): break for item in items: with lock: if handle(item): break
- 在生成器内部保持锁,但在将控制权返回到外部作用域时释放它
@statement_template def released(lock): lock.release() try: yield finally: lock.acquire()
用法如下
with lock: for item in items: with released(lock): yield item
- 读取来自文件集合的行(例如,处理多个配置源)
def all_lines(filenames): for name in filenames: with open(name) as f: for line in f: yield line
用法如下
with all_lines(filenames) as lines: for line in lines: update_config(line)
- 并非所有用途都需要涉及资源管理
@statement_template def tag(*args, **kwds): name = cgi.escape(args[0]) if kwds: kwd_pairs = ["%s=%s" % cgi.escape(key), cgi.escape(value) for key, value in kwds] print '<%s %s>' % name, " ".join(kwd_pairs) else: print '<%s>' % name yield print '</%s>' % name
用法如下
with tag('html'): with tag('head'): with tag('title'): print 'A web page' with tag('body'): for par in pars: with tag('p'): print par with tag('a', href="https://pythonlang.cn"): print "Not a dead parrot!"
- 来自 PEP 343,另一个有用的例子是一个阻止信号的操作。用法可以是这样的
from signal import blocked_signals with blocked_signals(): # code executed without worrying about signals
一个可选参数可能是一个要阻塞的信号列表;默认情况下,所有信号都被阻塞。实现留给读者作为练习。
- 此功能的另一个用途是用于十进制上下文
# New methods on decimal Context objects def __enter__(self): if self._old_context is not None: raise RuntimeError("Already suspending other Context") self._old_context = getcontext() setcontext(self) def __exit__(self, *args): setcontext(self._old_context) self._old_context = None
用法如下
with decimal.Context(precision=28): # Code here executes with the given context # The context always reverts after this statement
未解决的问题
无,因为此 PEP 已被撤回。
已拒绝的选项
使基本结构成为循环结构
正如 PEP 340 的 block
语句所示,这个想法的主要问题是它会导致在循环内部的 try
语句的分解出现问题,并且包含 break
和 continue
语句(因为这些语句将应用于 block
结构,而不是原始循环)。由于一个关键目标是能够将任意异常处理(除了抑制)分解到语句模板中,这是一个确定的问题。
也存在一个可理解性问题,这可以在 示例 中看到。在显示获取整个循环或循环的每次迭代的锁的示例中,如果用户定义的语句本身是一个循环,则将其从 for
循环外部移动到 for
循环内部将具有重大的语义影响,超出了人们的预期。
最后,对于循环结构,存在着关于 TOOWTDI 的重大问题,因为经常不清楚特定情况应该使用传统的 for
循环还是新的循环结构来处理。使用当前的 PEP,不存在此类问题 - for
循环继续用于迭代,而新的 do
语句用于分解异常处理。
另一个问题,特别是与 PEP 340 的匿名块语句相关,是它们使得直接编写语句模板变得非常困难(即不使用生成器)。当前的提案解决了这个问题,这可以通过 示例 中基于类的语句模板的各种实现的相对简单性来看出。
允许语句模板抑制异常
此 PEP 的早期版本赋予语句模板抑制异常的能力。BDFL 对相关的复杂性表示担忧,并且在阅读了 Raymond Chen 关于隐藏 C 代码宏中流控制的弊端的一篇文章后,我表示同意 [1]。
删除抑制能力消除了用户定义语句的解释和实现中的大量复杂性,进一步支持它作为正确选择。PEP 的旧版本不得不跳过一些可怕的障碍以避免在 __exit__()
方法中意外地抑制异常 - 在当前建议的语义下,该问题不存在。
有一个示例(auto_retry
)实际上使用了抑制异常的能力。这个用例虽然不那么优雅,但在用户代码中完全写出来时,控制流要明显得多
def attempts(num_tries):
return reversed(xrange(num_tries))
for retry in attempts(3):
try:
make_attempt()
except IOError:
if not retry:
raise
对于它的价值,那些别有用心的开发者仍然可以这样写
for attempt in auto_retry(3, IOError):
try:
with attempt:
make_attempt()
except FailedAttempt:
pass
为了保护无辜者,这里没有包含实际支持该功能的代码。
区分非异常退出
此 PEP 的早期版本允许语句模板区分正常退出块和通过 return
、break
或 continue
语句退出。BDFL 在 PEP 343 及其相关的讨论中也曾考虑过类似的想法。这增加了对语义描述的显著复杂性,并且它要求每个语句模板都决定是否应该将这些语句视为异常,或者视为退出块的正常机制。
这个模板逐个决策过程极有可能造成混淆 - 考虑如果一个数据库连接器提供了一个将早期退出视为异常的事务模板,而第二个连接器将它们视为正常的块终止。
因此,此 PEP 现在使用最简单的解决方案 - 就语句模板而言,早期退出与正常的块终止看起来相同。
不将引发的异常注入生成器
PEP 343 建议简单地对用于定义语句模板的生成器无条件调用 next()。这意味着模板生成器最终看起来相当不直观,并且保留了在 try
/finally
内部产生 yield 的禁令,这意味着 Python 的异常处理功能无法用于处理多个资源的管理。
此 PEP 提倡的替代方案(将引发的异常注入到生成器框架中)意味着可以像 示例 中的 lock_opening
所示的那样优雅地管理多个资源
使所有生成器都成为语句模板
将模板对象与生成器本身分离使得拥有可重用的生成器模板成为可能。也就是说,如果接受此 PEP,以下代码将正常工作
open_it = lock_opening(parrot_lock, "dead_parrot.txt")
with open_it as f:
# use the file for a while
with open_it as f:
# use the file again
第二个好处是迭代器生成器和模板生成器是非常不同的东西 - 装饰器使这种区别清晰,并防止一个被用于需要另一个的地方。
最后,需要装饰器才能使用生成器对象的原生方法来实现生成器终结。
使用 do
作为关键字
do
是在 PEP 340 讨论期间提出的一个备选关键字。它与命名恰当的函数一起读起来很好,但与方法或提供原生语句模板支持的对象一起读起来很糟糕。
当 do
首次被提出时,BDFL 拒绝了 PEP 310 的 with
关键字,基于希望将其用于 Pascal/Delphi 风格的 with
语句。从那时起,BDFL 已撤回此反对意见,因为他不再打算提供此类语句。这种改变想法显然是基于 C# 开发人员不提供此功能的原因 [2]。
不使用关键字
这是一个有趣的选择,并且可以使其读起来相当好。但是,对于新用户来说,在文档中查找它很笨拙,并且有些人认为它过于神奇。因此,此 PEP 使用基于关键字的建议。
增强 try
语句
此建议涉及为裸 try
语句提供类似于为 with
语句提出的签名。
我认为尝试将 with
语句写成增强的 try
语句与尝试将 for
循环写成增强的 while
循环一样有意义。也就是说,虽然前者的语义可以解释为使用后者的特定方式,但前者不是后者的 *实例*。围绕更基本语句添加的额外语义导致了一个新的结构,并且不应该将这两个不同的语句混淆。
这可以通过以下事实看出:“增强”的 try
语句仍然需要用“非增强”的 try
语句来解释。如果它与众不同,那么给它一个不同的名称更有意义。
使模板协议直接反映 try
语句
一个建议是在协议中使用单独的方法来覆盖通用 try
语句结构的不同部分。使用 try
、except
、else
和 finally
这些术语,我们将得到类似以下内容
class my_template(object):
def __init__(self, *args):
# Any required arguments (e.g. a file name)
# get stored in member variables
# The various BLOCK's will need to updated to reflect
# that.
def __try__(self):
SETUP_BLOCK
def __except__(self, exc, value, traceback):
if isinstance(exc, exc_type1):
EXCEPT_BLOCK1
if isinstance(exc, exc_type2):
EXCEPT_BLOCK2
else:
EXCEPT_BLOCK3
def __else__(self):
ELSE_BLOCK
def __finally__(self):
FINALLY_BLOCK
除了更喜欢添加两个方法槽而不是四个之外,我认为能够简单地在 __exit__()
方法中重现原始 try
语句代码的略微修改版本(如 分解任意异常处理 中所示)要容易得多,而不是将功能拆分到几个不同的方法中(或者弄清楚如果模板没有使用所有子句,应该使用哪个方法)。
为了使讨论不那么理论化,这里有一个使用两种方法和四种方法协议而不是生成器实现的transaction
示例。两种实现都保证在遇到break
、return
或continue
语句时提交(就像示例部分中基于生成器的实现一样)。
class transaction_2method(object):
def __init__(self, db):
self.db = db
def __enter__(self):
pass
def __exit__(self, exc_type, *exc_details):
if exc_type is None:
self.db.commit()
else:
self.db.rollback()
class transaction_4method(object):
def __init__(self, db):
self.db = db
self.commit = False
def __try__(self):
self.commit = True
def __except__(self, exc_type, exc_value, traceback):
self.db.rollback()
self.commit = False
def __else__(self):
pass
def __finally__(self):
if self.commit:
self.db.commit()
self.commit = False
还有两个不太重要的点,与建议中特定的方法名称有关。__try__()
方法的名称具有误导性,因为SETUP_BLOCK
在进入try
语句之前执行,并且__else__()
方法的名称在孤立的情况下不清楚,因为许多其他 Python 语句都包含else
子句。
迭代器结束(已撤回)
能够在生成器内部使用用户定义的语句可能会增加对迭代器确定性终结的需求,因为资源管理被推到生成器内部,而不是像目前那样在外部进行处理。
PEP 目前建议通过将所有生成器都设为语句模板并使用with
语句来处理终结来处理此问题。但是,此 PEP 的早期版本提出了以下更复杂的方法,该方法允许生成器的作者标记对终结的需求,并让for
循环自动处理它。它包含在此作为一项冗长、详细的被拒绝选项。
迭代器协议添加:__finish__
建议为迭代器提供一个可选的新方法,称为__finish__()
。它不带任何参数,也不应返回任何内容。
预计__finish__
方法将清理迭代器打开的所有资源。具有__finish__()
方法的迭代器在 PEP 的其余部分中称为“可终结迭代器”。
尽力而为的结束
可终结迭代器应确保它提供了一个__del__
方法,该方法也执行终结(例如,通过调用__finish__()
方法)。这允许 Python 在未对迭代器应用确定性终结的情况下,仍然尽最大努力进行终结。
确定性结束
如果在for
循环中使用的迭代器具有__finish__()
方法,则增强的for
循环语义将保证该方法将被执行,而不管退出循环的方式如何。这对于利用用户定义的语句或现在允许的try
/finally
语句的迭代器生成器,或者对于依赖及时终结来释放已分配资源(例如,将线程或数据库连接释放回池中)的新迭代器来说非常重要。
for
循环语法
不建议对for
循环语法进行任何更改。这只是为了定义描述语义所需的语句部分。
for VAR1 in EXPR1:
BLOCK1
else:
BLOCK2
更新的 for
循环语义
当目标迭代器没有__finish__()
方法时,for
循环将按如下方式执行(即与现状无变化)。
itr = iter(EXPR1)
exhausted = False
while True:
try:
VAR1 = itr.next()
except StopIteration:
exhausted = True
break
BLOCK1
if exhausted:
BLOCK2
当目标迭代器具有__finish__()
方法时,for
循环将按如下方式执行。
itr = iter(EXPR1)
exhausted = False
try:
while True:
try:
VAR1 = itr.next()
except StopIteration:
exhausted = True
break
BLOCK1
if exhausted:
BLOCK2
finally:
itr.__finish__()
实现将需要小心避免在迭代器没有__finish__()
方法时产生try
/finally
开销。
生成器迭代器结束:__finish__()
方法
当使用适当的装饰器启用时,生成器将具有一个__finish__()
方法,该方法在内部框架中引发TerminateIteration
。
def __finish__(self):
try:
self._inject_exception(TerminateIteration)
except TerminateIteration:
pass
需要一个装饰器(例如needs_finish()
)来启用此功能,以便现有的生成器(不期望终结)继续按预期工作。
可结束迭代器的部分迭代
可以部分迭代可终结迭代器,尽管需要小心确保迭代器仍然能够及时终结(它之所以可终结是有原因的!)。首先,我们需要一个类来通过隐藏迭代器的__finish__()
方法来使for
循环无法访问,从而启用可终结迭代器的部分迭代。
class partial_iter(object):
def __init__(self, iterable):
self.iter = iter(iterable)
def __iter__(self):
return self
def next(self):
return self.itr.next()
其次,需要一个合适的语句模板来确保迭代器最终完成。
@statement_template
def finishing(iterable):
itr = iter(iterable)
itr_finish = getattr(itr, "__finish__", None)
if itr_finish is None:
yield itr
else:
try:
yield partial_iter(itr)
finally:
itr_finish()
然后可以按如下方式使用它。
do finishing(finishable_itr) as itr:
for header_item in itr:
if end_of_header(header_item):
break
# process header item
for body_item in itr:
# process body item
请注意,对于不可终结的迭代器,以上所有内容都不需要 - 如果没有__finish__()
方法,for
循环不会及时对其进行终结,因此本质上允许部分迭代。将不可终结迭代器的部分迭代作为默认行为是使此迭代器协议的添加向后兼容的关键要素。
致谢
适用于PEP 340的致谢部分适用,因为本文是从该 PEP 的讨论中发展而来的,但还要特别感谢 Michael Hudson、Paul Moore 和 Guido van Rossum 最初编写了PEP 310和 PEP 340,以及(无特定顺序)Fredrik Lundh、Phillip J. Eby、Steven Bethard、Josiah Carlson、Greg Ewing、Tim Delaney 和 Arnold deVos 提出了促使本文包含特定想法的建议。
参考文献
版权
本文已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0346.rst
上次修改时间:2023-10-11 12:05:51 GMT