目录
1)上下文管理协议(Context Manager Protocol)
Python 程序中最常见的错误为语法错误。语法错误又称解析错误,是指开发人员编写了不符合 Python 语法格式的代码所引起的错误。含有语法错误的程序无法被解释器解释,必须经过修正后程序才能正常运行。
- while True
- print("语法格式错误")
上述示例代码中的循环语句后少了冒号(:),不符合 Python 的语句格式。因此,语法分析器会检测到错误。
在 PyCharm 中运行上述代码后,错误信息会在结果输出区进行显示,具体如下:
以上错误信息中包含了错误所在的行号、错误类型和具体信息,错误信息中使用的小箭头(^)指出语法错误的具体位置,方便开发人员快速地定位并进行修正。产生语法错误时引发的异常类型为 SyntaxError。
一段语法格式正确的 Python 代码运行后产生的错误时逻辑错误。逻辑错误可能时由于外界条件(例如:网络断开、文件格式损坏等)引起的,也有可能时程序本身设计不严谨导致的。例如:
- for i in 3:
- print(i)
运行结果:
以上示例代码没有然后语法格式错误,但执行后仍然出现 TypeError(类型错误) 异常,这是因为代码中使用 for 循环遍历整数 3,而 for 循环不支持遍历整型数据。
无论是哪种错误,都会导致程序无法正常运行。我们将程序运行期间检测到的错误称为异常。如果异常不被处理,程序默认的处理方式是直接崩溃。
Python 中所有的异常均由类实现,所有的异常类都继承自基类 BaseException。BaseException 类中包含 4 个子类,其中子类 Exception 是大多数常见异常类(如:SyntaxError、ZeroDivisionError等)的父类。
类名 | 描述 |
---|---|
SyntaxError | 发生语法错误时引发 |
FileNotFoundError | 未找到指定文件或目录时引发 |
NameError | 找不到指定名称的变量时引发 |
ZeroDivisionError | 除数为 0 时引发 |
IndexError | 当使用超出列表范围的索引时引发 |
KeyError | 当使用字典不存在的键时引发 |
AttributeError | 当尝试访问未知对象属性时引发 |
TypeError | 当试图在使用 a 类型的场合使用 b 类型时引发 |
try...except 语句用于捕获程序运行时的异常,其语法格式如下:
- try:
- 可能会出现错误的代码
- ...
- except 异常类型:
- 错误处理语句
- ...
上述格式中,try 子句后面是可能出错的代码,except 子句后面是捕获的异常类型及捕获到异常后的处理语句。
try...except 语句的执行过程如下:
(1) 先执行 try 子句,即 try 与 except 之间的代码。
(2) 若 try 子句中没有产生异常,则忽略 except 子句中的代码
(3) 若 try 子句产生异常,则忽略 try 子句的剩余代码,执行 except 子句的代码。
使用 try...except 语句捕获程序运行时的异常。例如:
- try:
- for i in 2:
- print(i)
-
- except:
- print('int 类型不支持迭代操作。')
上述代码对整数进行迭代操作,由于整数不支持迭代操作,因此上述代码在执行过程中必定会产生异常。运行上述代码程序并不会崩溃,这是因为 except 语句捕获到程序中的异常,并告诉 Python 解释器如何处理该异常 —— 忽略异常之后的代码,执行 except 语句后的异常处理代码。
程序运行结果 :
try...except 语句可以捕获和处理程序运行时的单个异常、多个异常、所有异常,也可以在except 子句中使用关键字 as 获取系统反馈的异常的具体信息。
使用 try...except 语句捕获和处理单个异常时,需要在 except 子句的后面指定具体的异常类型。例如:
- try:
- for i in 2:
- print(i)
- except TypeError as e:
- print(f'异常原因:{e}')
以上代码的 try 子句中使用 for 循环遍历了整数 2,导致程序捕获到 TypeError 异常,转而执行 except 子句的代码。因为 except 子句指定处理异常 TypeError,且获取了异常信息 e,所以程序会执行 except 子句中的输出语句,而不会出现崩溃。
程序运行结果:
'int' object is not iterable ---- “int” 对象不可迭代
注意:如果指定的异常与程序产生的异常不一致,程序运行时仍会崩溃。
一段代码中可能会产生多个异常,此时可以将多个具体的异常类组成元组放在 except 语句后处理,也可以联合使用多个 except 语句。例如:
- try:
- print(count)
- demo_list = ["Python", "Java", "C", "C++"]
- print(demo_list[5])
- except (NameError, IndexError) as error:
- print(f"异常原因:{error}")
上述代码首先在 try 子句中使用 print() 输出一个没有定义过的变量(count),这回引发 NameError 异常;然后又使用 print() 访问列表 demo_list 中的第 5 个元素,而 demo_list 中只有 4 个元素,这会产生异常 IndexError。
程序运行结果:
name 'count' is not defined ---- 未定义名称“count”
在处理多个异常时,还可以将 except 子句拆开,每个 except 子句对应一种异常。将上述代码修改为多个 except 子句。代码如下:
- try:
- print(count)
- demo_list = ["Python", "Java", "C", "C++"]
- print(demo_list[5])
- except NameError as error:
- print(f"异常原因:{error}")
- except IndexError as error:
- print(f"异常原因:{error}")
程序运行结果:
上述两段代码因为执行到 print(count) 时就被捕获异常,所以后续代码还没运行,没出现报错情况。但事实上 print(demo_list[5]) 语句也是错误语句。
在 Python 中,使用 try...except 语句捕获所有异常有两种方式:指定异常类为 Exception 类和省略异常类。
(1)指定异常类为 Exception 类。在 except 子句的后面指定具体的异常类为 Exception,由于 Exception 类是常见异常类的父类,因此它可以指代所有常见的异常。例如:
- try:
- print(count)
- demo_list = ["Python", "Java", "C", "C++"]
- print(demo_list[5])
- except Exception as error:
- print(f"异常原因:{error}")
上述示例的 try 子句中首先访问了未声明的变量 count,然后创建了一个包含 4 个元素的数组 demo_list,并访问该数组中索引为 5 的元素,导致程序可捕获到 NameError 和 IndexError,转而执行 except 子句的代码。因为 except 子句指定了处理异常类 Exception,而 IndexError 类是 Exception 的子类,所以程序会执行 except 子句中的输出语句,而不会出现崩溃。
程序运行结果:
(2)省略异常类。在 except 子句的后面省略异常类,表明处理所有捕获到的异常,示例如下:
- try:
- print(count)
- demo_list = ["Python", "Java", "C", "C++"]
- print(demo_list[5])
- except:
- print("程序出现异常,原因未知")
程序运行结果:
虽然使用省略异常类的方式也能捕获所有常见的异常,但这种方式不能获取异常的具体信息。
异常处理的主要目的是防止因外部环境的变化导致程序产生无法控制的错误,而不是处理程序的设计错误。因此,将所有的代码都用 try 子句包含起来的做法是不推荐的,try 子句应尽量只包含可能产生异常的代码。Python 中 try...except 语句害可以与 else 子句联合使用,该子句放在 except 语句之后,当 try 子句没有出现错误时应执行 else 语句中的代码。其格式如下:
- try:
- 可能出错的语句
- ...
- except:
- 出错后的执行语句
- else:
- 未出错时的执行语句
例如,某程序的分页显示数据功能可以根据用户输入控制每页显示多少条数据,但要求用户输入的数据为整形数据,如果输入的数据符合输入要求,每页显示用户指定条数的数据;如果输入的数据不符合要求,则显示默认条数的数据。例如:
- num = input("请输入每页显示多少条数据:") # 用户输入为字符串
- try:
- page_size = int(num) # 将字符串转化为数字
- except Exception as e:
- page_size = 20 # 若转换出错,则使用预设的数据量
- print(f"当前页显示{page_size}条数据")
- else:
- print(f"当前页显示{num}条数据") # 加载数据
如果用户输入的数据符合要求,结果如下:
如果用户输入的数据不符合要求,结果如下:
由以上两次输出结果可知,若用户输入的数据符合要求,程序未产生任何异常,并执行 else 子句中的代码;若用户输入的数据不符合要求,程序产生异常,并执行 except 子句中的代码。
finally 子句与 try...except 语句连用时,无论 try...except 是否捕获到异常,finally 子句后的代码都要执行,其语法格式如下:
- try:
- 可能出现错误的语句
- ...
- except:
- 出错后的执行语句
- ...
- finally:
- 无论是否出错都会执行的语句
- ...
Python 在处理文件时,为避免打开的文件作用过多的系统资源,在完成对文件的操作后需要使用 close() 方法关闭文件。为了确保文件一定会被关闭,可以将文件关闭操作放在 finally 子句中。
例如:
注意:运行该代码时要先创建一个 '异常.txt' 文件,否则 file.close 语句会报
- try:
- file = open('异常.txt', 'r')
- file.write('人生苦短,我用 Python')
- except Exception as error:
- print("写入文件失败", error)
- finally:
- file.close()
- print('文件已关闭')
若以上示例中没有 finally 语句,以上程序会因出现 Unsupported Operation 异常而无法保证打开的文件会被关闭;但使用 finally 语句后,无论程序是否崩溃, ”file.close()“ 语句一定被执行,文件必定会被关闭。
Python 程序中的异常不仅可以由系统抛出,还可以由开发人员使用关键字 raise 主动抛出。只要异常没有被处理,异常就会向上传递,直至最顶级也未处理,则会使用系统默认的方式处理(程序崩溃)。另外,程序开发阶段还可以使用 assert 语句检测一个表达式是否符合要求,不符合要求则抛出异常。接下来,本节将针对 raise 语句、异常的传递、assert 断言语句进行介绍。
raise 语句用于引发特定的异常,其使用方式大致可分为 3 种:
(1)由异常类名引发异常。
(2)由异常对象引发异常。
(3)由程序中出现过的异常引发异常
在 raise 语句后添加具体的异常类,使用类名引发异常,语法格式如下:
raise 异常类名
当 raise 语句指定了异常的类名时,Python 解释器会自动创建该异常类的对象,进而引发异常。
例如:
raise NameError
程序运行结果:
使用异常对象引发相应异常,其语法格式如下:
raise 异常对象
例如:
- name_error = NameError()
- raise name_error
上述代码创建了一个 NameError 类的对象 name_error,然后使用 raise 语句通过对象 name_error 引发异常 NameError。
程序运行结果:
仅使用 raise 关键字可重新引发刚才发生的异常,其语法格式如下:
raise
例如:
- try:
- num
- except NameError as e:
- raise
上述代码中,try 子句中声明了未赋值的变量num,导致程序会捕获到 NameError 异常,转而执行 except 子句的代码。由于 except 子句指定处理异常 NameError,因此程序会执行 except 子句中的代码,再次使用 raise 语句引发刚才捕获到的异常 NameError。
程序运行结果:
如果程序中的异常没有被处理,默认情况下会将该异常传递给上一级,如果上一级仍然没有处理,会继续向上传递,直至异常被处理或程序崩溃。
下面通过一个计算正方形面积的示例演示异常的传递,该示例中共包含 3 个函数:get_width()、calc_area()、show_area(),其中 get_width() 函数用于计算正方形边长,calc_area() 函数用于计算正方形面积,show_area() 函数用于展示计算的正方形面积,具体代码如下:
- def get_width(): # 计算边长
- print("get_width 开始执行")
- num = int(input("请输入除数:"))
- width_len = 10/num # 发生异常
- print("get_width 执行结束")
- return width_len
-
-
- def calc_area(): # 计算正方形面积
- print("calc_area 开始执行")
- width_len = get_width()
- print("calc_area 执行结束")
- return width_len * width_len
-
-
- def show_area(): # 数据展示
- try:
- print("show_area 开始执行")
- area_val = calc_area()
- print(f"正方形的面积是:{area_val}")
- print("show_area 执行结束")
- except ZeroDivisionError as e:
- print(f"捕捉到异常:{e}")
-
-
- if __name__ == '__main__':
- show_area()
上述代码中的函数 show_area() 为程序入口,该函数调用函数 calc_area(),函数 calc_area() 调用函数 get_width()。
get_width() 函数使用变量 num 接收用户输入的除数,通过语句 width_len = 10/num 计算正方形的边长,如果用户输入的 num 值为 0,程序会引发 ZeroDivisionError 异常。因为 get_width() 函数中并没有捕获异常的语句,所以 get_width() 函数中的异常向上传递到 calc_area() 函数,而 calc_area() 函数中也没有捕获异常信息的语句,只能将异常信息继续向上传递给 show_area() 函数。
show_area() 函数中设置了异常捕获语句 try...except,当它接收到由 calc_area() 函数传递来的异常后,会通过 try...except 捕获到异常信息。
运行程序,根据提示输入 0,结果如下:
assert 断言语句用于判断一个表达式是否为真,如果表达式为 True,不做任何操作,否则引发 AssertionError 异常。
assert 断言语句格式如下:
assert 表达式[,参数]
在以上格式中,表达式时 assert 语句的判定对象,参数通常是一个自定义的描述异常具体信息的字符串。
例如,一个会员管理系统要求会员的年龄必须大于 18 岁,则可以对年龄进行断言,代码如下:
- age = 17
- assert age >= 18, "年龄必须大于 18 岁"
以上示例中的 age >= 18 就是 assert 语句要断言的表达式,“年龄必须大于 18 岁”是断言的异常参数。程序运行时,由于 age = 17,断言表达式的值为 False,所以系统抛出了 AssertionError 异常,并在异常后显示了自定义的异常信息。
程序运行结果:
assert 断言语句多用于程序开发测试阶段,其主要目的是确保代码的正确性。如果开发人员能确保程序正确执行,那么不建议再使用 assert 语句抛出异常。
Python 中定义了大量的异常类,虽然这些异常类可以描述编程时出现的绝大部分情况,但仍难以涵盖所有可能出现的异常。Python 允许程序开发人员自定义异常。自定义的异常类方法很简单,只需创建一个类,让它继承 Exception 类或其他异常类即可。
定义一个继承自异常类 Exception 的类 CustomError。例如:
- class CustomError(Exception):
- pass # pass 表示空语句,是为了保证程序结构的完整性
接下来,演示自定义异常类 CustomError 的用法。例如:
- try:
- pass
- raise CustomError("出现错误")
- except CustomError as error:
- print(error)
上述代码在 try 语句中通过 raise 语句引发自定义异常类,同时还为异常指定提示信息。
自定义的异常类与普通类一样,也可以包含属性和方法,但一般情况下不添加或者只为其添加几个用于描述异常的详细信息的属性即可。
定义一个检测用户上传图片格式的异常类 FileTypeError,在 FileTypeError 类的构造方法中调用父类的 __init__() 方法并将异常信息作为参数,代码如下:
- class FileTypeError(Exception):
- def __init__(self, err="仅支持 jpg/png/bmp 格式"):
- super().__init__(err)
-
-
- file_name = input("请输入上传图片的名称(包含格式):")
- try:
- if file_name.split(".")[1] in ["jpg", "png", "bmp"]:
- print("上传成功")
- else:
- raise FileTypeError()
- except Exception as error:
- print(error)
上述代码中,首先定义了一个继承自 Exception 类的 FileTypeError 类,然后根据用户输入的文件信息,检测上传的图片是否符合要求。如果符合图片格式要求,则输出“上传成功”提示,否则使用 raise 语句抛出 FileTypeError。由于在使用 raise 语句抛出 FileTypeError 异常类时未传入任何参数,因此程序在捕获到 FileTypeError 异常后,会返回默认的异常详细信息“仅支持 jpg/png/bmp 格式”提示用户。
运行程序,输入符合图片格式要求的文件名,结果如下:
运行程序,输入不符合图片格式要求的文件名,结果如下:
使用 finally 子句虽然能处理关闭文件的操作,但这种方式过于繁琐,每次都需要编写调用close() 方法的代码。因此,Python 引入了 with 语句替代 finally 子句中调用 close() 方法释放资源的操作。with 语句支持创建资源、抛出异常、释放资源等操作,并且可以化简代码。本节将对 with语句的使用、上下文管理、自定义上下文管理进行介绍。
with 语句适用于对资源进行访问的场合,无论资源在使用过程中是否发生异常,都可以使用with 语句保证执行释放资源操作。
with语句的语法格式如下:
- with 上下文表达式 [as 资源对象]:
- 语句体
以上语法中的上下文表达式返回一个上下文管理器对象,如果指定了 as 子句,将上下文管理器对象的 __enter__() 方法的返回值赋值给资源对象。资源对象可以时单个变量,也可以是元组。
使用 with 语句操作文件对象的示例如下:
- with open('with_sence.txt') as file:
- for aline in file:
- print(aline)
上述代码使用 with 语句打开文件 with_sence.txt,如果文件能够顺利打开则会将文件对象赋值给 file 对象,然后通过 for 循环对 file 进行遍历输出,当对文件遍历之后,with 语句会关闭文件;如果文件不能顺利打开,with 语句也会将文件 with_sence.txt 关闭。
注意:不是所有对象都可以使用 with 语句,只有支持上下文管理协议的对象才可以使用,目前支持该协议的对象如下:file、decimal.Context、thread.LockType、threading.BoundedSemaphore、threading.Condition、threading.Lock、threading.RLock、threading.Semaphore。
with 语句之所以能够自动关闭资源,是因为它使用了一种名为上下文管理的技术管理资源。下面对上下文管理器的知识进行介绍。
上下文管理协议包括 __enter__() 和 __exit__() 方法,支持该协议的对象均需要实现这两个方法。__enter__() 和 __exit__() 方法的含义与用途如下:
(1)__enter__(self): 进入上下文管理器时调用此方法,它的返回值被放入 with...as 语句的 as 说明符指定的变量中。
(2)__exit__(self, type, value, traceback): 离开上下文管理器时调用此方法。在 __exit__() 方法中,参数 type, value, traceback 的含义分别为异常的类型、异常值、异常回溯追踪。如果 __exit__() 方法内部引发异常,该异常会覆盖掉其执行体中引发的异常。处理异常时不需要重新抛出异常,只需要返回 False。
支持上下文管理协议的对象就是上下文管理器,这种对象实现了 __enter__() 和 __exit__() 方法。通过 with 语句即可调用上下文管理器,它负责建立运行时的上下文。
with 语句中关键字 with 之后的表达式返回一个支持上下文管理协议的对象,也就是返回一个上下文管理器。
由上下文管理器创建,通过上下文管理器的 __enter__() 和 __exit__() 方法实现。__enter__() 方法在语句体执行之前执行,__exit__() 方法在语句体执行之后执行。
在开发中可以根据实际情况设计自定义上下文管理器,只需要让定义的类支持上下文管理协议,并实现 __enter__() 与 __exit__() 方法即可。
接下来,构建自定义的上下文管理器,代码如下:
- class OpenOperation:
- def __init__(self, path, mode):
- # 记录要操作的文件路径和模式
- self.__path = path
- self.__mode = mode
-
- def __enter__(self):
- print('代码执行到__enter__')
- self.__handle = open(self.__path, self.__mode)
- return self.__handle
-
- def __exit__(self, exc_type, exc_val, exc_tb):
- print("代码执行到__exit__")
- self.__handle.close()
-
-
- with OpenOperation('自定义上下文管理.txt', 'a+') as file:
- # 创建写入文件
- file.write("Custom Context Manage")
- print("文件写入成功")
上述代码自定义了上下文管理器 OpenOperation 类,在该类的 __enter__() 方法中打开文件, __exit__() 方法中关闭文件。
程序运行结果:
从输出结果中可以看出,使用 with 语句生成上下文管理器之后,程序先调用了 __enter__() 方法,其次执行该方法中的语句体,然后执行 with 语句块中的代码,最后在文件写入完成之后执行 __exit__() 方法关闭资源。