6-2-异常编码细节

1. try/except/else 语句

try 是复合语句,它的最完整的形式如下:

1
2
3
4
5
6
7
8
9
10
11
12
try:
<statements>
except <name1>:
<statements>
except (name2, name3):
<statements>
except <name4> as <data>:
<statements>
except:
<statements>
else:
<statements>

1.1 语句的运行方式

  • 如果 try 代码块语句执行时的确发生了异常,Python 就跳回 try,执行第一个符合引发异常的 except 子句下面的语句。当 except 代码块执行后,控制权就会到整个 try 语句后继续执行。
  • 如果异常发生在 try 代码块内,没有符合的 except 子句,异常就会向上传递到程序中的之前进入的 try 中,或者如果它是第一条这样的语句,就传递到这个进程的顶层,Python 会终止程序并打印默认出错消息。
  • 如果 try 首行底下执行的语句没有发生异常,Python 就会执行 else 行下的语句,控制权会在整个 try 语句下继续。

1.2 try 语句分句

分句形式 说明

except:|捕捉所有(其他)异常类型

except name:|只捕捉特定的异常

except name as value:|捕捉所列的异常并赋值其实例

except (name1, name2):|捕捉任何列出的异常

except (name1, name2) as value:|捕捉任何列出的异常并赋值其实例

else:|如果没有引发异常,就运行

finally:|总是会运行此代码块

如果想要编写通用的“捕捉一切”分句,空的 except 就可以做到。尽管方便,空 except 也可能捕捉和程序代码无关、意料之外的系统异常,而且可能意外拦截其他处理器的异常。

Python 3.X 引入了一个替代方案来解决这些问题之一——捕获一个名为 Exception 的异常几乎与一个空的 except 具有相同的效果,但是忽略和系统退出相关的异常:

1
2
3
4
try:
action()
except Exception:
...

1.3 try/else 分句

没有 else,是无法知道控制流程是否已经通过了 try 语句,因为没有异常引发或者因为异常发生了且已被处理过。

2. try/finally 语句

  • 如果 try 代码块运行时没有异常发生,Python 会跳至执行 finally 代码块,然后在整个 try 语句后继续执行下去。
  • 如果 try 代码块运行时有异常发生,Python 依然会回来运行 finally 代码块,但是接着会把异常向上传递到较高的 try 语句或顶层默认处理器。程序不会在 try 语句下继续运行。

finally 可以和 except 及 else 出现在相同语句内。

3. 统一 try/except/finally 语句

1
2
3
4
5
6
7
8
9
10
11
try:
main-action
except Exception1:
handler1
except Exception2:
handler2
...
else:
else-block
finally:
finally-block

即使异常处理器或者 else-block 内有错误发生而引发了新的异常,finally-block 内的程序代码依然会执行。

3.1 统一 try 语句语法

方括号表示可选,星号表示0个或多个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# format1
try:
statements
except [type [as value]]:
statements
[except [type [as value]]:
statements]*
[else:
statements]
[finally:
statements]

# format2
try:
statements
finally:
statements

至少有一个 except 的时候,else 才能够出现,并且总是可能混合 except 和 finally,而不管是否有一个 else。也可能混合 finally 和 else,但只在 except 也出现的时候。

4. raise 语句

要显式地触发异常,可以使用 raise 语句。raise 关键字,后面跟着可选的要引发的类或者类的一个实例。

4.1 引发异常

对于内置异常,如下两种形式是对等的,都会引发指定的异常类的一个实例,但是第一种形式隐式地创建实例:

1
raise IndexError                      # 类
---------------------------------------------------------------------------

IndexError                                Traceback (most recent call last)

<ipython-input-1-55a00e7db5b5> in <module>()
----> 1 raise IndexError

IndexError: 
1
raise IndexError()                    # 实例
---------------------------------------------------------------------------

IndexError                                Traceback (most recent call last)

<ipython-input-2-7811308d6908> in <module>()
----> 1 raise IndexError()

IndexError: 

当引发一个异常的时候,Python 把引发的实例与该异常一起发送。如果一个 try 包含了一个名为 except name as X: 的子句,变量 X 将会分配给引发中所提供的实例:

1
2
3
4
try:
pass
except IndexError as X: # 引发的实例对象赋值给 X
pass

包含 as 使得处理器能够访问实例中的数据以及异常类中的方法。

一旦异常在程序中某处由一条 except 子句捕获,它就死掉了,除非由另一个 raise 语句或错误重新引发它。

4.2 利用 raise 传递异常

raise 语句不包括异常名称或额外数据值时,就是重新引发当前异常。如果需要捕捉和处理一个异常,又不希望异常在程序代码中死掉,一般就会使用这种形式:

1
2
3
4
5
try:
raise IndexError('spam')
except IndexError:
print('propagating')
raise
propagating

---------------------------------------------------------------------------

IndexError                                Traceback (most recent call last)

<ipython-input-4-30f65ef43030> in <module>()
      1 try:
----> 2     raise IndexError('spam')
      3 except IndexError:
      4     print('propagating')
      5     raise

IndexError: spam

通过这种方式执行 raise 时,会重新引发异常,并将其传递给更高层的处理器。

4.3 Python 3.X 异常链:raise from

Python 3.X 允许 raise 语句拥有一个可选的 from 子句:

1
raise exception from otherexception

当使用 from 的时候,第二个表达式指定了另一个异常类或实例,它会附加到引发异常的 __cause__ 属性。如果引发的异常没有捕获,Python 把异常也作为标准出错消息的一部分打印出来:

1
2
3
4
try:
1 / 0
except Exception as E:
raise TypeError('bad!') from E
---------------------------------------------------------------------------

ZeroDivisionError                         Traceback (most recent call last)

<ipython-input-5-a11a717782c4> in <module>()
      1 try:
----> 2     1 / 0
      3 except Exception as E:

ZeroDivisionError: division by zero

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

TypeError                                 Traceback (most recent call last)

<ipython-input-5-a11a717782c4> in <module>()
      2     1 / 0
      3 except Exception as E:
----> 4     raise TypeError('bad!') from E

TypeError: bad!

5. assert 语句

assert 语句是 raise 常见使用模式的语法简写,assert 可视为条件式的 raise 语句:

1
assert <test>, <data>

如果 test 计算为假,Python 就会引发异常:data 项(如果提供了的话)是异常的额外数据。

5.1 例子:收集约束条件(但不是错误)

assert 语句通常用于验证开发期间程序的状况。显示时,其出错消息正文会自动包括源代码的行信息,以及列在 assert 语句中的值:

1
2
3
4
5
def f(x):
assert x < 0, 'x must be negative'
return x ** 2

f(1)
---------------------------------------------------------------------------

AssertionError                            Traceback (most recent call last)

<ipython-input-6-c4fb041f2e31> in <module>()
      3     return x ** 2
      4 
----> 5 f(1)

<ipython-input-6-c4fb041f2e31> in f(x)
      1 def f(x):
----> 2     assert x < 0, 'x must be negative'
      3     return x ** 2
      4 
      5 f(1)

AssertionError: x must be negative

assert 几乎都是用来收集用户定义的约束条件,而不是捕捉内在的程序设计错误。因为 Python 会自行收集程序的设计错误,通常来说,没必要写 assert 去捕捉超出索引值、类型不匹配以及除数为零之类的事情。

1
2
3
4
5
def reciprocal(x):
assert x != 0 # 没用的 assert
return 1 / x # 自动检测 zero

reciprocal(0)
---------------------------------------------------------------------------

AssertionError                            Traceback (most recent call last)

<ipython-input-7-f01ca2928417> in <module>()
      3     return 1 / x
      4 
----> 5 reciprocal(0)

<ipython-input-7-f01ca2928417> in reciprocal(x)
      1 def reciprocal(x):
----> 2     assert x != 0
      3     return 1 / x
      4 
      5 reciprocal(0)

AssertionError: 

6. with/as 环境管理器

with/as 语句是作为常见 try/finally 用法模式的替代方案。with 语句支持更丰富的基于对象的协议,可为代码块定义支持进入和离开的动作。

6.1 基本使用

1
2
with expression [as variable]:
with-block

expression 要返回一个对象,此对象也可返回一个值,赋值给变量名 variable。

6.2 环境管理协议

with 语句实际工作方式:

  1. 计算表达式,所得到的对象称为环境管理器,它必须有 __enter____exit__ 方法。
  2. 环境管理器的 __enter__ 方法会被调用。如果 as 子句存在,其返回值会赋值给 As 子句中的变量,否则,直接丢弃。
  3. 代码块中嵌套的代码会执行。
  4. 如果 with 代码块引发异常,__exit__(type, value, traceback) 方法就会被调用。这些也是由 sys.exc_info 返回的相同值。如果此方法返回值为假,则异常会重新引发。否则,异常会终止。正常情况下异常是应该被重新引发,这样的话才能传递到 with 语句之外。
  5. 如果 with 代码块没有引发异常,__exit__ 方法依然会被调用,其 type、value 以及 traceback 参数都会以 None 传递。

6.3 多环境管理器

with 语句可以使用新的逗号语法指定多个环境管理器:

1
2
3
4
with open('data') as fin, open('res', 'w') as fout:
for line in fin:
if 'some key' in line:
fout.write(line)