/ Python

对 Python 异常处理的笔记

简述 Pro Python 这本书里对 Python 异常处理的讨论。

包括异常的捕获、处理,异常链,以及异常处理相关的 else, finally, with 关键字等内容。


最简单的捕获异常的方式

def count_lines(filename):
    try:
        return len(open(filename, 'r').readlines())
    except:
        return 0

很少有人在生产环境写这样的代码,它会捕获所有的异常,可能包括你希望处理的,也包括你其实并不想处理的异常,都会走到 except 下的代码块里,执行了 return 0 这行代码。多数情况下,这可能不是我们想要的结果。

好的做法是明确写出自己希望要捕获、要处理的异常类型。

def count_lines(filename):
    try:
        return len(open(filename, 'r').readlines())
    except IOError:
        return 0

这样如果出现 IOError 类型的异常(比如文件不存在,不可读等),会执行 return 0 语句。如果出现了不是 IOError 的异常,比如给这个函数传进去一个字典作为参数,则会抛出 TypeError 异常。

建议使用 logging 模块来输出异常信息,由 as 关键字来提供一个包含了异常信息的变量。

import logging

def count_lines(filename):
    try:
        return len(open(filename, 'r').readlines())
    except IOError as e:
        logging.error(e)
        return 0

捕获多个异常

如果要捕获多个异常,而且希望对这多个异常都执行一样的处理逻辑

def count_lines(filename):
    try:
        return len(open(filename, 'r').readlines())
    except (EnvironmentError, TypeError):
        logging.error(e)
        return 0

这样捕获到 EnvironmentErrorTypeError进行同样的处理。

如果要捕获多种异常,对各种异常做不同的处理

import logging

def count_lines(filename):
    try:
        return len(open(filename, 'r').readlines())
    except TypeError as e:
        logging.error(e)
        return 0
    except EnvironmentError as e:
        logging.error(e.args[1])
        return 0

这里对 TypeError 异常,输出错误的内容(e),如果捕获到 EnvironmentError 异常,输出 e.args[1](EnvironmentError 异常里,e.args[0] 是错误码,e.args[1] 是错误信息)。

Exception Chain

如果在处理异常的过程中,这块处理异常的代码又出现了一个异常,像下面这段代码

def get_value(dictionary, name):
	try:
    	return dictionary[name]
    except Exception as e:
    	log = open('logfile.txt', 'w')
        log.write('%s\n' % e)
        log.close()

这样调用 get_value 函数

get_value({}, 'test')`

会走到异常处理的逻辑,如果 logfile.txt 是只读的,那么会出现另外一个异常。

Traceback (most recent call last):
...
KeyError: 'test'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
...
IOError: [Errno 13] Permission denied: 'logfile.txt'

写文件时出现的这个异常会额外包含一个 __context__ 属性,它保存着之前的那个异常(KeyError)

下面是另外一个例子,主动抛出 (raise) 的异常

def validate(value, validator):
	try:
    	return validator(value)
    except Exception as e:
    	raise ValueError('Invalid value: %s' % value) from e

def validator(value):
	if len(value) > 10:
    	raise ValueError("Value can't exceed 10 characters")

这样调用 validate 函数

validate(False, validator)`

会抛出异常,输出以下信息:

Traceback (most recent call last):
...
TypeError: object of type 'bool' has no len()
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
...
ValueError: invalid value: False

最新的异常会被抛出(validate 函数里主动抛出的 ValueError 异常),这个异常里包含 __context__ 属性(其中包含执行 validator(value)时的 TypeError 异常 )。

else

如果一段代码需要在没有出现任何异常的情况才执行,使用 else 关键字。

import logging

def count_lines(filename):
    try:
        file = open(filename, 'r')
    except TypeError as e:
        logging.error(e)
        return 0
    except EnvironmentError as e:
        logging.error(e.args[1])
        return 0
    else:
        return len(file.readlines())

finally

如果不论是否产生了异常,都要执行某段代码,用 finally 关键字

import logging

def count_lines(filename):
	file = None
    try:
        file = open(filename, 'r')
        lines = file.readlines()
    except TypeError as e:
        logging.error(e)
        return 0
    except EnvironmentError as e:
        logging.error(e.args[1])
        return 0
    except UnicodeDecodeError as e:
        logging.error(e)
        return 0
    else:
        return len(file.readlines())
    finally:
        if file:
            file.close()

如果没有异常出现,会走 else 分支的逻辑,也会走 finally 的逻辑,这个函数的返回值是 else 分支的 return 语句返回的内容。(else 分支先于 finally 执行,并且 else 分支里有 return,但是 finally 分支还是会执行。)如果在 finally 分支里返回,那么函数的返回值就是 finally 分支里返回的内容。

类似的,如果在一个循环里使用了 finally,那么即使在执行这次循环里有 continue 或者 break 中断了这一次或中断整个循环,在这一次循环内的 finally 块的代码还是会执行。

for i in range(3):
    try:
        a = {}
        print(a['hello'])
    except:
        print('continue')
        continue
    finally:
        print('finally')

这段代码的输出是

continue
finally
continue
finally
continue
finally

with

可能有时候想达到这样的效果:有异常就往外抛,不去捕获,但是最后有个 finally 分支去做一些必要的清理工作。比如这样

def count_lines(filename):
    file = open(filename, 'r')
    try:
        return len(file.readlines())
    finally:
        file.close()

如果函数的第一行打开文件就出现了异常,那就抛出异常,下面的代码就不执行了。如果文件正常打开,在 try 块里的代码不论出现什么样的异常都会正常地抛出,最终都会在 finally 块里执行 file.close() 关闭文件。用异常处理来做这样的事,有点“浪费”。更好的方法是用 with 关键字。

def count_lines(filename):
    with open(filename, 'r') as file:
        return len(file.readlines())

with 这一块代码中,有异常就会抛出。对于 open 打开文件的操作,出了 with 这一块,会自动关闭打开的文件。效果跟前一段代码一样,代码变漂亮不少。