PEP 3151 – 重构操作系统和 IO 异常层次结构
- 作者:
- Antoine Pitrou <solipsis at pitrou.net>
- BDFL 委托:
- Barry Warsaw
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2010 年 7 月 21 日
- Python 版本:
- 3.3
- 发布历史:
- 决议:
- Python-Dev 消息
摘要
标准异常层次结构是 Python 语言的重要组成部分。它具有两个决定性特质:既通用又具有选择性。通用性在于无论上下文如何(例如,无论您是尝试将某个内容添加到整数、调用字符串方法,还是将对象写入套接字,对于错误的参数类型都会引发 TypeError),都可以引发并处理相同的异常类型。选择性在于它允许用户轻松处理(静默、检查、处理、存储或封装...)特定类型的错误条件,同时让其他错误冒泡到更高的调用上下文。例如,您可以选择捕获 ZeroDivisionErrors,而不会影响其他 ArithmeticErrors(如 OverflowErrors)的默认处理。
本 PEP 提议修改异常层次结构的一部分,以更好地体现上述特质:与操作系统调用相关的错误(OSError、IOError、mmap.error、select.error 及其所有子类)。
基本原理
缺乏细粒度异常
当前各种 OS 相关异常不允许用户轻松过滤所需的故障类型。例如,考虑在文件存在时删除文件的任务。预先检查(LBYL)的习语存在明显的竞争条件
if os.path.exists(filename):
os.remove(filename)
如果在调用 os.path.exists 和 os.remove 之间,另一个线程或进程创建了名为 filename 的文件,则该文件将不会被删除。这可能导致应用程序中的错误,甚至安全问题。
因此,解决方案是尝试删除文件,如果文件不存在则忽略错误(这种习语被称为“更容易请求宽恕而不是请求许可”,即 EAFP)。细致的代码将如下所示(这在 POSIX 和 Windows 系统下都适用)
try:
os.remove(filename)
except OSError as e:
if e.errno != errno.ENOENT:
raise
甚至
try:
os.remove(filename)
except EnvironmentError as e:
if e.errno != errno.ENOENT:
raise
这需要输入更多的内容,并且还迫使用户记住 errno 模块中各种神秘的助记符。它增加了额外的认知负担,并且很快就会令人厌烦。因此,许多程序员会转而编写以下代码,它过于宽泛地抑制了异常
try:
os.remove(filename)
except OSError:
pass
os.remove 不仅在文件不存在时会引发 OSError,在其他可能的情况下也会引发(例如,文件名指向目录,或者当前进程没有删除文件的权限),这些都表明应用程序逻辑中存在错误,因此不应被静默。程序员更愿意编写的代码应该是这样的
try:
os.remove(filename)
except FileNotFoundError:
pass
兼容性策略
重构异常层次结构显然会改变至少某些现有代码的确切语义。虽然在不改变确切语义的情况下无法改善当前状况,但可以定义一种更窄的兼容性类型,我们称之为 *有用兼容性*。
为此,我们首先必须解释我们所说的 *细致* 和 *粗心* 异常处理。*粗心*(或“天真”)代码被定义为盲目捕获任何 OSError、IOError、socket.error、mmap.error、WindowsError、select.error 而不检查 errno 属性的代码。这是因为这些异常类型过于宽泛,无法表示任何特定的含义。它们中的任何一个都可能因各种错误条件而引发,例如:坏的文件描述符(通常表示编程错误)、未连接的套接字(同上)、套接字超时、文件类型不匹配、无效参数、传输失败、权限不足、不存在的目录、文件系统已满等。
(此外,某些异常的使用是不规则的;附录 B 揭示了 select 模块的情况,它根据实现方式引发不同的异常)
*细致* 代码定义为在捕获上述任何异常时,检查 errno 属性以确定实际错误条件并根据其采取行动的代码。
然后我们可以定义 *有用兼容性* 如下
- 有用兼容性不会使异常捕获变得更窄,但对于 *粗心* 的异常捕获代码,它可能会更广。给定以下代码片段,在此 PEP 之前捕获的所有异常在此 PEP 之后也将被捕获,但反之则可能不成立(因为
OSError、IOError和其他异常的合并意味着except子句撒下的网略宽一些)try: ... os.remove(filename) ... except OSError: pass
- 有用兼容性不会改变 *细致* 异常捕获代码的行为。给定以下代码片段,无论是否实现了本 PEP,相同的错误都应该被静默或重新引发
try: os.remove(filename) except OSError as e: if e.errno != errno.ENOENT: raise
这种折衷的理由是粗心代码无法真正得到帮助,但至少“工作”的代码不会突然引发错误并崩溃。这很重要,因为此类代码很可能存在于用作 cron 任务或自动化系统管理程序的脚本中。
另一方面,细致的代码不应受到惩罚。实际上,本 PEP 的一个目的是简化细致代码的编写。
步骤 1:合并异常类型
解决方案的第一步是合并现有异常类型。提出以下更改
- 将 socket.error 和 select.error 都别名为 OSError
- 将 mmap.error 别名为 OSError
- 将 WindowsError 和 VMSError 都别名为 OSError
- 将 IOError 别名为 OSError
- 将 EnvironmentError 合并到 OSError 中
这些更改中的每一个都不会保留精确的兼容性,但会保留 *有用兼容性*(参见上文“兼容性”部分)。
这些更改中的每一个都可以单独接受或拒绝,但当然,如果第一步被完全接受,则可以实现最大的影响。在这种情况下,IO 异常子层次结构将变为
+-- OSError (replacing IOError, WindowsError, EnvironmentError, etc.)
+-- io.BlockingIOError
+-- io.UnsupportedOperation (also inherits from ValueError)
+-- socket.gaierror
+-- socket.herror
+-- socket.timeout
理由
如 基本原理 部分所述,这第一步不仅为用户呈现了更简单的视图,而且还允许 步骤 2 得到更好、更完整的解决(参见 先决条件)。
保留 OSError 作为通用 OS 相关异常的官方名称的理由是,它比 IOError 更通用。 EnvironmentError 键入更繁琐,也鲜为人知。
附录 B 中的调查显示,IOError 是目前标准库中主要的错误。对于第三方 Python 代码,Google 代码搜索显示 IOError 在用户代码中的流行度是 EnvironmentError 的十倍,是 OSError 的三倍 [3]。然而,由于中期不打算弃用 IOError,OSError 流行度较低并不是问题。
异常属性
由于 WindowsError 合并到 OSError 中,后者在 Windows 下获得了 winerror 属性。在它没有意义的情况下,它被设置为 None,就像 errno、filename 和 strerror 属性一样(例如,当 OSError 直接由 Python 代码引发时)。
名称的弃用
以下段落概述了旧异常名称可能的弃用策略。然而,目前已决定将它们保留为别名。此决定可能会在 Python 4.0 发布时进行修订。
内置异常
弃用旧的内置异常不能通过拦截内置命名空间中的所有查找来直接完成,因为这些查找对性能至关重要。我们也不能在对象级别操作,因为已弃用的名称将被别名到未弃用的对象。
一种解决方案是在编译时识别这些名称,然后发出单独的 LOAD_OLD_GLOBAL 操作码,而不是常规的 LOAD_GLOBAL。当名称不存在于全局命名空间中,而只存在于内置命名空间中时,这个专门的操作码将处理 DeprecationWarning(或 PendingDeprecationWarning,取决于所决定的策略)的输出。这足以避免误报(例如,如果有人在模块中定义自己的 OSError),并且漏报将很少见(例如,当有人通过 builtins 模块而不是直接访问 OSError 时)。
模块级异常
上述方法不易使用,因为它需要在编译代码对象时对某些模块进行特殊处理。然而,这些名称在结构上可见性较低(它们不出现在内置命名空间中),也鲜为人知,因此我们可能会决定让它们存在于自己的命名空间中。
步骤 2:定义其他子类
解决方案的第二步是通过定义子类来扩展层次结构,这些子类将针对特定的 errno 值而不是其父类被引发。具体哪些 errno 值尚待讨论,但对现有异常匹配实践的调查(参见 附录 A)有助于我们提出所有值的一个合理子集。事实上,尝试映射所有 errno 助记符似乎是愚蠢、毫无意义的,并且会污染根命名空间。
此外,在少数情况下,不同的 errno 值可能会引发相同的异常子类。例如,EAGAIN、EALREADY、EWOULDBLOCK 和 EINPROGRESS 都用于表示非阻塞套接字上的操作将阻塞(因此稍后需要再次尝试)。因此,它们都可以引发相同的子类,并让用户在需要时检查 errno 属性(参见下文“异常属性”)。
先决条件
步骤 1 是对此的松散先决条件。
先决条件,因为某些 errno 目前可以附加到不同的异常类:例如,ENOENT 可以根据上下文附加到 OSError 和 IOError。如果我们不想破坏 *有用兼容性*,我们就不能让 except OSError(或 IOError)无法匹配今天会成功的异常。
松散的,因为如果现有异常类未合并,我们可以决定部分解决步骤 2:例如,ENOENT 可能在以前引发 IOError 的情况下引发假设的 FileNotFoundError,但在其他情况下继续引发 OSError。
如果新的子类使用多重继承来匹配所有现有的超类(或者至少是 OSError 和 IOError,这可能是最普遍的),那么对步骤 1 的依赖可以完全消除。然而,这将使层次结构更加复杂,因此用户更难理解。
新异常类
以下是提议的子类暂定列表,以及描述和映射到它们的 errno 列表,以供讨论
FileExistsError:尝试创建已存在的文件或目录 (EEXIST)FileNotFoundError:在所有请求文件和目录但不存在的情况下 (ENOENT)IsADirectoryError:在目录上请求文件级操作(open(), os.remove()…) (EISDIR)NotADirectoryError:在其他内容上请求目录级操作 (ENOTDIR)PermissionError:尝试在没有足够访问权限的情况下运行操作——例如文件系统权限 (EACCES, EPERM)BlockingIOError:操作将在设置为非阻塞操作的对象(例如套接字)上阻塞 (EAGAIN, EALREADY, EWOULDBLOCK, EINPROGRESS);这是现有io.BlockingIOError的扩展作用BrokenPipeError:尝试写入管道而另一端已关闭,或尝试写入已关闭写入的套接字 (EPIPE, ESHUTDOWN)InterruptedError:系统调用被传入信号中断 (EINTR)ConnectionAbortedError:连接尝试被对端中止 (ECONNABORTED)ConnectionRefusedError:连接被对端拒绝 (ECONNREFUSED)ConnectionResetError:连接被对端重置 (ECONNRESET)TimeoutError:连接超时 (ETIMEDOUT);这可以重新定义为通用超时异常,替换socket.timeout,也适用于其他类型的超时(例如 Lock.acquire() 中的超时)ChildProcessError:子进程上的操作失败 (ECHILD);这主要由 wait() 系列函数引发。ProcessLookupError:给定进程(例如,由其进程 ID 标识)不存在 (ESRCH)。
此外,提议包含以下异常类
ConnectionError:ConnectionAbortedError、ConnectionRefusedError和ConnectionResetError的基类
以下图表尝试总结了拟议的添加内容,以及相应的 errno 值(如果适用)。子层次结构的根(OSError,假设 步骤 1 被完全接受)未显示
+-- BlockingIOError EAGAIN, EALREADY, EWOULDBLOCK, EINPROGRESS
+-- ChildProcessError ECHILD
+-- ConnectionError
+-- BrokenPipeError EPIPE, ESHUTDOWN
+-- ConnectionAbortedError ECONNABORTED
+-- ConnectionRefusedError ECONNREFUSED
+-- ConnectionResetError ECONNRESET
+-- FileExistsError EEXIST
+-- FileNotFoundError ENOENT
+-- InterruptedError EINTR
+-- IsADirectoryError EISDIR
+-- NotADirectoryError ENOTDIR
+-- PermissionError EACCES, EPERM
+-- ProcessLookupError ESRCH
+-- TimeoutError ETIMEDOUT
命名
可能会出现各种命名争议。其中之一是所有异常类名称是否都应以“Error”结尾。支持方认为这与异常层次结构的其他部分保持一致,反对派则认为这会增加冗长(特别是对于像 ConnectionAbortedError 这样的长名称)。
异常属性
为了保持 *有用兼容性*,这些子类仍应为超类上定义的各种异常属性(例如 errno、filename 以及可选的 winerror)设置适当的值。
实施
由于提议子类的引发纯粹基于 errno 的值,因此扩展模块(无论是标准模块还是第三方模块)应基本无需更改。
第一种可能性是调整 PyErr_SetFromErrno() 函数家族(Windows 下为 PyErr_SetFromWindowsErr())以引发适当的 OSError 子类。但是,这不会涵盖直接引发 OSError 的 Python 代码,例如以下常见用法(在 Lib/tempfile.py 中可见)
raise IOError(_errno.EEXIST, "No usable temporary file name found")
Marc-Andre Lemburg 提出的第二种可能性是调整 OSError.__new__ 以实例化适当的子类。这具有同时涵盖上述 Python 代码的优点。
可能的异议
命名空间污染
使异常层次结构更细粒度会使根(或内置)命名空间更大。然而,这需要适度,因为
- 仅提出了少数附加类;
- 虽然标准异常类型存在于根命名空间中,但它们通过使用驼峰命名法与其他内置函数在视觉上有所区别,而几乎所有其他内置函数都使用小写命名(除了 True、False、None、Ellipsis 和 NotImplemented)
另一种选择是提供一个单独的模块来包含更细粒度的异常,但这将违背鼓励细致代码而非粗心代码的目的,因为用户必须首先导入新模块,而不是使用已经可访问的名称。
早前讨论
尽管这是首次提出如此正式的提案,但该想法在过去曾获得非正式支持 [1];包括引入更细粒度的异常类以及合并 OSError 和 IOError。
单独移除 WindowsError 作为 另一个 PEP 的一部分被讨论并拒绝,但似乎存在一种共识,即与 OSError 的区别没有意义。这至少支持将其与 OSError 别名。
实施
参考实现已集成到 Python 3.3 中。它以前在 http://hg.python.org/features/pep-3151/ 的 pep-3151 分支中开发,并在 bug 跟踪器 http://bugs.python.org/issue12555 上进行跟踪。它已在各种系统上成功测试:Linux、Windows、OpenIndiana 和 FreeBSD buildbots。
一个麻烦的来源是 OSError 和 WindowsError 的各自构造函数不兼容。解决方法是保留 OSError 签名并添加第四个可选参数,以允许传递 Windows 错误代码(这与 POSIX errno 不同)。第四个参数存储为 winerror,其 POSIX 转换存储为 errno。PyErr_SetFromWindowsErr* 函数已调整为使用正确的构造函数调用。
一个轻微的复杂情况是当使用 OSError 而不是 WindowsError 调用 PyErr_SetExcFromWindowsErr* 函数时:异常对象的 errno 属性将存储 Windows 错误代码(例如 ERROR_BROKEN_PIPE 的 109)而不是其 POSIX 转换(例如 EPIPE 的 32),而现在它存储的是 POSIX 转换。对于非套接字错误代码,这仅发生在私有 _multiprocessing 模块中,该模块没有兼容性问题。
注意
对于套接字错误,errno 模块所反映的“POSIX errno”在数值上等于 WSAGetLastError 系统调用返回的 Windows 套接字错误代码
>>> errno.EWOULDBLOCK
10035
>>> errno.WSAEWOULDBLOCK
10035
可能的替代方案
模式匹配
另一种可能性是在捕获异常时引入高级模式匹配语法。例如
try:
os.remove(filename)
except OSError as e if e.errno == errno.ENOENT:
pass
此提案的几个问题
- 它引入了新的语法,作者认为这比重构异常层次结构是一个更重大的改变
- 它并没有显著减少输入工作量
- 它并没有减轻程序员记住 errno 助记符的负担
本 PEP 忽略的异常
本 PEP 忽略 EOFError,它在各种协议和文件格式实现中(例如 GzipFile)表示输入流截断。EOFError 与操作系统或 IO 无关,它是更高层次上引发的逻辑错误。
本 PEP 还忽略了 SSLError,它由 ssl 模块引发,用于传播 OpenSSL 库发出的错误。SSLError 理想情况下将受益于类似但单独的处理,因为它定义了自己的错误类型常量(ssl.SSL_ERROR_WANT_READ 等)。在 Python 3.2 中,当 SSLError 表示套接字超时时,它已被 socket.timeout 替换(参见 问题 10272)。
最后,socket.gaierror 和 socket.herror 的命运尚未确定。虽然它们值得更清晰的名称,但这可以与异常层次结构重组工作分开处理。
附录 A:常见 errno 调查
这是标准库及其测试中,作为 except 子句的一部分,检查的各种 errno 助记符的快速清单。
与 OSError 相关的常见 errno
EBADF:无效文件描述符(通常表示文件描述符已关闭)EEXIST:文件或目录已存在EINTR:函数调用被中断EISDIR:是目录ENOTDIR:不是目录ENOENT:没有此类文件或目录EOPNOTSUPP:套接字不支持的操作(可能与现有 io.UnsupportedOperation 混淆)EPERM:操作不允许(例如使用 os.setuid() 时)
与 IOError 相关的常见 errno
EACCES:权限被拒绝(用于文件系统操作)EBADF:错误的文件描述符(与 select.epoll);对只写 GzipFile 进行读操作,反之亦然EBUSY:设备或资源忙EISDIR:是目录(尝试 open() 时)ENODEV:无此类设备ENOENT:无此类文件或目录(尝试 open() 时)ETIMEDOUT:连接超时
与 socket.error 相关的常见 errno
所有这些错误也可能与普通的 IOError 相关联,例如在套接字的文件描述符上调用 read() 时。
EAGAIN:资源暂时不可用(在非阻塞套接字调用期间,connect() 除外)EALREADY:连接已在进行中(在非阻塞 connect() 期间)EINPROGRESS:操作正在进行中(在非阻塞 connect() 期间)EINTR:函数调用被中断EISCONN:套接字已连接ECONNABORTED:连接被对端中止(在 accept() 调用期间)ECONNREFUSED:连接被对端拒绝ECONNRESET:连接被对端重置ENOTCONN:套接字未连接ESHUTDOWN:传输端点关闭后无法发送EWOULDBLOCK:与EAGAIN相同的原因
与 select.error 相关的常见 errno
EINTR:函数调用被中断
附录 B:引发的操作系统和 IO 错误调查
关于 VMSError
VMSError 完全不被解释器核心和标准库使用。它是作为 Jean-François Piéronne 在 2002 年提交的 OpenVMS 补丁的一部分添加的 [4];引入 VMSError 的动机是它可能被第三方软件包引发。
解释器核心
PYTHONSTARTUP 的处理引发 IOError(但错误被丢弃)
$ PYTHONSTARTUP=foox ./python
Python 3.2a0 (py3k:82920M, Jul 16 2010, 22:53:23)
[GCC 4.4.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
Could not open PYTHONSTARTUP
IOError: [Errno 2] No such file or directory: 'foox'
PyObject_Print() 在 ferror() 对 FILE * 参数(在源代码树中,总是 stdout 或 stderr)发出错误信号时引发 IOError。
使用 mbcs 编码进行 Unicode 编码和解码在某些错误条件下可能会引发 WindowsError。
标准库
bz2
始终引发 IOError(未使用 OSError)
>>> bz2.BZ2File("foox", "rb")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IOError: [Errno 2] No such file or directory
>>> bz2.BZ2File("LICENSE", "rb").read()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IOError: invalid data stream
>>> bz2.BZ2File("/tmp/zzz.bz2", "wb").read()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IOError: file is not ready for reading
curses
未检查。
dbm.gnu, dbm.ndbm
_dbm.error 和 _gdbm.error 继承自 IOError
>>> dbm.gnu.open("foox")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
_gdbm.error: [Errno 2] No such file or directory
fcntl
始终引发 IOError(未使用 OSError)。
imp 模块
对无效文件描述符引发 IOError
>>> imp.load_source("foo", "foo", 123)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IOError: [Errno 9] Bad file descriptor
io 模块
在 Unix 下尝试打开目录时引发 IOError
>>> open("Python/", "r")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IOError: [Errno 21] Is a directory: 'Python/'
对于不支持的操作,引发 IOError 或 io.UnsupportedOperation(继承自前者)
>>> open("LICENSE").write("bar")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IOError: not writable
>>> io.StringIO().fileno()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
io.UnsupportedOperation: fileno
>>> open("LICENSE").seek(1, 1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IOError: can't do nonzero cur-relative seeks
当底层 I/O 层行为异常(即违反其应实现的 API)时,引发 IOError 或 TypeError。
当底层 OS 资源失效时引发 IOError
>>> f = open("LICENSE")
>>> os.close(f.fileno())
>>> f.read()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IOError: [Errno 9] Bad file descriptor
...或用于实现特定的优化
>>> f = open("LICENSE")
>>> next(f)
'A. HISTORY OF THE SOFTWARE\n'
>>> f.tell()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IOError: telling position disabled by next() call
当对非阻塞对象进行调用会阻塞时,引发 BlockingIOError(继承自 IOError)。
mmap
在 Unix 下,始终引发其自己的 mmap.error(继承自 EnvironmentError)
>>> mmap.mmap(123, 10)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
mmap.error: [Errno 9] Bad file descriptor
>>> mmap.mmap(os.open("/tmp", os.O_RDONLY), 10)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
mmap.error: [Errno 13] Permission denied
然而,在 Windows 下,它主要引发 WindowsError(源代码也显示了几处 mmap.error 的出现)
>>> fd = os.open("LICENSE", os.O_RDONLY)
>>> m = mmap.mmap(fd, 16384)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
WindowsError: [Error 5] Accès refusé
>>> sys.last_value.errno
13
>>> errno.errorcode[13]
'EACCES'
>>> m = mmap.mmap(-1, 4096)
>>> m.resize(16384)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
WindowsError: [Error 87] Paramètre incorrect
>>> sys.last_value.errno
22
>>> errno.errorcode[22]
'EINVAL'
多进程
未检查。
os / posix
os(或 posix)模块始终引发 OSError,但在 Windows 下可能会改为引发 WindowsError。
ossaudiodev
始终引发 IOError(未使用 OSError)
>>> ossaudiodev.open("foo", "r")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IOError: [Errno 2] No such file or directory: 'foo'
readline
在各种文件处理函数中引发 IOError
>>> readline.read_history_file("foo")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IOError: [Errno 2] No such file or directory
>>> readline.read_init_file("foo")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IOError: [Errno 2] No such file or directory
>>> readline.write_history_file("/dev/nonexistent")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IOError: [Errno 13] Permission denied
select
- select() 和 poll 对象引发
select.error,它不继承自任何东西(但 poll.modify() 引发 IOError); - epoll 对象引发 IOError;
- kqueue 对象同时引发 OSError 和 IOError。
顺便提一下,不派生自 EnvironmentError 意味着 select.error 没有获得有用的 errno 属性。用户代码必须检查 args[0]
>>> signal.alarm(1); select.select([], [], [])
0
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
select.error: (4, 'Interrupted system call')
>>> e = sys.last_value
>>> e
error(4, 'Interrupted system call')
>>> e.errno == errno.EINTR
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'error' object has no attribute 'errno'
>>> e.args[0] == errno.EINTR
True
signal
signal.ItimerError 继承自 IOError。
socket
socket.error 继承自 IOError。
sys
如果 GetVersionEx() 调用失败,sys.getwindowsversion() 会引发带有虚假错误号的 WindowsError。
time
在 time.time() 和 time.sleep() 的内部错误中引发 IOError。
zipimport
zipimporter.get_data() 可能引发 IOError。
致谢
Alyssa Coghlan 提供了重要输入。
参考资料
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-3151.rst