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 语句的‘兼容形式’……引入兼容形式的动机是为了允许旧代码向后兼容,这些代码引发了标准异常。”
例如,期望在1.5版本之前的代码使用字符串异常语法,例如
raise TypeError, 'not an int'
在 TypeError 是字符串的版本和它是类的版本上都能工作。
当没有这种考虑时——即,当期望的异常类型在代码必须支持的任何软件版本中都不是字符串时——没有充分的理由进行隐式实例化,而且不这样做更清晰。例如
- 在代码
try: raise MyError, raised except MyError, caught: pass
中,
raise和except语句的语法平行关系强烈暗示raised和caught指的是同一个对象。对于字符串异常,这确实是事实,但对于实例异常,则不是。 - 当实例化是隐式的时,并不明显它何时发生,例如,它是在异常被引发时还是在异常被捕获时发生的。由于它实际上发生在
raise处,代码应该说明这一点。(请注意,在 C API 层面,异常可以“引发”和“捕获”而无需实例化;这被用作优化,例如
PyIter_Next。但在 Python 中,没有这样的优化,也不应该有。) - 没有参数的隐式实例化
raise语句,例如raise MyError
根本没有按其字面意思执行:它并没有引发指定的对象。
- 等价性
raise MyError raise MyError()
混淆了类和实例,为初学者造成了可能的困惑。(此外,不清楚解释器是否能区分新式类和此类实例,因此隐式实例化可能阻碍任何让异常成为新式对象的未来计划。)
总之,隐式实例化除了向后兼容性之外没有任何优点,因此应该与它所确保兼容性的东西一起逐步淘汰,即字符串异常。
规范
raise_stmt [3] 的语法将从
raise_stmt ::= "raise" [expression ["," expression ["," expression]]]
到
raise_stmt ::= "raise" [expression ["," expression]]
如果不存在表达式,则 raise 语句的行为与现在一样:它会重新引发当前作用域中最后一个活动的异常,如果当前作用域中没有活动的异常,则会引发一个 TypeError,表明这就是问题所在。
否则,将评估第一个表达式,产生 *引发对象*。然后评估第二个表达式(如果存在),产生 *替代的 traceback*。如果不存在第二个表达式,则替代的 traceback 为 None。
引发的对象必须是一个实例。实例的类是异常类型,实例本身是异常值。如果引发的对象不是实例——例如,如果它是一个类或字符串——则会引发一个 TypeError。
如果替代的 traceback 不是 None,它必须是一个 traceback 对象,并且它将被用作异常发生地点的替代,而不是当前位置。如果它既不是 traceback 对象也不是 None,则会引发一个 TypeError。
向后兼容性
迁移计划
Future 语句
根据 PEP 236 的 future 语句
from __future__ import raise_with_two_args
raise 语句的语法和语义将如上所述。这个未来的特性将在 Python 2.4 中出现;其效果是成为 Python 3.0 的标准。(假设过渡期——2.4 到 3.0——至少为一年,以符合 PEP 5 的指导方针。)
如下面的示例所示,这个 future 语句仅对使用 raise 的替代 traceback 参数的代码是必需的;简单的异常引发不需要它。
警告
将发出三个新的 警告,都属于 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')
提供 traceback 对象的代码
代码,例如
raise MyError, 'spam', mytraceback
将在编译时发出警告。应将该语句更改为
raise MyError('spam'), mytraceback
并且 future 语句
from __future__ import raise_with_two_args
应添加到模块的顶部。请注意,添加此 future 语句还会将其他两个警告变为错误,因此还必须应用前几个示例中描述的更改。
特殊情况
raise sys.exc_type, sys.exc_info, sys.exc_traceback
(其目的是重新引发先前的异常)应简单地更改为
raise
计划的失败
可能出现这种情况:在 PEP 的过渡期内,引发字符串或隐式实例化的 raise 语句在生产或测试中并未执行。在这种情况下,它不会发出任何警告,而是会在 Python 3.0 或后续版本中的某一天突然失败。(失败之处在于会引发错误的异常,即一个抱怨 raise 参数的 TypeError,而不是期望的异常。)
通过延长过渡期可以减少这种情况的发生;除非在编译时对每个 raise 语句发出警告,否则无法完全避免。
拒绝
如果此 PEP 被接受,几乎所有现有的 Python 代码都需要进行审查,并可能进行修改;即使以上所有支持显式实例化的论点都被接受,清晰度的提高也太微不足道,不足以证明进行修改的成本和引入新错误的风险是合理的。
因此,该提议已被拒绝 [6]。
请注意,字符串异常的移除独立于此提议;被拒绝的是移除隐式异常实例化。
讨论总结
少数受访者赞成该提议,但占主导地位的观点是,任何此类迁移的成本都将不成比例地高于假定的收益。如上所述,这一点本身就足以拒绝 PEP。
新式异常
隐式实例化可能与允许新式类实例用作异常的未来计划发生冲突。为了决定是否隐式实例化,raise 机制必须确定第一个参数是类还是实例——但对于新式类,没有清晰而强烈的区别。
根据此提议,问题将得到避免,因为异常已经实例化。然而,有两种可能的替代解决方案
- 要求异常类型是
Exception的子类,并且仅当issubclass(firstarg, Exception)
- 仅当
isinstance(firstarg, type)
因此,完全消除隐式实例化并非解决此问题的必要条件。
显式实例化的丑陋
一些受访者认为,显式实例化语法更丑陋,特别是在异常构造函数没有提供参数的情况下
raise TypeError()
当异常实例本身不重要时,即当唯一相关的点是异常类型时,问题尤为严重
try:
# ... deeply nested search loop ...
raise Found
except Found:
# ...
在这些情况下,raise 和 except 之间的对称性可以更清晰地表达代码的意图。
Guido 认为,即使对于只有一个参数的情况,隐式实例化语法也“稍微好看一些”,因为它标点符号较少。
警告的性能损失
弃用 apply() 的经验表明,使用警告框架可能会产生显著的性能损失。
显式实例化的代码将不受影响,因为确定是否发出警告所需的运行时检查与确定是否进行隐式实例化所需的检查完全相同。也就是说,此类语句已经承担了这些检查的成本。
隐式实例化的代码将承担高昂的成本:计时测试表明,发出警告(无论是否被抑制)比简单地实例化、引发和捕获异常花费的时间大约是五倍。
此成本因 raise 语句很少出现在性能关键的执行路径上而得到缓解。
Traceback 参数
按照目前的提议,在所有 2.x 版本的 Python 中都不可能方便地使用 raise 的 traceback 参数。
为了兼容版本 < 2.4,必须使用三参数形式;但这种形式在版本 >= 2.4 中会产生警告。这些警告可以被抑制,但这样做很麻烦,因为相关类型的警告是在编译时发出的。
如果此 PEP 仍在考虑中,此反对意见将通过延长过渡期来解决。例如,警告可以首先在 3.0 中发出,然后在某个更高版本中变为错误。
参考资料
版权
本文档已置于公共领域。
来源: https://github.com/python/peps/blob/main/peps/pep-0317.rst
最后修改: 2025-02-01 08:59:27 GMT