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

Python 增强提案

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 340 期间,我将本 PEP 的草稿作为 PEP 3XX 保存在我自己的网站上(因为我没有 CVS 访问权限来足够快地更新已提交的 PEP 以跟踪 python-dev 上的活动)。

自从本 PEP 的第一个草稿以来,Guido 编写了 PEP 343 作为 PEP 340 的简化版本。PEP 343(在撰写本文时)对新语句使用与本 PEP 完全相同的语义,但使用了一种略微不同的机制来允许使用生成器编写语句模板。但是,Guido 已表示他打算接受 Raymond Hettinger 编写的新的 PEP,该 PEP 将集成 PEP 288PEP 325,并将允许使用类似于本 PEP 中描述的生成器装饰器来为 PEP 343 编写语句模板。另一个区别是关键字的选择(“with”与“do”),Guido 已表示他将在 PEP 343 的上下文中组织对此进行投票。

因此,提交到 python.org 以供存档的本 PEP 版本将在提交后立即 **撤回**。PEP 343 和组合的生成器增强 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 310PEP 340PEP 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 循环中的迭代变量一样,VAR1BLOCK1 和用户定义语句之后的代码中都是可见的。

请注意,语句模板只能对异常做出反应,不能抑制它们。有关原因,请参阅 已拒绝的选项

语句模板协议:__enter__

__enter__() 方法不接受任何参数,如果它引发异常,则永远不会执行 BLOCK1。如果发生这种情况,则不会调用 __exit__() 方法。如果使用了 as 子句,则此方法返回的值将分配给 VAR1。没有其他返回值的对象通常应返回 self 而不是 None,以允许在 with 语句中就地创建。

语句模板应使用此方法来设置语句执行期间存在的条件(例如,获取同步锁)。

并非始终可用的语句模板(例如已关闭的文件对象)如果在模板处于无效状态时尝试调用__enter__(),则应引发RuntimeError

语句模板协议:__exit__

__exit__()方法接受三个参数,分别对应于raise语句的三个“参数”:类型、值和回溯。始终提供所有参数,如果未发生异常,则将其设置为None。如果__enter__()方法成功完成,则此方法将由with语句机制准确调用一次。

语句模板在此方法中执行其异常处理。如果第一个参数为None,则表示BLOCK1的非异常完成 - 执行要么到达块的末尾,要么使用returnbreakcontinue语句强制提前完成。否则,这三个参数反映了终止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中遇到breakreturncontinue语句,则仅执行FINALLY_BLOCK,因为语句已完成。使用语句模板,ELSE_BLOCK也将执行,因为这些语句被视为任何其他非异常块终止。对于有关系例,这很可能是一件好事(请参阅示例中的transaction),因为在编写异常处理程序时,很容易忘记这个既不执行except也不执行else子句的漏洞。

其次,语句模板不会抑制任何异常。例如,如果原始代码抑制了exc_type1exc_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,则yieldreturn一样,将提供None的默认值(即yieldyield 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/finallywith语句确保对象得到正确管理。

此限制可能需要在全局范围内解除 - 将其限制为仅允许在用作语句模板的生成器内部使用将很困难。因此,本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 语句的主体已顺利完成。

示例

  1. 一个确保在块开始时获取的锁在离开块时释放的模板
    # 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).
    
  2. 一个打开文件的模板,确保在离开块时关闭文件
    # 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()
    
  3. 一个提交或回滚数据库事务的模板
    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
    
  4. 可以嵌套块并组合模板
    @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()
    
  5. 临时重定向 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"
    
  6. 一个 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")
    
  7. 查找具有特定头的第一个文件
    for name in filenames:
        with open(name) as f:
            if f.read(2) == 0xFEB0:
                break
    
  8. 查找您可以处理的第一个项目,为整个循环或每次迭代保持锁
    with lock:
        for item in items:
            if handle(item):
                break
    
    for item in items:
        with lock:
            if handle(item):
                break
    
  9. 在生成器内部保持锁,但在将控制权返回到外部作用域时释放它
    @statement_template
    def released(lock):
        lock.release()
        try:
            yield
        finally:
            lock.acquire()
    

    用法如下

    with lock:
        for item in items:
            with released(lock):
                yield item
    
  10. 读取来自文件集合的行(例如,处理多个配置源)
    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)
    
  11. 并非所有用途都需要涉及资源管理
    @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!"
    
  12. 来自 PEP 343,另一个有用的例子是一个阻止信号的操作。用法可以是这样的
    from signal import blocked_signals
    
    with blocked_signals():
        # code executed without worrying about signals
    

    一个可选参数可能是一个要阻塞的信号列表;默认情况下,所有信号都被阻塞。实现留给读者作为练习。

  13. 此功能的另一个用途是用于十进制上下文
    # 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 340block 语句所示,这个想法的主要问题是它会导致在循环内部的 try 语句的分解出现问题,并且包含 breakcontinue 语句(因为这些语句将应用于 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 的早期版本允许语句模板区分正常退出块和通过 returnbreakcontinue 语句退出。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 310with 关键字,基于希望将其用于 Pascal/Delphi 风格的 with 语句。从那时起,BDFL 已撤回此反对意见,因为他不再打算提供此类语句。这种改变想法显然是基于 C# 开发人员不提供此功能的原因 [2]

不使用关键字

这是一个有趣的选择,并且可以使其读起来相当好。但是,对于新用户来说,在文档中查找它很笨拙,并且有些人认为它过于神奇。因此,此 PEP 使用基于关键字的建议。

增强 try 语句

此建议涉及为裸 try 语句提供类似于为 with 语句提出的签名。

我认为尝试将 with 语句写成增强的 try 语句与尝试将 for 循环写成增强的 while 循环一样有意义。也就是说,虽然前者的语义可以解释为使用后者的特定方式,但前者不是后者的 *实例*。围绕更基本语句添加的额外语义导致了一个新的结构,并且不应该将这两个不同的语句混淆。

这可以通过以下事实看出:“增强”的 try 语句仍然需要用“非增强”的 try 语句来解释。如果它与众不同,那么给它一个不同的名称更有意义。

使模板协议直接反映 try 语句

一个建议是在协议中使用单独的方法来覆盖通用 try 语句结构的不同部分。使用 tryexceptelsefinally 这些术语,我们将得到类似以下内容

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示例。两种实现都保证在遇到breakreturncontinue语句时提交(就像示例部分中基于生成器的实现一样)。

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