Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 377 – 允许 __enter__() 方法跳过语句体

作者:
Alyssa Coghlan <ncoghlan at gmail.com>
状态:
已拒绝
类型:
标准跟踪
创建日期:
2009 年 3 月 8 日
Python 版本:
2.7, 3.1
发布历史:
2009 年 3 月 8 日

目录

摘要

本 PEP 提出了一种向后兼容的机制,允许 __enter__() 方法跳过关联的 with 语句体。目前缺乏此能力意味着 contextlib.contextmanager 装饰器无法实现其将任意代码转换为上下文管理器(通过将其移动到带有适当位置的 yield 的生成器函数中)的规范。一个症状是 contextlib.nested 目前在写入相应的嵌套 with 语句不会的情况下会引发 RuntimeError [1]

拟议的更改是引入一个新的流程控制异常 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],将 import 风格的语法添加到 with 语句中,允许在单个 with 语句中包含多个上下文管理器,而无需使用 contextlib.nested。在这种情况下,编译器可以选择在 AST 级别发出多个 with 语句,从而能够准确地重现实际嵌套 with 语句的语义。然而,这样的更改将凸显而不是缓解当前 PEP 旨在解决的问题:将无法使用 contextlib.contextmanager 来可靠地提取此类 with 语句,因为它们将表现出与上述示例中的 combined() 上下文管理器相同的语义差异。

性能影响

实现新的语义需要将 __enter____exit__ 方法的引用存储在临时变量中,而不是在堆栈上。这导致 with 语句的速度相对于 Python 2.6/3.1 略有下降。然而,实现自定义 SETUP_WITH 操作码将消除两种方法之间的任何差异(并且通过消除十几次不必要的 eval 循环往返而大大提高速度)。

参考实现

补丁已附加到 Issue 5251 [1]。该补丁仅使用现有操作码(即,没有 SETUP_WITH)。

致谢

James William Pye 提出了该问题并提出了本 PEP 所述的解决方案的基本框架。

参考资料


来源:https://github.com/python/peps/blob/main/peps/pep-0377.rst

最后修改:2025-02-01 08:59:27 GMT