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

Python 增强提案

PEP 317 – 消除隐式异常实例化

作者:
Steven Taschuk <staschuk at telusplanet.net>
状态:
已拒绝
类型:
标准跟踪
创建:
2003年5月6日
Python 版本:
2.4
历史记录:
2003年6月9日

目录

摘要

“为了新代码的清晰度,建议使用 raise class(argument, ...) 形式(即明确调用构造函数)。 ”

—Guido van Rossum,1997 年 [1]

此 PEP 提出正式弃用并最终消除 raise 语句的某些形式,这些形式隐式地实例化异常。例如,根据本提案,以下语句:

raise HullBreachError
raise KitchenError, 'all out of baked beans'

必须替换为它们的同义词

raise HullBreachError()
raise KitchenError('all out of baked beans')

请注意,这些后一种语句已经是合法的,本 PEP 不会改变它们的含义。

消除这些形式的 raise 使得无法使用字符串异常;因此,本 PEP 还建议正式弃用并最终消除字符串异常。

采用本提案会破坏向后兼容性。根据拟议的实施时间表,Python 2.4 将引入有关 raise 用法的警告,这些警告最终将变得不正确,而 Python 3.0 将完全消除它们。(假设此过渡期——从 2.4 到 3.0——将至少持续一年,以符合 PEP 5 的指南。)

动机

字符串异常

假设移除字符串异常将是毫无争议的,因为自至少 Python 1.5 以来,就一直打算这样做,当时标准异常类型已更改为类 [1]

为了记录:应该移除字符串异常,因为两种类型的异常的存在使语言变得复杂,而没有任何补偿。实例异常更优越,因为例如,

  • 类-实例关系更自然地表达了异常类型和值之间的关系,
  • 它们可以使用超类-子类关系进行自然组织,以及
  • 它们可以封装错误报告行为(例如)。

隐式实例化

Guido 在 1997 年的论文 [1] 中论述将标准异常更改为类,清楚地解释了为什么 raise 可以隐式地实例化

“raise 语句已扩展,允许在没有显式实例化的前提下引发类异常。以下形式,称为 raise 语句的“兼容形式” […] 引入兼容形式的动机是为了允许与引发标准异常的旧代码向后兼容。”

例如,希望在 TypeError 是字符串的 Python 版本和 TypeError 是类的 Python 版本上,使用字符串异常语法的 1.5 之前代码

raise TypeError, 'not an int'

都可以正常工作。

当不存在这种考虑因素时——也就是说,当所需的异常类型在代码必须支持的任何软件版本中都不是字符串时——就没有充分的理由进行隐式实例化,而且不进行隐式实例化更清晰。例如

  1. 在代码中
    try:
        raise MyError, raised
    except MyError, caught:
        pass
    

    raiseexcept 语句之间的句法平行强烈表明 raisedcaught 指的是同一个对象。对于字符串异常来说,情况确实如此,但对于实例异常来说则不然。

  2. 当实例化是隐式进行时,不清楚实例化何时发生,例如,不清楚实例化是在引发异常时还是在捕获异常时发生。由于它实际上是在 raise 处发生的,因此代码应该说明这一点。

    (请注意,在 C API 级别,异常可以在没有实例化的前提下“引发”和“捕获”;例如,PyIter_Next 使用此方法作为优化。但在 Python 中,这种优化不可用,也不应该可用。)

  3. 一个不带参数的隐式实例化 raise 语句,例如
    raise MyError
    

    根本没有做到它所说的:它没有引发命名对象。

  4. 以下两者的等效性:
    raise MyError
    raise MyError()
    

    将类和实例混为一谈,为初学者创造了潜在的混淆来源。(此外,不清楚解释器是否可以区分新式类和此类类的实例,因此隐式实例化可能是未来任何让异常成为新式对象的计划的障碍。)

简而言之,除了向后兼容性之外,隐式实例化没有任何优势,因此应该与它存在以确保兼容性的东西一起逐步淘汰,即字符串异常。

规范

raise_stmt 的语法 [3] 将从

raise_stmt ::= "raise" [expression ["," expression ["," expression]]]

更改为

raise_stmt ::= "raise" [expression ["," expression]]

如果不存在表达式,则 raise 语句的行为与目前一样:它重新引发当前作用域中最后一次活动的异常,如果当前作用域中没有异常处于活动状态,则会引发一个 TypeError,表明这就是问题所在。

否则,将计算第一个表达式,生成引发对象。然后计算第二个表达式(如果存在),生成替换回溯。如果不存在第二个表达式,则替换回溯为 None

引发对象必须是一个实例。实例的类是异常类型,实例本身是异常值。如果引发对象不是实例——例如,如果它是一个类或字符串——则会引发一个 TypeError

如果替换回溯不是 None,则它必须是一个回溯对象,并且它将替换当前位置,成为发生异常的位置。如果它既不是回溯对象也不是 None,则会引发一个 TypeError

向后兼容性

迁移计划

未来语句

PEP 236 未来语句

from __future__ import raise_with_two_args

raise 语句的语法和语义将如上所述。此未来功能将在 Python 2.4 中出现;它的作用是在 Python 3.0 中成为标准。

如下面的示例所示,此未来语句仅在使用替换回溯参数的 raise 的代码中需要;简单的异常引发不需要它。

警告

将发布三个新的 警告,它们都属于 DeprecationWarning 类,用于指出将在拟议更改下变得不正确的 raise 用法。

第一个警告是在执行一个 raise 语句时发布的,其中第一个表达式的计算结果为一个字符串。此警告的消息为

raising strings will be impossible in the future

第二个警告是在执行一个 raise 语句时发布的,其中第一个表达式的计算结果为一个类。此警告的消息为

raising classes will be impossible in the future

第三个警告是在编译一个带有三个表达式的 raise 语句时发布的。(注意,不是在执行时;这一点很重要,因为此警告预示的 SyntaxError 将在编译时发生。)此警告的消息为

raising with three arguments will be impossible in the future

这些警告将出现在 Python 2.4 中,并在 Python 3.0 中消失,此时导致这些警告的条件将简单地变为错误。

示例

使用隐式实例化的代码

如下代码:

class MyError(Exception):
    pass

raise MyError, 'spam'

在执行 raise 语句时会发出警告。raise 语句应该更改为显式地实例化

raise MyError('spam')

使用字符串异常的代码

如下代码:

MyError = 'spam'
raise MyError, 'eggs'

在执行 raise 语句时会发出警告。异常类型应该更改为一个类

class MyError(Exception):
    pass

并且,与前面的示例一样,raise 语句应该更改为显式地实例化

raise MyError('eggs')

提供回溯对象的代码

如下代码:

raise MyError, 'spam', mytraceback

在编译时会发出警告。该语句应该更改为

raise MyError('spam'), mytraceback

并且未来语句

from __future__ import raise_with_two_args

应该添加到模块的顶部。请注意,添加此未来语句还会将另外两个警告变为错误,因此也必须应用前面示例中描述的更改。

特殊情况

raise sys.exc_type, sys.exc_info, sys.exc_traceback

(用于重新引发之前的异常)应该简单地更改为

raise

计划失败

可能会出现这种情况,即在针对本 PEP 的逐步实施阶段,引发字符串或隐式实例化的 raise 语句不会在生产或测试中执行。在这种情况下,它不会发出任何警告,而是会在将来某一天在 Python 3.0 或更高版本中突然失败。(失败的原因是引发了错误的异常,即一个 TypeError,抱怨 raise 的参数,而不是预期的异常。)

通过延长逐步实施阶段,可以减少这种情况的发生;除了针对每个 raise 语句在编译时发出警告之外,无法完全避免这种情况。

拒绝

如果本 PEP 被接受,几乎所有现有的 Python 代码都需要进行审查,并且可能需要进行修改;即使所有支持显式实例化的上述论点都被接受,清晰度的改进也微不足道,无法证明进行修改的成本以及由此引入新错误的风险。

因此,本提案已被拒绝 [6]

请注意,字符串异常与本提案无关,计划独立移除;被拒绝的是移除隐式异常实例化。

讨论总结

一小部分响应者赞成该提案,但主要意见是,任何这样的迁移都将是代价高昂,超过了预期的益处。如上所述,这一点本身就足以拒绝该 PEP。

新式异常

隐式实例化可能会与未来允许将新式类实例用作异常的计划发生冲突。为了决定是否隐式实例化,raise 机制必须确定第一个参数是类还是实例 - 但对于新式类,没有明确且强烈的区别。

根据此提议,问题将得到避免,因为异常已在实例化。但是,有两个可能的替代方案

  1. 要求异常类型是 Exception 的子类,并且当且仅当隐式实例化时
    issubclass(firstarg, Exception)
    
  2. 当且仅当隐式实例化时
    isinstance(firstarg, type)
    

因此,完全消除隐式实例化对于解决此问题并非必要。

显式实例化的丑陋

一些回复者认为,显式实例化语法更难看,特别是在没有向异常构造函数提供任何参数的情况下

raise TypeError()

当异常实例本身无关紧要时,问题尤其严重,也就是说,当唯一相关的点是异常类型时

try:
    # ... deeply nested search loop ...
        raise Found
except Found:
    # ...

在这种情况下,raiseexcept 之间的对称性可以更清楚地表达代码的意图。

Guido 认为,即使在只有一个参数的情况下,隐式实例化语法也“稍微漂亮一些”,因为它标点符号更少。

警告的性能损失

对弃用 apply() 的经验表明,使用警告框架会带来显着的性能损失。

显式实例化的代码不受影响,因为确定是否发出警告所需的运行时检查正是最初决定是否隐式实例化的那些检查。也就是说,这些语句已经产生了这些检查的成本。

隐式实例化的代码会产生很大的成本:计时试验表明,发出警告(无论是否被抑制)比简单地实例化、抛出和捕获异常要多花费五倍的时间。

由于 raise 语句很少出现在性能关键的执行路径上,因此该惩罚得到了缓解。

回溯参数

按照目前的提议,无法方便地在所有 2.x 版本的 Python 中使用 raise 的跟踪参数。

为了与版本 < 2.4 兼容,必须使用三参数形式;但这种形式会在版本 >= 2.4 中产生警告。这些警告可以被抑制,但这样做很麻烦,因为相关的警告类型是在编译时发出的。

如果这个 PEP 仍在考虑之中,这个反对意见将通过延长逐步引入期来解决。例如,警告可以在 3.0 中首次发出,并在以后的某个版本中变为错误。

参考


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

最后修改: 2023-09-09 17:39:29 GMT