PEP 377 – 允许 __enter__() 方法跳过语句体
- 作者:
- Alyssa Coghlan <ncoghlan at gmail.com>
- 状态:
- 已拒绝
- 类型:
- 标准跟踪
- 创建:
- 2009-03-08
- Python 版本:
- 2.7, 3.1
- 历史记录:
- 2009-03-08
摘要
本 PEP 提出了一种向后兼容的机制,允许 __enter__()
方法跳过关联的 with
语句的主体。目前缺乏此功能意味着 contextlib.contextmanager
装饰器无法实现其规范,即通过将其移动到具有适当位置的 yield 的生成器函数中,将任意代码转换为上下文管理器。这的一个症状是 contextlib.nested
目前会在编写相应的嵌套 with
语句不会出现 [1] 的情况下引发 RuntimeError
。
提议的更改是引入一个新的流程控制异常 SkipStatement
,如果 __enter__()
引发此异常,则跳过 with
语句主体的执行。
PEP 拒绝
Guido 拒绝了此 PEP [4],因为它在没有成比例地提高表达性和正确性的情况下,增加了过多的复杂性。在没有令人信服的使用案例需要本 PEP 提出的更复杂语义的情况下,现有行为被认为是可以接受的。
提议的更改
with
语句的语义将更改为在对 __enter__()
的调用周围包含一个新的 try
/except
/else
块。如果 __enter__()
方法引发了 SkipStatement
,则 with
语句的主体部分(现在位于 else
子句中)将不会执行。为了避免在这种情况下使任何 as
子句中的名称未绑定,一个新的 StatementSkipped
单例(类似于现有的 NotImplemented
单例)将被分配给出现在 as
子句中的所有名称。
with
语句的组件保持与 PEP 343 中所述相同。
with EXPR as VAR:
BLOCK
修改后,with
语句的语义将如下所示
mgr = (EXPR)
exit = mgr.__exit__ # Not calling it yet
try:
value = mgr.__enter__()
except SkipStatement:
VAR = StatementSkipped
# Only if "as VAR" is present and
# VAR is a single name
# If VAR is a tuple of names, then StatementSkipped
# will be assigned to each name in the tuple
else:
exc = True
try:
try:
VAR = value # Only if "as VAR" is present
BLOCK
except:
# The exceptional case is handled here
exc = False
if not exit(*sys.exc_info()):
raise
# The exception is swallowed if exit() returns true
finally:
# The normal and non-local-goto cases are handled here
if exc:
exit(None, None, None)
在 with
语句语义中进行上述更改后,contextlib.contextmanager()
将被修改为在底层生成器不产生 yield 时引发 SkipStatement
而不是 RuntimeError
。
更改的理由
目前,一些看似无害的上下文管理器在执行时可能会引发 RuntimeError
。当上下文管理器的 __enter__()
方法遇到与上下文管理器相对应代码的已编写版本将跳过现在是 with
语句主体的代码的情况时,就会发生这种情况。由于 __enter__()
方法没有可用的机制来向解释器发出此信号,因此它被迫引发一个异常,该异常不仅跳过 with
语句的主体,而且还跳过所有代码直到最近的异常处理程序。这违反了 with
语句的设计目标之一,该目标是能够通过将任意公共异常处理代码放入生成器函数并将代码的可变部分替换为 yield
语句,将其分解到单个上下文管理器中。
具体来说,如果 cmB().__enter__()
引发一个异常,然后 cmA().__exit__()
处理并抑制该异常,则以下示例的行为会有所不同。
with cmA():
with cmB():
do_stuff()
# This will resume here without executing "do_stuff()"
@contextlib.contextmanager
def combined():
with cmA():
with cmB():
yield
with combined():
do_stuff()
# This will raise a RuntimeError complaining that the context
# manager's underlying generator didn't yield
with contextlib.nested(cmA(), cmB()):
do_stuff()
# This will raise the same RuntimeError as the contextmanager()
# example (unsurprising, given that the nested() implementation
# uses contextmanager())
# The following class based version shows that the issue isn't
# specific to contextlib.contextmanager() (it also shows how
# much simpler it is to write context managers as generators
# instead of as classes!)
class CM(object):
def __init__(self):
self.cmA = None
self.cmB = None
def __enter__(self):
if self.cmA is not None:
raise RuntimeError("Can't re-use this CM")
self.cmA = cmA()
self.cmA.__enter__()
try:
self.cmB = cmB()
self.cmB.__enter__()
except:
self.cmA.__exit__(*sys.exc_info())
# Can't suppress in __enter__(), so must raise
raise
def __exit__(self, *args):
suppress = False
try:
if self.cmB is not None:
suppress = self.cmB.__exit__(*args)
except:
suppress = self.cmA.__exit__(*sys.exc_info()):
if not suppress:
# Exception has changed, so reraise explicitly
raise
else:
if suppress:
# cmB already suppressed the exception,
# so don't pass it to cmA
suppress = self.cmA.__exit__(None, None, None):
else:
suppress = self.cmA.__exit__(*args):
return suppress
在提议的语义更改到位后,基于 contextlib 的上述示例将“正常工作”,但基于类的版本需要进行少量调整以利用新的语义。
class CM(object):
def __init__(self):
self.cmA = None
self.cmB = None
def __enter__(self):
if self.cmA is not None:
raise RuntimeError("Can't re-use this CM")
self.cmA = cmA()
self.cmA.__enter__()
try:
self.cmB = cmB()
self.cmB.__enter__()
except:
if self.cmA.__exit__(*sys.exc_info()):
# Suppress the exception, but don't run
# the body of the with statement either
raise SkipStatement
raise
def __exit__(self, *args):
suppress = False
try:
if self.cmB is not None:
suppress = self.cmB.__exit__(*args)
except:
suppress = self.cmA.__exit__(*sys.exc_info()):
if not suppress:
# Exception has changed, so reraise explicitly
raise
else:
if suppress:
# cmB already suppressed the exception,
# so don't pass it to cmA
suppress = self.cmA.__exit__(None, None, None):
else:
suppress = self.cmA.__exit__(*args):
return suppress
目前有一个初步建议 [3],在 with
语句中添加类似导入的语法,以允许在单个 with
语句中包含多个上下文管理器,而无需使用 contextlib.nested
。在这种情况下,编译器可以选择在 AST 级别简单地发出多个 with
语句,从而允许准确地再现实际嵌套 with
语句的语义。但是,这样的更改会突出而不是缓解当前 PEP 旨在解决的问题:无法使用 contextlib.contextmanager
来可靠地分解此类 with
语句,因为它们将表现出与上述示例中的 combined()
上下文管理器完全相同的语义差异。
性能影响
实现新的语义需要将对 __enter__
和 __exit__
方法的引用存储在临时变量中而不是堆栈上。这导致与 Python 2.6/3.1 相比,with
语句速度略有下降。但是,实现自定义的 SETUP_WITH
操作码将抵消两种方法之间的任何差异(以及通过消除十几个不必要的 eval 循环来显着提高速度)。
参考实现
修补程序附加到问题 5251 [1]。该修补程序仅使用现有的操作码(即没有 SETUP_WITH
)。
致谢
James William Pye 既提出了问题,也提出了本 PEP 中描述的解决方案的基本轮廓。
参考文献
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0377.rst