盘一盘 Python 系列特别篇 - 异常处理

建议阅读 40 分钟

在公众号对话框回复 EH

获取完整 Jupyter Notebook

0

引言

从上贴【错误类型】的内容我们知道,Python 在程序报错时会返回详细信息,如错误发生的行数和具体的错误类型。

首先需要明白的是,我们无法完全阻止错误发生,但是可以提前预防以至于程序不会崩溃。这个提前预防的动作称为异常处理(exception handling)。

总之异常处理就是为了防患于未然。

本帖的内容如下:

  1. try-except
  2. try-except-else
  3. try-except-else-finally
  4. 抛出 Exception
  5. 总结

1

Try-Except

异常处理最常见的语句就是 try-except 组合,细分又有三种类型:

  1. 知道错误但不确定类型,用 except Exception
  2. 知道错误而且确定类型,用 except some_exception
  3. 知道错误而且有多个错误
    • 用多个 except
    • 用 except (exc_1, exc_2, ... exc_n)

1.1

知道错误但不确定类型

范式

例子

def divide(a, b):
    try:
        c = a / b
        print(f"Result = {c:.4f}.")
    except:
        print('Divisor is zero and division is impossible!')
divide(10, 3)
Result = 3.3333.
divide(10, 0)
Divisor is zero and division is impossible!

在做除法时我们知道分母为零会报错,因此我们把 c = a/b 这行代码写在 try 语句下面。测试代码:

  • 10 除以 3 ,程序正常运行
  • 10 除以 0 ,异常被 except 语句下处理,输出有用的信息

1.2

知道错误而且确定类型

范式

例子

其实上面错误的具体类型我们是可以查出来的,输入 10/0,得到该错误是 ZeroDivisionError。

10/0
---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
<ipython-input-4-e574edb36883> in <module>
----> 1 10/0

ZeroDivisionError: division by zero

这样我们在用 except 语句处理异常时,可以在后面“显性”写出我们要处理的错误类型,即 ZeroDivisionError。

def divide(a, b):
    try:
        c = a / b
        print(f"Result = {c:.4f}.")
    except ZeroDivisionError:
        print('Divisor is zero and division is impossible!')
divide(10, 0)
Divisor is zero and division is impossible!

运行结果没问题。

但是在实际写代码中,你不知道会犯什么稀奇古怪的错误,如下代码第 4 行。变量 cc 在使用之前没有定义,报错。

def divide(a, b):
    try:
        c = a / b
        d = cc + 1
        print(f"Result = {c:.4f}.")
    except ZeroDivisionError:
        print('Divisor is zero and division is impossible!')
divide(10, 2)
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-8-15fa568ba405> in <module>
----> 1 divide(10, 2)

<ipython-input-7-97d83e2f78ac> in divide(a, b)
      2     try:
      3         c = a / b
----> 4 d = cc + 1
      5         print(f"Result = {c:.4f}.")
      6     except ZeroDivisionError:

NameError: name 'cc' is not defined

为了保险起见,我们在已经确定代码会出现 ZeroDivisionError 的情况下,将不确定的所有错误都放在 except Exception 后的语句中处理,即打印出 ‘Something wrong!’

def divide(a, b):
    try:
        c = a / b
        d = cc + 1
        print(f"Result = {c:.4f}.")
    except ZeroDivisionError:
        print('Divisor is zero and division is impossible!')
    except Exception:
        print('Something wrong!')
divide(10, 0)
Divisor is zero and division is impossible!
divide(10, 2)
Something wrong!

1.3

知道错误而且多个错误

第一种范式

例子

假设你预期代码会出现 ZeroDivisionError 和 NameError 的错误,你可以用多个 except 语句来实现。

def divide(a, b):
    try:
        c = a / b
        d = cc + 1
        print(f"Result = {c:.4f}.")
    except ZeroDivisionError:
        print('Divisor is zero and division is impossible!')
    except NameError:
        print('Some variable name is undefined!')
divide(10, 2)
Some variable name is undefined!
divide(10, 0)
Divisor is zero and division is impossible!

运行结果没问题。

第二种范式

此外,你还可以将多个 except 语句整合到第一个 except 语句中,范式如上。

两者几乎是等价的,下面我们换个例子来分析两者的区别。

多个 except 语句

下面函数将变量 a 转换成整数

  • 如果 a 是浮点型变量 1.3 或者字符型变量 '1031',程序运行正常。
  • 如果 a 是这种字符型变量 '1 mio',会报 ValueError 的错误。
  • 如果 a 是列表型变量 [1, 2],会报 TypeError 的错误(这对元组、字典、集合都适用)。
def convert_to_int(a):
    try:
        int_value = int(a)
        print('The converted integer is', int_value)
    except ValueError:
        print("'a' is not a numerical value or expression.")
    except TypeError:
        print("The type of 'a' is not compatiable.")

当程序正常运行而转换成整型变量的输出。

convert_to_int(1.3)
The converted integer is 1
convert_to_int('1031')
The converted integer is 1031

当程序报错但异常 ValueError 被处理时的输出。

convert_to_int('1 mio')
'a' is not a numerical value or expression.

当程序报错但异常 TypeError 被处理时的输出。

convert_to_int([1, 2])
The type of 'a' is not compatiable.

单个 except 语句

我们可以将多个 except 语句写到一个 except 语句中,两者等价关系如下:

except exc_1:

except exc_2:

...

except exc_n:

=

except (exc_1, exc_2, ... exc_n)

关键点就是要用小括号把每个特定异常“打包”起来。

def convert_to_int(a):
    try:
        int_value = int(a)
        print('The converted integer is', int_value)
    except (ValueError, TypeError):
        print("Error occurred. Either 'a' is a numerical value " \
              "or expression or type of 'a' is incompatible.")

结果运行如下。

convert_to_int('1 mio')
Error occurred. Either 'a' is a numerical value or expression or type of 'a' is incompatible.
convert_to_int([1, 2])
Error occurred. Either 'a' is a numerical value or expression or type of 'a' is incompatible.

哪种范式更好呢?

  • 如果要根据要处理的异常执行不同的代码,可以采用第一种范式,根据不同异常输出更明确的信息。
  • 如果要为所有要处理的异常执行同一段代码,可以采用第二种范式,因为它避免了多个 except 子句中的重复代码。

此外我们还可以给异常起别名(alias),用以下范式,别名可以任意取,一般用 e 或者 err 当做其名称。

except (exp_1, ... exp_n) as err

def convert_to_int(a):
    try:
        int_value = int(a)
        print('The converted integer is', int_value)
    except (ValueError, TypeError) as err:
        print('GOT ERROR WITH MESSAGE: {0}'.format(err.args[0]))
convert_to_int('1 mio')
GOT ERROR WITH MESSAGE: invalid literal for int() with base 10: '1 mio'
convert_to_int([1, 2])
GOT ERROR WITH MESSAGE: int() argument must be a string, a bytes-like object or a number, not 'list'

2

Try-Except-Else

上节讲了当异常被处理时运行 except 语句中的代码,如果没有异常出现,我们可以加一个 else 语句,而运行其下的代码。这时就是 try-except-else 组合。

范式

首先要明确的是,else 语句是可有可无的。如果存在,则 else 语句应始终在 except 语句之后。

  • 当 try 语句下的代码未发生异常时,才会执行 else 子句下的代码。
  • 当 try 语句下的代码中发生异常,则 except 语句将处理异常,else 语句将不会执行。

例子

def divide(a, b):
    try:
        c = a / b
    except ZeroDivisionError:
        print('Divisor is zero and division is impossible!')
    else:
        print(f"Result = {c:.4f}.")
divide(10, 2)
Result = 5.0000.
divide(10, 0)
Divisor is zero and division is impossible!

3

Try-Except-Else-Finally

假如不管异常是否出现我们都需要运行某些代码,我们可以加 finally 语句。这时就是 try-except-else-finally 组合。

范式

无论是否发生异常,finally 语句始终在 try 语句运行之前执行。

在实际应用中,finally 语句在程序跑完后用于释放资源、关闭文件或断开数据库连接等。

例子

def divide(a, b):
    try:
        c = a / b
    except ZeroDivisionError:
        print('Divisor is zero and division is impossible!')
    else:
        print(f"Result = {c:.4f}.")
    finally:
        print('Error or no error, FINALLY DONE!')

当异常出现的时候,没问题,异常被处理了,而且在 finally 语句下的信息也随后打出。

divide(10, 0)
Divisor is zero and division is impossible!
Error or no error, FINALLY DONE!

当程序正常运行的时候,没问题,结果打出,而且在 finally 语句下的信息也随后打出。

divide(10, 2)
Result = 5.0000.
Error or no error, FINALLY DONE!

当没有实现预测的异常出现的时候,程序报错,但是在 finally 语句下的信息还是会随后打出。

divide(10, '2')
Error or no error, FINALLY DONE!

---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-51-ae7e5f3b4399> in <module>
----> 1 divide(10, '2')

<ipython-input-48-ef792bab0247> in divide(a, b)
      1 def divide(a, b):
      2     try:
----> 3 c = a / b
      4     except ZeroDivisionError:
      5         print('Divisor is zero and division is impossible!')

TypeError: unsupported operand type(s) for /: 'int' and 'str'

这时为了处理“未预测”的异常,我们加一句 except Exception 即可。

def divide(a, b):
    try:
        c = a / b
    except ZeroDivisionError:
        print('Divisor is zero and division is impossible!')
    except Exception:
        print('Something wrong!')
    else:
        print(f"Result = {c:.4f}.")
    finally:
        print('Error or no error, FINALLY DONE!')
divide(10, '2')
Something wrong!
Error or no error, FINALLY DONE!

再看一个从电脑硬盘中读取文件(假设路径中有一个 Error.txt 的文件)的例子。

finish_task = False

try:
    inputFileName = input("输入要读取的文件名 (txt 格式): ")
    inputFileName = inputFileName + ".txt"
    inputFile = open(inputFileName, "r")
except IOError:
    print("\n文件", inputFileName, "不能被打开")
except Exception:
    print("\n有不明错误")
else:
    print("\n正在打开文件", inputFileName, "\n")
    finish_task = True

    for line in inputFile:
        print(line, end="")

    print("\n\n完成读取文件", inputFileName)
finally:
    if finish_task:
        inputFile.close()
        print("\n关闭文件", inputFileName)
    else:
        print("\n未能完成读取文件", inputFileName)

按 Enter 下面的空白框会跳出来。

如果输入一个错误的文件名,比如 asf。

输入要读取的文件名 (txt 格式): asf

文件 asf.txt 不能被打开

未能完成读取文件 asf.txt

如果输入一个正确的文件名,比如 Error。

输入要读取的文件名 (txt 格式): Error

正在打开文件 Error.txt

Errors or mistakes in a program are often referred to as bugs. They are almost always the fault of the programmer. The process of finding and eliminating errors is called debugging. Errors can be classified into three major groups:

I. Syntax errors
II. Runtime errors
III. Logical errors

完成读取文件 Error.txt

关闭文件 Error.txt

4

Raise Exception

除了上面处理异常的操作之外,我们还可以用 raise 关键词“抛出”异常:

  • 抛出 Python 里内置的异常
  • 抛出我们自定义的异常

抛出内置异常

在下例中,如果输入非整数,我们抛出一个 ValueError(注意这是 Python 里面内置的异常对象),顺带“This is not a positive number”的信息。

try:
    a = int(input("Enter a positive integer: "))
    if a <= 0:
        raise ValueError("That is not a positive number!")
except ValueError as err:
    print(err)

抛出自定义异常

在下例中,我们记录连续两天的组合价值

  • 如果昨天和今天的价值都小于零,我们抛出 ValueError 并带着 "Negative worth!" 的信息。
  • 如果组合增值小于零,我们也抛出 ValueError 并带着 "Negative return!" 的信息。

代码如下:

def portfolio_value(last_worth, current_worth):
    if last_worth < 0 or current_worth < 0:
        raise ValueError('Negative worth!')
    value = current_worth - last_worth
    if value < 0:
        raise ValueError('Negative return!')

两种情况的运行结果如下:

try:
    portfolio_value(-10000, 10001)
except ValueError as err:
    print(err)
Negative worth!
try:
    portfolio_value(10000, 9999)
except ValueError as err:
    print(err)
Negative return!

但是在第二种组合增值为负的情况下,严格来说不算是 ValueError,顶多算个警告,这时我们可以自定义一个 NegativePortfolioValueWarning 的异常。

在 Python 里,所有异常都是 Exception 的子类,因此在定义其类时需要

class Error(Exception):

class your_exception(Error):

具体代码如下。

class Error(Exception):
    pass

class NegativePortfolioValueWarning(Error):
    def __init__(self, expression, message):
        self.expression = expression
        self.message = message

细分 ValueError 和 NegativePortfolioValueWarning 。

def portfolio_value(last_worth, current_worth):
    if last_worth < 0 or current_worth < 0:
        raise ValueError('Negative worth!')
    value = current_worth - last_worth
    if value < 0:
        raise NegativePortfolioValueWarning(
        'NegativePortfolioValueWarning', \
        'Negative return. Take a look!')

两种情况的运行结果如下:

try:
    portfolio_value(-10000, 10001)
except ValueError as err:
    print(err)
except NegativePortfolioValueWarning as err:
    print('[', err.args[0], '] -', err.args[1])
Negative worth!
try:
    portfolio_value(10000, 9999)
except ValueError as err:
    print(err)
except NegativePortfolioValueWarning as err:
    print('[', err.args[0], '] -', err.args[1]
[ NegativePortfolioValueWarning ] - Negative return. Take a look!

5

总结

一图胜千言!

信息量太小?那这张呢?

Stay Tuned!

下一篇
举报

扫码关注云+社区

领取腾讯云代金券