Python 异常处理
什么是异常
本文将介绍异常处理及调试的重要性。首先,我们来讨论异常处理。异常与错误有所不同。在编写代码时,我们经常会遇到各种错误,例如语法错误,包括缺少冒号、符号错误或拼写错误。如下图所示:
这些错误会导致代码无法通过编译,从而导致程序报错无法继续执行,这是一类错误。另一类错误是逻辑错误,即使编译通过,但代码的逻辑不正确。如下图所示:
例如,判断年龄是否大于18时,使用了大于号而非大于等于号,这是逻辑错误。
与错误不同,异常指的是错误出现时可以在正常的控制流程之外采取的行为。如下图所示:
接下来,我们将通过编写代码来说明异常处理的意义。我们新建创建一个名为“exception”的Python文件。让用户输入年龄,并判断是否成年,代码如下:
# 从用户输入获取年龄,并将其转换为整数类型
age = int(input("请输入年龄:"))
# 判断年龄是否大于等于18
if age >= 18:
# 如果大于等于18,输出成年信息
print("你已成年")
else:
# 如果小于18,输出未成年信息
print("你未成年")
# 输出程序结束信息
print("程序结束")
然而,用户可能不按常规输入数字,而是输入了其他字符,如“ABC”。这时程序会报错,错误类型为ValueError
,表示无法将字符串转换为整数。错误截图如下:
尽管程序在这种情况下无法正常运行,但这并非语法错误,而是一种异常,因为我们未能考虑到用户可能输入非数字的情况。类似地,当我们尝试访问列表或字典中不存在的元素或键时,代码如下:
# 创建一个包含整数的列表
list_value = [1, 2, 3]
# 尝试访问列表中索引为3的元素(索引从0开始)
print(list_value[3])
# 创建一个包含键值对的字典
dict_value = {'name': 'andy', 'age': 18}
# 尝试访问字典中键名为'gender'的键值
print(dict_value['gender'])
也会引发异常,如IndexError
和KeyError
。在Python中,还有许多其他类型的异常,如TypeError
和IOError
等。

因此,学习异常处理的目的在于处理这些错误,使程序能够在各种情况下正常运行。
try-except语句
本节将介绍如何使用try except语句来处理异常。try except结构类似于if else语句,其原理是程序首先进入try语句块执行正常代码,如果没有遇到异常,则跳过except继续执行主程序。如下图所示:
然而,如果在try语句块中执行正常代码时遇到异常,程序将跳转到except语句块执行异常处理代码,待处理完毕后再继续执行主程序。如下图所示:
接下来,我们将通过代码演示这一过程。我们需要创建一个新的Python文件,命名为try_except,并将前面的代码复制进去。我们需要检测用户输入的信息是否正确,如果不正确会引发异常。现在,我们将使用try except来修改代码。代码如下:
# 使用try except语句来处理异常
try:
# 尝试从用户输入获取年龄,并将其转换为整数类型
age = int(input("请输入年龄:"))
# 判断年龄是否大于等于18
if age >= 18:
# 如果大于等于18,输出成年信息
print("你已成年")
else:
# 如果小于18,输出未成年信息
print("你未成年")
except :
print("输入不合法")
# 输出程序结束信息
print("程序结束")
在运行程序时,先输入一个正常的年龄值18,此时输出"已成年";然后输入非数字字符"abc",程序不再报错,而是输出"输入不合法"。显然,这样的提示信息对用户更加友好。此外,我们还测试了程序在执行except语句后是否继续往下执行,结果是程序确实继续执行了。
接下来,我们对比了未使用异常处理的情况。在未使用异常处理时,程序会在输入非数字时报错并终止执行,而没有输出后面的语句。如下图所示:
此时,错误信息为ValueError,属于异常类型之一。
在处理异常时,我们还可以指定异常类型。通过添加一个异常类型,我们可以更精确地捕获特定类型的异常。代码如下:
# 使用try except语句来处理异常
try:
# 尝试从用户输入获取年龄,并将其转换为整数类型
age = int(input("请输入年龄:"))
# 判断年龄是否大于等于18
if age >= 18:
# 如 果大于等于18,输出成年信息
print("你已成年")
else:
# 如果小于18,输出未成年信息
print("你未成年")
except Exception:
print("输入不合法")
print(Exception)
输出结果:
请输入年龄:abc
输入不合法
<class 'Exception'>
在Python中,异常类是一种类,用于表示各种不同类型的异常情况。要查看Python中的异常类,我们可以查阅Python标准库。在标准库中,异常类被分成了不同的分类,包括基类、具体异常和异常层次结构。如下图所示:
基类通常指的是BaseException
或者称之为父类,而具体异常则是它的子类。这些子类中包含了各种不同类型的异常。
例如,我们遇到的ValueError
是其中之一,表示数值错误。除了ValueError
之外,还有许多其他异常类,如ImportError
(导入错误)、NameError
(名字错误)等。在处理异常时,我们可以直接指定要捕获的异常类的名称,如except ValueError
,并使用as
来为异常指定一个缩写,如as error
。代码如下:
# 使用try except语句 来处理异常
try:
# 尝试从用户输入获取年龄,并将其转换为整数类型
age = int(input("请输入年龄:"))
# 判断年龄是否大于等于18
if age >= 18:
# 如果大于等于18,输出成年信息
print("你已成年")
else:
# 如果小于18,输出未成年信息
print("你未成年")
except ValueError as error:
# 如果用户输入的内容无法转换为整数,捕获ValueError异常
print("输入不合法")
# 打印异常信息
print(error)
# 输出程序结束信息
print("程序结束")
输出结果:
请输入年龄:abc
输入不合法
invalid literal for int() with base 10: 'abc'
程序结束
这样做可以输出更详细的错误信息,有助于排查和解决问题。因此,使用try except语句处理异常是一种常见的方式,它可以帮助我们更好地管理代码中可能出现的各种异常情况,提高程序的健壮性和可靠性。
try-except-else语句
本节将介绍try except else语句。与前一节介绍的try except相比,try except else多了一个else部分。我们先来了解其运行原理。程序首先从try语句开始执行,然后执行其中的正常代码。如果没有遇到异常,程序将跳过except部分,进入else并执行其中的代码。如下所示:
接下来,我们将在代码中演示如何使用try except else。我们创建一个新的Python文件,命名为try_except_else,并复制上一节的代码进。代码如下:
# 使用try except语句来处理异常
try:
# 尝试从用户输入获取年龄,并将其转换为整数类型
age = int(input("请输入年龄:"))
# 判断年龄是否大于等于18
if age >= 18:
# 如果大于等于18,输出成年信息
print("你已成年")
else:
# 如果小于18,输出未成年信息
print("你未成年")
except ValueError as error:
# 如果用户输入的内容无法转换为整数,捕获ValueError异常
print("输入不合法")
# 打印异常信息
print(error)
# 输出程序结束信息
print("程序结束")
在这段代码中,我们主要使用try语句来检测用户输入是否合法。然而,将if else判断语句放在此处并不太合理,特别是当代码量较大时。这种情况下,可以使用else语句,将其放在try语句的外部。修改代码如下:
# 尝试从用户输入获取年龄,并将其转换为整数类型
try:
age = int(input("请输入年龄:"))
# 捕获ValueError异常,即用户输入的内容无法转换为整数
except ValueError as error:
# 输出输入不合法的提示信息
print("输入不合法")
else:
# 如果用户输入的内容可以转换为整数,则判断年龄是否大于等于18
if age >= 18:
# 如果大于等于18,输出成年信息
print("你已成年")
else:
# 如果小于18,输出未成年信息
print("你未成年")
# 输出程序结束信息
print("程序结束")
在这种结构下,程序先执行try部分,检查用户输入是否合法。如果不合法,则执行except部分,输出"输入不合法"并打印错误信息。这个错误信息通常是给开发人员查看的,我们可以将其删除。如果输入合法,则执行else部分,继续判断年龄是否成年或未成年。最后,程序跳出整个try except else结构,执行下面的打印语句"程序结束"。
现在让我们来运行一下。首先输入一个合法的年龄值18,程序输出"你已成年"。这表明程序在此处检测到合法输入后,直接进入else部分判断年龄。执行完else后,程序跳出整个结构,执行打印语句"程序结束"。接着,我们输入一个不合法的值"abc",程序在此处检测到异常,执行except部分,并输出"输入不合法"。需要注意的是,此时程序不再执行else部分,而是跳出当前的try except else结构,继续执行下面的打印语句。
有些人可能会想,如果我不使用else,而是将if else语句放在try语句内部会怎样?代码如下图所示:
当输入"abc"时,程序报错并提示"age没有定义"。这是因为在此处程序已经发生了异常,而age没有被赋值,然后程序继续执行,导致"age未定义"的报错。因此,在这种情况下,我们通常会将if else语句放在try语句内部。当逻辑较多时,我们可以单独使用else,将其放在try语句的外部。这就是try except else语句的使用场景。
try-except-finally语句
本节介绍了另一种异常处理结构try except finally。在这种结构中,无论是否发生异常,都会执行finally下面的代码块。运行逻辑如下图:
当没有异常发生时,程序从try语句开始执行,然后执行其中的正常代码,最后跳转到finally部分执行其中的代码。如果发生异常,程序首先执行try语句中的正常代码,然后执行except部分处理异常,最后无论是否有异常都会执行finally下面的代码块。如下图所示:
finally语句的主要作用是确保资源的释放。例如,在操作文件时,通常的流程是打开文件、操作文件,然后关闭文件。如果在操作文件时发生异常,程序可能会中止,但此时文件仍然处于打开状态,会占用一定的内存资源。因此,可以使用finally来确保无论发生异常与否,都会在finally语句中关闭文件,避免资源的浪费。
在代码中,我们首先尝试打开一个名为test.txt的文本文件,向其中写入内容,并关闭文件。我们添加了异常处理,使用try来包裹文件操作的代码块,并在except部分处理异常情况,输出"操作异常"。关闭文件的操作放在了finally语句中,以确保无论是否发生异常,都会执行关闭文件的操作。代码如下:
# 尝试打开一个名为text.txt的文件,以写入方式打开
try:
file = open("text.txt", "w")
# 操作文件,向文件中写入内容
s = "hello world"
file.write(s)
# 捕获所有异常,打印"操作异常"
except:
print("操作异常")
finally:
# 无论是否发生异常,都会执行关闭文件的操作
file.close()
# 输出"关闭文件"
print("关闭文件")
我们运行代码,可以看到正常情况下文件操作成功,并且文件被关闭。然后我们故意使代码产生异常,例如写入一个不存在的变量s2,此时程序会输出"操作异常",但是在此之前finally部分已经执行了关闭文件的操作。如下图所示:
这证明了finally语句的作用,即不管是否发生异常,都会执行其中的代码块。
处理多个异常
本节将介绍处理多个异常的情况。所谓多个异常是指在执行一个代码段的过程中存在多个异常条件。在这种情况下,我们可以使用多个 except
来分别捕获异常。接下来,我们将在代码中看一下如何处理多个异常的情况。让我们新建一个文件,命名为 more_except.py
。我们仍然使用之前提到的年龄例子。代码如下:
age = int(input("请输入年龄:"))
x = 10 / age
在没有使用异常处理的情况下,运行代码会报错。例如,当我们输入非数字字符时会触发 ValueError
错误。接着,如果我们输入 0,则会触发 ZeroDivisionError
错误,因为不能将 10 除以 0。
在这种情况下,我们需要检测两处异常:首先,检查用户输入的年龄是否能够转换为整数;其次,检查年龄是否为 0,因为不能将 10 除以 0。因此,我们需要使用多个 except
来处理这两个异常。代码如下:
# 尝试执行以下代码块,捕获可能发生的异常
try:
# 用户输入年龄,并将其转换为整数类型
age = int(input("请输入年龄:"))
# 将 10 除以用户输入的年龄,将结果赋值给变量 x
x = 10 / age
# 捕获 ValueError 异常,即用户输入无法转换为整数
except ValueError:
print("请输入整数")
# 捕获 ZeroDivisionError 异常,即年龄为0时发生的错误
except ZeroDivisionError:
print("年龄不能为0")
# 如果没有异常发生,执行以下代码块
else:
# 打印年龄和计算结果
print(f"age is {age}")
print(f"x is {x}")
我们首先使用 try
将代码放入其中,然后开始检测异常。首先检测 ValueError
异常,并输出提示信息:“请输入整数”。异常检测完成后,我们还需要检测第二个异常,即 ZeroDivisionError
。在 except
中再次检测这个异常,并输出提示信息:“年龄不能为0”。通过运行代码,我们可以看到当输入不合理的年龄时,例如字符或 0,会分别被两个 except
捕获,并输出相应的提示信息。
此外,我们还可以在一个 except
语句中写入多个异常,使用元组将它们包裹起来。这样可以将多个异常合并在一起,编写起来更加简洁。不过需要注意的是,这里使用的是一个元组,元组内是每一个异常元素。代码如下:
# 尝试执行以下代码块,捕获可能发生的异常
try:
# 用户输入年龄,并将其转换为整数类型
age = int(input("请输入年龄:"))
# 将 10 除以用户输入的年龄,将结果赋值给变量 x
x = 10 / age
# 捕获可能的异常,包括 ValueError(输入无法转换为整数)和 ZeroDivisionError(除以0)
except (ValueError, ZeroDivisionError):
print("请输入合理的年龄")
# 如果没有异常发生,执行以下代码块
else:
# 打印年龄和计算结果
print(f"age is {age}")
print(f"x is {x}")
raise主动抛出异常
在前面的内容中,我们已经介绍了几种使用异常处理的方式,包括 try-except
、try-except-finally
等。这些异常处理方式都有一个共同的特点,就是只有在 try
语句中遇到异常时才会触发异常处理。然而,有些人可能会觉得这种方式过于被动,希望能够在必要时主动抛出异常。那么,是否可以主动抛出异常呢?答案是肯定的,我们可以使用 raise
关键字在任何时候、任何地点主动抛出异常。
下面我们来看一下如何使用 raise
。我们先新建一个文件命名为 raise.py
。现在有一个需求,如果用户输入的年龄小于18岁,也就是未成年,那么我们就希望抛出一个异常,提示用户他还未成年,禁止执行某些操作。代码如下:
# 获取用户输入的年龄并将其转换为整数类型
age = int(input("请输入年龄:"))
# 如果年龄小于18,抛出 ValueError 异常,提示用户未成年
if age < 18:
raise ValueError("你还未成年")
# 如果没有抛出异常,打印"continue"
print("continue")
接下来我们运行代码。当我们输入年龄小于18时,程序会立即抛出异常,并终止运行后面的代码,输出相应的提示信息。注意到在抛出异常后,后面的代码(例如 print
)将不会执行。如下图所示:
除了使用通用的 Exception
外,我们还可以根据具体情况选择合适的异常类型。例如,如果我们希望提示用户输入的年龄不合法,我们可以选择 ValueError
异常类型。
通常情况下,我们会将需要判断的内容放到一个函数中。例如,我们创建一个名为 is_adult
的函数,接受一个年龄参数,然后判断该年龄是否大于等于18,如果小于18,则抛出异常。在实际调用该函数时,通常会将其放入 try-except
语句中,以便捕获可能抛出的异常,并进行处理。代码如下:
# 获取用户输入的年龄并将其转换为整数类型
age = int(input("请输入年龄:"))
# 定义一个函数,用于判断年龄是否成年
def is_adult(age):
# 如果年龄小于18,抛出 ValueError 异常,提示用户未成年
if age < 18:
raise ValueError("你还未成年")
try:
# 调用 is_adult 函数,并传入年龄参数
is_adult(age)
# 捕获 可能抛出的 ValueError 异常
except ValueError as e:
# 打印异常信息
print(e)
# 如果没有抛出异常,打印"continue"
print("continue")
需要注意的是,虽然 raise
函数提供了主动抛出异常的功能,但我们并不应该滥用异常。对于简单的顺序逻辑,通常使用 if-else
就足够了,不必使用异常。在开发过程中,应该根据具体情况慎重选择是否使用异常。
自定义异常类
在 Python 标准库中,内置了许多异常类,其中 BaseException
是顶级基类异常,而在 Exception
下面则有许多具体的异常类。如下图所示:
有时候我们会发现尽管内置的异常很多,但它们很难满足我们具体的需求。例如,在验证用户登录是否合法的情况下,我们想要判断用户输入的用户名是否为空、密码是否为空,以及用户名和密码是否匹配等情况。对于这些具体的需求,内置的异常类可能无法满足我们的需求,或者我们希望让异常更加详细可读,增加一些异常类型的其他功能。为此,我们可以根据自己的需求来自定义异常类型,这就是我们本节要介绍的自定义异常。
下面我们通过代码来学习一下如何自定义异常。我们首先创建一个 Python 文件,命名为 define_exception.py
。在之前我们都是使用内置的异常 类,但现在我们需要定义一个自己的异常。我们将使用类的形式来定义,假设我们的异常类叫做 ValidateError
,代表一个验证异常。这个类需要继承自 Python 标准库中的某些异常类,这里我们选择继承自 ValueError
类,因为它是 Python 内置的异常类,无需导入。代码如下:
# 定义一个自定义异常类 ValidateError,继承自 ValueError
class ValidateError(ValueError):
# 初始化方法,接受一个参数 message,并传递给父类的初始化方法
def __init__(self, message, *args, **kwargs):
super().__init__(message, *args, **kwargs)
在这个类下面,我们首先定义一个初始化方法,接受一个参数 message
,然后再添加几个可选的参数 *args
和 **kwargs
。接着,我们直接调用 ValueError
父类的初始化方法。要调用父类的初始化方法,我们可以使用 super()
函数,并直接调用它的 __init__
方法,传递我们刚才的 message
、args
和 kwargs
。现在我们定义的这个类就完成了,它包含了一个初始化方法,并调用了父类的初始化方法,即 ValueError
的初始化方法。
我们定义的 ValidateError
并没有实现特别的功能。而我们现在想要让这个异常更加具体一些,是一个验证异常。接下来我们开始创建一个函数 validate_account
来检查一个账户。我们需要传递两个参数,一个是 username
,另一个是 password
,即用户名和密码。我们首先判断用户名是否为空,如果为空,就使用 raise
主动抛出一个异常。异常类型就是我们刚才定义的 ValidateError
,错误信息是“用户名不能为空”。接着我们再来判断一下密码,如果密码为空,同样抛出一个异常“密码不能为空”。然后,我们再来判断用户名和密码是否匹配。通常情况下,我们会查询数据库,将用户输入的用户名和密码与数据库中的进行匹配。但由于我们目前还没有学习数据库相关的知识,因此我们简化了这个流程,直接判断用户名和密码是否与预设的一致。如果不匹配,我们同样抛出一个异常“用户名和密码不匹配”。代码如下:
# 定义一个自定义异常类 ValidateError,继承自 ValueError
class ValidateError(ValueError):
# 初始化方法,接受一个参数 message,并传递给父类的初始化方法
def __init__(self, message, *args, **kwargs):
super().__init__(message, *args, **kwargs)
# 定义一个函数 validate_account,用于验证用户名和密码是否合法
def validate_account(username, password):
# 如果用户名为空,抛出 ValidateError 异常
if username == '':
raise ValidateError('用户名不能为空')
# 如果密码为空,抛出 ValueError 异常
if password == '':
raise ValueError('密码不能为空')
# 如果用户名和密码不匹配,抛出 ValueError 异常
if username != 'andy' or password != '123456':
raise ValueError('用户名和密码不匹配')
# 获取用户输入的用户名和密码
username = input('请输入用户名:')
password = input('请输入密码:')
现在我们来验证一下,如果发生异常了,能否正常抛出。我们调用 validate_account
函数,传递一个用户名和密码。我们输入用户名为空的情况,可以看到程序成功捕获到了异常并输出了相应的错误信息。接着我们测试密码为空的情况,同样成功捕获到了异常。如果用户名和密码都输入正确,就不会抛出异常。
然而,由于异常会导致程序终止,为了让程序能够正常运行不中断,我们使用 try-except
来捕获这个异常。如果发生异常,异常类型是 ValueError
,我们将异常对象赋值给 e
,然后输出异常信息。否则,我们输出“用户名和密码正确”。代码如下:
# 定义一个自定义异常类 ValidateError,继承自 ValueError
class ValidateError(ValueError):
# 初始化方法,接受一个参数 message,并传递给父类的初始化方法
def __init__(self, message, *args, **kwargs):
super().__init__(message, *args, **kwargs)
# 定义一个函数 validate_account,用于验证用户名和密码是否合法
def validate_account(username, password):
# 如果用户名为空,抛出 ValidateError 异常
if username == '':
raise ValidateError('用户名不能为空')
# 如果密码为空,抛出 ValueError 异常
if password == '':
raise ValueError('密码不能为空')
# 如果用户名和密码不匹配,抛出 ValueError 异常
if username != 'andy' or password != '123456':
raise ValueError('用户名和密码不匹配')
# 获取用户输入的用户名和密码
username = input('请输入用户名:')
password = input('请输入密码:')
try:
# 调用 validate_account 函数,验证用户名和密码是否合法
validate_account(username, password)
except ValueError as e:
# 捕获可能抛出的 ValueError 异常,并打印异常信息
print(e)
else:
# 如果没有抛出异常,打印“用户名和密码正确”
print("用户名和密码正确")
这就是我们自定义的一个异常类 ValidateError
。这个类继承了 Python 标准库中的异常类 ValueError
。虽然我们可以选择继承其他的类,比如 Exception
,但在这个例子中,我们只是在初始化方法中调用了父类的初始化方法,没有做其他设置。因此,它并没有实际的功能,只是让异常更加精细了。通过自定义异常,我们可以根据具体的需求创建更加详细和具体的异常类型。