Exceptions

异常 exceptions 具有一些标准属性,这些属性在需要针对错误执行进一步操作的代码中可能非常有用。

PYTHON
e.args
Click to expand and view more

这是引发异常时提供的元组,在大多数情况下,这是一个包含描述错误字符串的单元元素元组。 对于 OSError 异常,其值是一个包含整数错误码、字符串错误消息,以及可选文件名的 2 元组或 3 元组。

PYTHON
e.__cause__
Click to expand and view more

如果该异常是在处理另一个异常时有意引起的 raise ... from ...,Python 会将这两个异常链接起来,形成异常链。

PYTHON
e.__context__
Click to expand and view more

如果异常是处理异常时无意间导致的,则会产生 e.__context__

PYTHON
e.__traceback__
Click to expand and view more

与异常相关联的堆栈回溯对象。

用于存储异常值的变量仅在相关的 except 块内部可以访问,一但控制论离开该块,该变量将变为未定义。

PYTHON
try:
    int('N/A')
except ValueError as e:
    print('Failed:', e)

print(e) # Fails -> NameError. 'e' not defined
Click to expand and view more

多异常处理块通过多个异常子句指定:

PYTHON
try:
    # do something
except TypeError as e:
    # Handle Type error
except ValueError as e:
    # Handle Value error
Click to expand and view more

当然也可以在单个子句中处理多个异常类型

PYTHON
try:
    # do something
except (TypeError, ValueError) as e:
    # Handle Type or Value error
Click to expand and view more

可以使用 pass 忽略报错

PYTHON
try:
    # do something
except ValueError:
    pass
Click to expand and view more

通常静默忽略报错是危险的,通常会引起许多奇怪的 bug,即使要忽略,也要通过某种方式报告异常的发生。

如果程序要捕捉除退出外的任何异常,可以这样:

PYTHON
try:
    # do something
except Exception as e:
    print(f'An error occured: {e!r}')
Click to expand and view more

其中,这里的 e!r 是将信息转化为 repr() 方便输出。

写法等价于含义
{e}str(e)人类可读字符串
{e!s}str(e)同上
{e!r}repr(e)面向开发者,精确表示
{e!a}ascii(e)仅 ASCII 表示

try 语句也支持 else 块,该块必须跟在 except 后面,如果没有引起对应异常就会执行 else 里面的内容:

PYTHON
try:
    file = open('foo.txt', 'rt')
except FileNotFoundError as e:
    print(f'Unable to open foo:' {e})
else:
    data = file.read()
    file.close()
Click to expand and view more

finally 中定义无论如何都要执行的清理操作,例如:

PYTHON
file = open('foo.txt', 'rt')
try:
    # do some stuff
finally:
    file.close()
    # File closed regardless of what happened
Click to expand and view more

finally 不是用来捕获异常的,而是执行无论是否出现异常都要执行的代码。 如果没有异常产生,则会立刻执行 finally 块中代码。

The Exception Hierarchy

异常层次结构

异常处理的一大挑战就是管理大量潜在的可能产生的异常。 例如,仅内置异常就有 60 都多种,此外,还有标准库中的各种数百种异常。 通常没有任何办法去确定代码可能产生的异常类型。

异常并未作为函数调用签名的一部分被记录,也没用任何编译器来验证代码中的异常处理是否正确。 因此,异常处理有时会显得随意且缺乏条理。

在管理异常时,一个有用的工具是认识到他们通过继承被组织成一个层次结构。 在写代码时不使用具体的异常,而是聚焦于更加通用的异常类别。

例如,在容器中查找值时可能出现各种错误:

PYTHON
try:
    item = items[index]
except IndexError:  # Raised if items is a seqence
    ...
except KeyError:  # Raised if items is a mapping
    ...
Click to expand and view more

不去判断两种具体的错误,而像下面这样:

PYTHON
try:
    item = items[index]
except LookupError:
    ...
Click to expand and view more

LookupError 是一个表示异常高层级分组的类。 IndexErrorKeyError 都继承自 LookupError,因此这里会捕获这里两个报错。

下表描述了常见的内置异常

异常类型描述
BaseException所有异常的根类型 Root class
Exception所有程序相关的 (program-related) 基本异常类型 Base class
ArithmeticError所有数学相关的 (math-related) 基本异常类型 Base class
ImportError所有导入相关的 (import-related) 基本异常类型
LookupError所有容器查找或范围相关的 (container lookup) 基本异常类型
OSError所有系统相关的 (system-related) 基本异常类型 (alias: IOError, EnvironmentError)
ValueError所有值错误相关的 (value-related) 基本异常类型,包含 Unicode
UnicodeError有关 Unicode 字符串编码的基本异常类型

其中,BaseException 类型很少使用,因为其会匹配所有可能的异常。 包括影响程序控制留的异常,例如 SystemExit, KeyboardInterruptStopIteration,捕获这些异常并发本意。 反而,所有普通的程序错误都继承 Exception

下表是一些直接继承自 Exception 的异常,但不属于一个大的异常组中。

Exception ClassDescription
NameErrorName not found in the local or global namespace
NotImplementedErrorUnimplemented feature
RuntimeErrorA generic “something bad happened” error
TypeErrorOperation applied to an object of the wrong type
UnboundLocalErrorUsage of a local variable before a value is assigned
AssertionErrorFailed assert statement
AttributeErrorBad attribute lookup on an object
EOFErrorEnd of File
MemoryErrorRecoverable out of memory error

Exceptions and Control Flow

一般异常都是用于错误处理的,然而有几个异常用于修改程序控制流,下表中的几个都直接继承自 BaseException

Exception ClassDescription
SystemExitRaised to indicate program exit
KeyboardInterruptRaised a programe is interrupted vai Control-C
StopIterationRaised to signal the end of iteration

SystemExit 用于让程序按预期终止,作为参数可以提供一个整数退出码或字符串消息。 如果提供一个字符串,会向 sys.stderr 输出,并以退出码 1 退出。

PYTHON
import sys

if len(sys.argv != 2):
    raise SystemExit(f'Usage: {sys.argv[0]} filename')
filename = sys.argv[1]
Click to expand and view more

当程序接收到 SIGINT 信号(通常按下 Ctrl-C 触发)时,会引发 KeyboardInterrupt 异常。 该异常的特殊之处在于它是异步的,这意味着它几乎可以在程序执行的任何时刻、任何语句处发生。 Python 的默认行为是在此处直接终止程序,若需要控制 SIGINT 信号传递,可以使用 signal 库。

StopIteration 异常是迭代协议的一部分,用于表示迭代结束。

Defining New Exceptions

如果要创建自定义异常,继承 Exception 类:

PYTHON
class NetworkError(Exception):
    pass
Click to expand and view more

抛出自定义异常也使用 raise 语句:

PYTHON
raise NetworkError('Cannot find host')
Click to expand and view more

当引发异常时,raise 语句提供的可选值将作为异常类构造函数的参数。 大多数情况下,这是一个表示某种错误的字符串。 然而,用户自定义的异常可以设计为接收一个或多个异常值:

PYTHON
class DeviceError(Exception):
    def __init__(self, errno, msg):
        self.args = (errno, msg)
        self.errno = errno
        self.errmsg = msg

# Raises an exception (multiple arguments)
raise DeviceError(1, 'Not Responding')
Click to expand and view more

args 是异常类的特殊属性,当异常被捕获但没有指定具体变量是,args 会自动添加。 该属性用于打印异常回溯信息,如果未定义该属性,当错误发生时,用户将无法看到任何有关异常的有用信息。

通过继承,可以将异常组织成一个层次结构。

PYTHON
class HostnameError(NetworkError):
    pass

class TimeoutError(NetworkError):
    pass

def error1():
    raise HostnameError('Unkonw host')

def error2():
    raise TimeoutError('Timed otu')

try:
    error1()
except NetworkError as e:
    if type(e) is HostnameError:
        # Perform speical actions for this kind of error
Click to expand and view more

Chained Exceptions

有时候你可能会向抛出一个链式异常

PYTHON
class ApplicationError(Exception):
    pass

def do_something():
    x = int('N/A')  # raise ValueError

def spam():
    try:
        do_something()
    except Exception as e:
        raise ApplicationError('It failed') from e
Click to expand and view more

如果抛出 ApplicationError 报错,则会有以下报错信息:

TEXT
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[3], line 3, in spam()
      2 try:
----> 3     do_something()
      4 except Exception as e:

Cell In[2], line 2, in do_something()
      1 def do_something():
----> 2     x = int('N/A')

ValueError: invalid literal for int() with base 10: 'N/A'

The above exception was the direct cause of the following exception:

ApplicationError                          Traceback (most recent call last)
Cell In[4], line 1
----> 1 spam()

Cell In[3], line 5, in spam()
      3     do_something()
      4 except Exception as e:
----> 5     raise ApplicationError('It failed') from e

ApplicationError: It failed
Click to expand and view more

如果捕获 ApplicationError,则 __cause__ 属性会包含其他异常,例如:

PYTHON
try:
    spam()
except ApplicationError as e:
    print('It failed. Reason:', e.__cause__)
Click to expand and view more

输出如下:

TEXT
It failed. Reason: invalid literal for int() with base 10: 'N/A'
Click to expand and view more

若想在不包含其他异常链的情况下引发新异常,可以 raise ... from None

PYTHON
def spam():
    try:
        do_something()
    except Exception as e:
        raise ApplicationError('It failed') from None
Click to expand and view more

输出如下:

TEXT
---------------------------------------------------------------------------
ApplicationError                          Traceback (most recent call last)
Cell In[9], line 1
----> 1 spam()

Cell In[8], line 5, in spam()
      3     do_something()
      4 except Exception as e:
----> 5     raise ApplicationError('It failed') from None

ApplicationError: It failed
Click to expand and view more

出现在 except 块中的编程错误同样会导致链式异常,但其运作方式略有不同。 假设有如下缺陷的代码:

PYTHON
def spam():
    try:
        do_something()
    except Exception as e:
        print('It failed:', err)  # str undefined (typo)
Click to expand and view more

这样导致的报错有些许不同:

TEXT
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[4], line 3, in spam()
      2 try:
----> 3     do_something()
      4 except Exception as e:

Cell In[3], line 2, in do_something()
      1 def do_something():
----> 2     x = int('N/A')

ValueError: invalid literal for int() with base 10: 'N/A'

During handling of the above exception, another exception occurred:

NameError                                 Traceback (most recent call last)
Cell In[5], line 1
----> 1 spam()

Cell In[4], line 5, in spam()
      3     do_something()
      4 except Exception as e:
----> 5     print('It failed:', err)

NameError: name 'err' is not defined
Click to expand and view more

Exception Trackbacks

异常栈回调信息在 __traceback__ 属性中,为了报告 bug,可能需要自己生成回溯信息。 使用 traceback 模块来实现:

PYTHON
import traceback

try:
    spam()
except Exception as e:
    tblines = traceback.format_exception(type(e), e, e.__traceback__)
    tbmsg = ''.join(tblines)
    print('It failed:')
    print(tbmsg)
Click to expand and view more

format_exception() 函数格式化异常信息,返回一个字符串,每个字符串是堆栈跟踪的一行

输出如下

TEXT
It failed
Traceback (most recent call last):
  File "<ipython-input-4-893664aa1d25>", line 3, in spam
    do_something()
  File "<ipython-input-3-0308b00b259c>", line 2, in do_something
    x = int('N/A')
        ^^^^^^^^^^
ValueError: invalid literal for int() with base 10: 'N/A'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<ipython-input-7-747b1ec03c7f>", line 2, in <module>
    spam()
  File "<ipython-input-4-893664aa1d25>", line 5, in spam
    print('It failed:', err)
                        ^^^
NameError: name 'err' is not defined
Click to expand and view more

Exception Handling Advice

异常处理是大型应用程序里很难的一部分,下面是一些实用的规则:

第一条规则是不要捕获哪些在代码特定位置无法直接处理的异常,例如

PYTHON
def read_data(filename):
    with open(filename, 'rt') as file:
        rows = []
        for line in file:
            row = line.split()
            row.append((row[0], int(row[1]), float(row[2])))
    return rows
Click to expand and view more

假如 open() 函数传入了一个错误的文件名,但其内部不应该判断这个异常。 read_data() 应该抛出异常,并在外面处理这个可能的问题,例如给函数提供文件名的部分应该处理这个问题。

另一方面,函数或许能从错误中恢复:

PYTHON
def read_data(filename):
    with open(filename, 'rt') as file:
        rows = []
        for line lin file:
            row = line.split
            try:
                row.append((row[0], int(row[1]), float(row[2])))
            except ValueError as e:
                print('Bad row:', row)
                pirnt('Reason:', e)
        return rows
Click to expand and view more

在捕获异常时,尽量使用 except 子句的范围合理精确。 上述代码本可以通过 except Exception 来捕获所有错误,但这样会导致代码捕获本身不应该被忽略的合法编程错误,这会使调试困难。

最后,如果要显示引发异常,考虑自定义异常

PYTHON
class ApplicationError(Exception):
    pass

class UnauthorizedUserError(ApplicationError):
    pass

def spam():
    ...
    raise UnauthorizedUserError('Go away')
    ...
Click to expand and view more

这看似细微,但在大型代码库中,更棘手的问题之一是如何确定程序故障的责任归属。 如果要自定义异常,最好能够区分合法编程异常和故意抛出的异常。 例如,如果代码抛出上面的 ApplicationError 那么你立刻就知道为什么会抛出这个异常。 另一方面,如果抛出了内置异常,那通常意味着更加严重的问题。

Contenxt Managers and the with Statement

管理系统资源,例如文件、锁和连接相关的异常通常是一个棘手的问题。

例如,一个被抛出的异常可能导致控制流跳过负责释放关键资源(如锁)的语句。

with 语句允许一系列语句在运行时上下文中执行,该上下文充当上下文管理器的对象控制。

PYTHON
with open('debuglog', 'wt') as file:
    file.write('Debugging\n')
    statements
    file.write('Done\n')

import threading
lock = threading.Lock()
with Lock:
    # Critical section
    statements
    # End critical section
Click to expand and view more

在第一个示例中,当控制流离开后续语句时,with 语句会自动关闭已打开的文件。 在第二个示例中,当控制进入和离开后续语句时,with 语句会自动获取并释放锁。

with obj 语句允许对象 obj 管理控制流进入和退出其关联代码块时的行为。 当 with obj 语句执行时,其会调用 obj.__enter__ 表示创建了一个新的上下文。 当离开该上下文时,会调用 obj.__exit__(type, value, traceback) 方法。 如果没有引发任何异常,这三个参数都设置为 None。 否则,他们包含与导致控制流离开上下文的异常相关类型、值和回溯信息。

如果 __exit__() 返回 True,则说明异常已经被正确处理,不应该被传播。 返回 NoneFalse 会导致异常传播。

with obj 语句接受一个可选的 as var 指定符 (specifier)。 如果指定,obj.__enter__() 返回的值将被赋予给 var。 这个值通常与 obj 相同,因为这允许在同一个步骤中构造对象并将其用作上下文管理器。

考虑下面类:

PYTHON
class Manager:
    def __init__(self, x):
        self.x = x

    def yow(self):
        pass

    def __enter__(self):
        return self

    def __exit__(self, ty, val, tb):
        pass
Click to expand and view more

你可以在上下文管理器中创建并使用一个实例:

PYTHON
with Manager(42) as m:
    m.yow()
Click to expand and view more

下面是一个关于 list transactions 的例子:

PYTHON
class ListTransaction:
    def __init__(self, thelist):
        self.thelist = thelist

    def __enter__(self):
        self.workingcopy = list(self.thelist)
        return self.workingcopy

    def __exit__(self, type, value, tb):
        if type is None:
            self.thelist[:] = self.workingcopy
            return False
Click to expand and view more

该类允许对现有列表进行一系列修改,但只有在未发生任何异常的情况下,修改才会生效。 否则,原始列表将保持不变。

PYTHON
items = [1, 2, 3]

with ListTransaction(items) as working:
    working.append(4)
    working.append(5)
    print(items)  # Produces [1, 2, 3, 4, 5]

try:
    with ListTransaction(item) as working:
        working.apned(6)
        working.apned(7)
        raise RuntimeError("We're hosed!")
except RuntimeError:
    pass

print(items)  # [1, 2, 3, 4, 5]
Click to expand and view more

contextlib 库包含更多关于上下文管理器的高级用法。 如果经常使用上下文管理器,该库值得一看。

Assertions and __debug__

assert 语句可以在程序中引入调试代码,一般形式是:

PYTHON
assert test [, msg]
Click to expand and view more

其中 test 是一个返回 TrueFalse 的表达式。 如果为 Falseassert 会抛出一个 AssertionError,并包含一条 msg 信息

PYTHON
def write_data(file, data):
    assert file, 'write_data: file not defiend!'
Click to expand and view more

断言语句不应用于必须执行,以确保程序正确性的代码。 因为 Python 以优化模式允许时(通过解释器的 -O 选项指定),这些断言将不会执行。

因此使用断言来检查用户输入或某些重要的操作结果是错误的,assert 用于永远应该为 True 的不变量。 如果这一项被违反,则应该报一个 bug,而不是给用户一个 error。

例如,如果之前展示的 write_data() 函数旨在提供最终用户使用,那么 assert 语句应该替换为常规的 if 语句,并配合适当的错误处理机制。 assert 的使用常见于测试中,例如,你可能使用其包含一个最小测试:

PYTHON
def factorial(n):
    result = 1
    while n > 1:
        return *= n
        n -= 1
    return result

assert factorial(5) == 120
Click to expand and view more

这种测试不是为了详尽,而是为了类似“冒烟测试”的功能。 如果函数中存在明显的错误,代码在导入时会因断言失败而立刻崩溃。

断言在指定预期输入和输出的预期方面也很有用。

PYTHON
def factorial(n):
    assert n > 0, "must supply a postive value"
    result = 1
    while n > 1:
        result *= n
        n -= 1
    return result
Click to expand and view more

同样,这不是为了检查用户输入。 这更多是用于检查系统内部的一致性,如果其他代码传入负数,那么就会报错,这样会方便调试。

Final Words

尽管 Python 支持多种涉及函数和对象的编程风格,但其程序执行的基本模型仍属于命令式编程。 异常处理需要非常谨慎对待的部分,尤其是设计库、框架和 API 时,异常还可能严重影响妥善管理,这些问题通常需要使用上下文管理器来解决。

Start searching

Enter keywords to search articles

↑↓
ESC
⌘K Shortcut