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

Python 增强提案

PEP 340 – 匿名块语句

作者:
Guido van Rossum
状态:
已拒绝
类型:
标准跟踪
创建日期:
2005年4月27日
发布历史:


目录

引言

本 PEP 提出了一种新型的复合语句,可用于资源管理。这种新语句类型暂时称为块语句,因为要使用的关键字尚未确定。

本 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 作为闭包在其包含函数之后仍然存在时出现重大怪异(也许延续会有帮助,但我不会深入探讨这一点 :-)。

但这样一来,IMO 资源清理模板模式的一个重要用例就丢失了。我经常编写这样的代码

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 中使用或设置的每个局部变量都必须成为一个“单元格”(我们用于在嵌套作用域之间共享变量的机制)。与常规局部变量相比,单元格会降低访问速度:访问涉及额外的 C 函数调用(PyCell_Get()PyCell_Set())。

也许并非完全巧合,上面的最后一个例子(findSomething() 重写以避免块内的 return)表明,与常规嵌套函数不同,我们希望 thunk **赋值**的变量也能与包含函数共享,即使它们没有在 thunk 之外赋值。

Greg Ewing 再次说道:“生成器已被证明更强大,因为你可以同时拥有多个生成器。这里是否有这种能力的使用?”

我相信这绝对有用;已经有好几个人展示了如何使用生成器来实现异步轻量级线程(例如,PEP 288 中引用的 David Mertz,以及 Fredrik Lundh [3])。

最后,格雷格说:“如果能够设计出合适的语法,thunk 实现有可能轻松处理多个块参数。很难想象如何通过生成器实现以通用的方式完成这一点。”

然而,多块的用例似乎难以捉摸。

(后来提出了改变 thunks 实现的建议,以消除这些异议中的大部分,但由此产生的语义解释和实现都相当复杂,因此我认为这违背了最初使用 thunks 的目的。)

示例

(这些例子中有些包含“yield None”。如果 PEP 342 被接受,这些当然可以改为只写“yield”。)

  1. 一个模板,用于确保在块开始时获取的锁在块离开时被释放
    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).
    
  2. 一个用于打开文件的模板,确保在块离开时文件被关闭
    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()
    
  3. 提交或回滚数据库事务的模板
    def transactional(db):
        try:
            yield None
        except:
            db.rollback()
            raise
        else:
            db.commit()
    
  4. 一个尝试某事最多 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()
    
  5. 可以嵌套块并组合模板
    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() 的源代码。)

  6. 可以用示例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
    

    (这个例子很容易修改以实现其他例子;它展示了生成器用于相同目的时是多么简单。)

  7. 暂时重定向标准输出
    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"
    
  8. 一个 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

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