异常处理机制

news/2024/12/29 23:48:08/

编程错误

编写程序时遇到的错误可大致分为 2 类,分别为语法错误和运行时错误。

语法错误

语法错误,也就是解析代码时出现的错误。当代码不符合Python语法规则时,Python解释器在解析时就会报出SyntaxError语法错误,与此同时还会明确指出最早探测到错误的语句。例如:

print "Hello,World!"

我们知道,Python 3已不再支持上面这种写法,所以在运行时,解释器会报如下错误:

SyntaxError: Missing parentheses in call to 'print'

运行时错误

运行时错误,即程序在语法上都是正确的,但在运行时发生了错误。例如:

a = 1/0

上面这句代码的意思是:用 1 除以 0,并赋值给 a 。因为 0 作除数是没有意义的,所以运行后会产生如下错误:

>>> a = 1/0
Traceback (most recent call last):File "<pyshell#2>", line 1, in <module>a = 1/0
ZeroDivisionError: division by zero

Python中,把这种运行时产生错误的情况叫做异常(Exceptions

异常处理机制

异常处理机制己经成为衡量一门编程语言是否成熟的标准之一,使用异常处理机制的 Python程序具有更好的容错性,更加健壮。

try except异常处理

Python提供了try except语句捕获并处理异常,该异常处理语句的基本语法结构如下:

try:可能产生异常的代码块
except [(Error1, Error2, ...) [as e]]:处理异常的代码块1
except [(Error3, Error4, ...) [as e]]:处理异常的代码块2

该格式中,[] 括起来的部分可以使用,也可以省略;(Error1,Error2,...) 、(Error3,Error4,...) 表示各自的except块可以处理异常的具体类型;[as e]表示将异常类型赋值给变量e(方便在 except 块中调用异常类型)。

try except语句的执行流程如下:

  1. 首先执行try中的代码块,如果执行过程中出现异常,系统会自动生成一个异常对象,该异常对象会提交给Python解释器,此过程被称为引发异常
  2. Python解释器收到异常对象时,会寻找能处理该异常对象的except块,如果找到合适的except块,则把该异常对象交给该except块处理,这个过程被称为捕获异常。如果Python解释器找不到捕获异常的except块,则程序运行终止,Python解释器也将退出。
try:a = int(input("输入被除数:"))b = int(input("输入除数:"))c = a / bprint("您输入的两个数相除的结果是:", c )
except (ValueError, ArithmeticError):print("程序发生了数字格式异常、算术异常之一")
except :print("未知异常")
print("程序继续运行")"""运行结果
输入被除数:a
程序发生了数字格式异常、算术异常之一
程序继续运行
"""

访问异常信息

如果程序需要在except块中访问异常对象的相关信息,可以通过为except块添加as a来实现。当Python解释器决定调用某个except块来处理该异常对象时,会将异常对象赋值给except块后的异常变量,程序即可通过该变量来获得异常对象的相关信息。

所有的异常对象都包含了如下几个常用属性和方法:

  • args:该属性返回异常的错误编号和描述字符串。
  • errno:该属性返回异常的错误编号。
  • strerror:该属性返回异常的描述宇符串。
  • with_traceback():通过该方法可处理异常的传播轨迹信息。
def foo():try:fis = open("a.txt");except Exception as e:# 访问异常的错误编号和详细信息print(e.args)# 访问异常的错误编号print(e.errno)# 访问异常的详细信息print(e.strerror)
foo()"""运行结果
(2, 'No such file or directory')
2
No such file or directory
"""

try except else详解

try except else语句是在原来try except语句的基础上再添加一个else子句,其作用是指定当try块中没有发现异常时要执行的代码。换句话说,当try块中发现异常,则else块中的语句将不会被执行。

s = input('请输入除数:')
try:result = 20 / int(s)print('20除以%s的结果是: %g' % (s , result))
except ValueError:print('值错误,您必须输入数值')
except ArithmeticError:print('算术错误,您不能输入0')
else:print('没有出现异常')
print("程序继续运行")"""
请输入除数:3
20 除以3 的结果是:6.66667
没有出现异常
程序继续运行
"""

try except finally资源回收

finally语句是与tryexcept语句配合使用的,其通常是用来做清理工作的。无论try中的语句是否跳入except中,最终都要进入finally语句,并执行其中的代码块。

在异常处理语法结构中,只有 try 块是必需的,也就是说:

  • 如果没有try块,则不能有后面的except块和finally块;
  • except块和finally块都是可选的,但except块和finally块至少出现其中之一,也可以同时出现;
  • 可以有多个except块,但捕获父类异常的except块应该位于捕获子类异常的except块的后面;
  • 不能只有try块,既没有except块,也没有finally块;
  • 多个except块必须位于try块之后,finally块必须位于所有的except块之后。

finally语句块和else语句块的区别是,else语句块只有在没有异常发生的情况下才会执行,而finally语句则不管异常是否发生都会执行。不仅如此,无论是正常退出、异常退出,还是通过break、continue、return语句退出,finally语句块都会执行。

import os
def test():fis = Nonetry:fis = open("a.txt")except OSError as e:print(e.strerror)# return语句强制方法返回return        # ①#os._exit(1)     # ②finally:# 关闭磁盘文件,回收资源if fis is not None:try:# 关闭资源fis.close()except OSError as ioe:print(ioe.strerror)print("执行finally块里的资源回收!")
test()"""运行结果
No such file or directory
执行finally里的资源回收!
"""

总结

到本节为止,读者已经学习了整个Python的异常处理机制的结构,接下来带领大家回顾一下,在此过程还会讲解一些新的知识。

首先,Python完整的异常处理语法结构如下:

try:#业务实现代码except Exception1 as e:#异常处理块1...except Exception2 as e:#异常处理块2...#可以有多个 except...else:#正常处理块finally :#资源回收块...

整个异常处理结构的执行过程,如图 1 所示。

https://blog-picture-1259089570.cos.ap-guangzhou.myqcloud.com/2-1ZI01002511S.jpg

图 1 异常处理语句块的执行流程

注意,在整个异常处理结构中,只有try块是必需的,也就是说:

  • 如果没有try块,则不能有后面的except块、else块和finally块。但是也不能只使用try块,要么使用try except结构,要么使用try finally结构;
  • except块、else块、finally块都是可选的,当然也可以同时出现;
  • 可以有多个except块,但捕获父类异常的except块应该位于捕获子类异常的except块的后面;
  • 多个except块必须位于try块之后,finally块必须位于所有的except块之后。
  • 要使用else块,其前面必须包含tryexcept

其中,很多初学者分不清 finallyelse的区别,这里着重说一下。else语句块只有在没有异常发生的情况下才会执行,而finally语句则不管异常是否发生都会执行。不仅如此,无论是正常退出、遇到异常退出,还是通过breakcontinuereturn语句退出,finally语句块都会执行。

注意,如果程序中运行了强制退出Python解释器的语句(如os._exit(1)),则finally语句将无法得到执行。例如:

import os
try:os._exit(1)
finally:print("执行finally语句")

运行程序,没有任何输出。因此,除非在try块、except块中调用了退出Python解释器的方法,否则不管在try块、except块中执行怎样的代码,出现怎样的情况,异常处理的finally块总会被执行。

另外在通常情况下,不要在finally块中使用如returnraise等导致方法中止的语句(raise语句将在后面介绍),一旦在finally块中使用了returnraise语句,将会导致try块、except块中的returnraise语句失效。看如下程序:

def test():try:# 因为finally块中包含了return语句# 所以下面的return语句失去作用return Truefinally:return False
print(test())

上面程序在finally块中定义了一条return False语句,这将导致try块中的return true失去作用。运行上面程序,输出结果为:

False

同样,如果Python程序在执行try块、except块包含有returnraise语句,则Python解释器执行到该语句时,会先去查找finally块,如果没有finally块,程序才会立即执行returnraise语句;反之,如果找到finally块,系统立即开始执行finally块,只有当finally块执行完成后,系统才会再次跳回来执行try块、except块里的returnraise语句。

但是,如果在 finally 块里也使用了returnraise等导致方法中止的语句,finally块己经中止了方法,系统将不会跳回去执行try块、except块里的任何代码。

尽量避免在finally块里使用return或raise等导致方法中止的语句,否则可能出现一些很奇怪的情况。

raise用法

在前面章节的学习中,遗留过一个问题,即是否可以在程序的指定位置手动抛出一个异常?答案是肯定的,Python允许我们在程序中手动设置异常,使用raise语句即可。

读者可能会感到疑惑,即我们从来都是想方设法地让程序正常运行,为什么还要手动设置异常呢?首先要分清楚程序发生异常和程序执行错误,它们完全是两码事,程序由于错误导致的运行异常,是需要程序员想办法解决的;但还有一些异常,是程序正常运行的结果,比如用raise手动引发的异常。

raise语句的基本语法格式为:

raise [exceptionName [(reason)]]

其中,用[]括起来的为可选参数,其作用是指定抛出的异常名称,以及异常信息的相关描述。如果可选参数全部省略,则raise会把当前错误原样抛出;如果仅省略 (reason),则在抛出异常时,将不附带任何的异常描述信息。

也就是说,raise语句有如下三种常用的用法:

  1. raise:单独一个raise。该语句引发当前上下文中捕获的异常(比如在except块中),或默认引发RuntimeError异常。
  2. raise异常类名称:raise后带一个异常类名称,表示引发执行类型的异常。
  3. raise异常类名称(描述信息):在引发指定类型的异常的同时,附带异常的描述信息。

显然,每次执行raise语句,都只能引发一次执行的异常。首先,我们来测试一下以上 3 种raise的用法:

>>> raiseTraceback (most recent call last):File "<pyshell#1>", line 1, in <module>raiseRuntimeError: No active exception to reraise
>>> raise ZeroDivisionErrorTraceback (most recent call last):File "<pyshell#0>", line 1, in <module>raise ZeroDivisionErrorZeroDivisionError
>>> raise ZeroDivisionError("除数不能为零")Traceback (most recent call last):File "<pyshell#2>", line 1, in <module>raise ZeroDivisionError("除数不能为零")ZeroDivisionError: 除数不能为零

当然,我们手动让程序引发异常,很多时候并不是为了让其崩溃。事实上,raise语句引发的异常通常用try exceptelse finally)异常处理结构来捕获并进行处理。例如:

try:a = input("输入一个数:")#判断用户输入的是否为数字if(not a.isdigit()):raise ValueError("a 必须是数字")
except ValueError as e:print("引发异常:",repr(e))

程序运行结果为:

输入一个数:a引发异常: ValueError('a 必须是数字',)

可以看到,当用户输入的不是数字时,程序会进入if判断语句,并执行raise引发ValueError异常。但由于其位于try块中,因为raise抛出的异常会被try捕获,并由except块进行处理。

因此,虽然程序中使用了raise语句引发异常,但程序的执行是正常的,手动抛出的异常并不会导致程序崩溃。

raise不需要参数

正如前面所看到的,在使用raise语句时可以不带参数,例如:

try:a = input("输入一个数:")if(not a.isdigit()):raise ValueError("a 必须是数字")
except ValueError as e:print("引发异常:",repr(e))raise

程序执行结果为:

输入一个数:a引发异常: ValueError('a 必须是数字',)Traceback (most recent call last):File "D:\python3.6\1.py", line 4, in <module>raise ValueError("a 必须是数字")ValueError: a 必须是数字

这里重点关注位于except块中的raise,由于在其之前我们已经手动引发了ValueError异常,因此这里当再使用raise语句时,它会再次引发一次。

当在没有引发过异常的程序使用无参的raise语句时,它默认引发的是RuntimeError异常。例如:

try:a = input("输入一个数:")if(not a.isdigit()):raise
except RuntimeError as e:print("引发异常:",repr(e))

程序执行结果为:

输入一个数:a引发异常: RuntimeError('No active exception to reraise',)

获取异常信息

sys.exc_info()方法:获取异常信息

在实际调试程序的过程中,有时只获得异常的类型是远远不够的,还需要借助更详细的异常信息才能解决问题。

捕获异常时,有 2 种方式可获得更多的异常信息,分别是:

  1. 使用sys模块中的exc_info方法;
  2. 使用traceback模块中的相关函数。

本节首先介绍如何使用sys模块中的exc_info()方法获得更多的异常信息。

模块sys中,有两个方法可以返回异常的全部信息,分别是exc_info()last_traceback(),这两个函数有相同的功能和用法,本节仅以exc_info()方法为例。

**exc_info()方法会将当前的异常信息以元组的形式返回,该元组中包含3个元素,分别为typevaluetraceback,**它们的含义分别是:

  • type:异常类型的名称,它是BaseException的子类
  • value:捕获到的异常实例。
  • traceback:是一个traceback对象。

举个例子:

#使用 sys 模块之前,需使用 import 引入
import sys
try:x = int(input("请输入一个被除数:"))print("30除以",x,"等于",30/x)
except:print(sys.exc_info())print("其他异常...")

当输入0时,程序运行结果为:

请输入一个被除数:0(<class 'ZeroDivisionError'>, ZeroDivisionError('division by zero',), <traceback object at 0x000001FCF638DD48>)其他异常...

输出结果中,第2行是抛出异常的全部信息,这是一个元组,有3个元素,第一个元素是一个ZeroDivisionError类;第2个元素是异常类型ZeroDivisionError类的一个实例;第3个元素为一个traceback对象。其中,通过前2个元素可以看出抛出的异常类型以及描述信息,对于第3个元素,是一个traceback对象,无法直接看出有关异常的信息,还需要对其做进一步处理。

要查看traceback对象包含的内容,需要先引进traceback模块,然后调用traceback模块中的print_tb方法,并将sys.exc_info()输出的traceback对象作为参数参入。例如:

#使用 sys 模块之前,需使用 import 引入
import sys
#引入traceback模块
import traceback
try:x = int(input("请输入一个被除数:"))print("30除以",x,"等于",30/x)
except:#print(sys.exc_info())traceback.print_tb(sys.exc_info()[2])print("其他异常...")

输入0,程序运行结果为:

请输入一个被除数:0File "C:\Users\mengma\Desktop\demo.py", line 7, in <module>print("30除以",x,"等于",30/x)其他异常...

可以看到,输出信息中包含了更多的异常信息,包括文件名、抛出异常的代码所在的行数、抛出异常的具体代码。

traceback模块:获取异常信息

除了使用sys.exc_info()方法获取更多的异常信息之外,还可以使用traceback模块,该模块可以用来查看异常的传播轨迹,追踪异常触发的源头。

下面示例显示了如何显示异常传播轨迹:

class SelfException(Exception):pass
def main():firstMethod()
def firstMethod():secondMethod()
def secondMethod():thirdMethod()
def thirdMethod():raise SelfException("自定义异常信息")
main()

上面程序中main()函数调用firstMethod()firstMethod()调用secondMethod()secondMethod()调用thirdMethod()thirdMethod()直接引发一个SelfException异常。运行上面程序,将会看到如下所示的结果:

Traceback (most recent call last):File "C:\Users\mengma\Desktop\1.py", line 11, in <module>main()File "C:\Users\mengma\Desktop\1.py", line 4, in main          <--mian函数firstMethod()File "C:\Users\mengma\Desktop\1.py", line 6, in firstMethod    <--第三个secondMethod()File "C:\Users\mengma\Desktop\1.py", line 8, in secondMethod  <--第二个thirdMethod()File "C:\Users\mengma\Desktop\1.py", line 10, in thirdMethod   <--异常源头raise SelfException("自定义异常信息")SelfException: 自定义异常信息

从输出结果可以看出,异常从thirdMethod()函数开始触发,传到secondMethod()函数,再传到firstMethod()函数,最后传到main()函数,在main()函数止,这个过程就是整个异常的传播轨迹。

在实际应用程序的开发中,大多数复杂操作都会被分解成一系列函数或方法调用。这是因为,为了具有更好的可重用性,会将每个可重用的代码单元定义成函数或方法,将复杂任务逐渐分解为更易管理的小型子任务。由于一个大的业务功能需要由多个函数或方法来共同实现,在最终编程模型中,很多对象将通过一系列函数或方法调用来实现通信,执行任务。

所以,当应用程序运行时,经常会发生一系列函数或方法调用,从而形成“函数调用战”。异常的传播则相反,只要异常没有被完全捕获(包括异常没有被捕获,或者异常被处理后重新引发了新异常),异常就从发生异常的函数或方法逐渐向外传播,首先传给该函数或方法的调用者,该函数或方法的调用者再传给其调用者,直至最后传到 Python解释器,此时Python解释器会中止该程序,并打印异常的传播轨迹信息。

很多初学者一看到输出结果所示的异常提示信息,就会惊慌失措,他们以为程序出现了很多严重的错误,其实只有一个错误,系统提示那么多行信息,只不过是显示异常依次触发的轨迹。

其实,上面程序的运算结果显示的异常传播轨迹信息非常清晰,它记录了应用程序中执行停止的各个点。最后一行信息详细显示了异常的类型和异常的详细消息。从这一行向上,逐个记录了异常发生源头、异常依次传播所经过的轨迹,并标明异常发生在哪个文件、哪一行、哪个函数处。

使用traceback模块查看异常传播轨迹,首先需要将traceback模块引入,该模块提供了如下两个常用方法:

  • traceback.print_exc():将异常传播轨迹信息输出到控制台或指定文件中。
  • format_exc():将异常传播轨迹信息转换成字符串。

可能有读者好奇,从上面方法看不出它们到底处理哪个异常的传播轨迹信息。实际上我们常用的print_exc()print_exc([limit[, file]])省略了limitfile两个参数的形式。而print_exc([limit[, file]])的完整形式是 print_exception(etype, value, tb[,limit[, file]]),在完整形式中,前面三个参数用于分别指定异常的如下信息:

  • etype:指定异常类型;
  • value:指定异常值;
  • tb:指定异常的traceback 信息;

当程序处于except块中时,该except块所捕获的异常信息可通过sys对象来获取,其中sys.exc_typesys.exc_valuesys.exc_traceback就代表当前except块内的异常类型、异常值和异常传播轨迹。

简单来说,print_exc([limit[, file]])相当于如下形式:

print_exception(sys.exc_etype, sys.exc_value, sys.exc_tb[, limit[, file]])

也就是说,使用print_exc([limit[, file]])会自动处理当前except块所捕获的异常。该方法还涉及两个参数:

  1. limit:用于限制显示异常传播的层数,比如函数 A调用函数B,函数B发生了异常,如果指定limit=1,则只显示函数 A 里面发生的异常。如果不设置limit参数,则默认全部显示。
  2. file:指定将异常传播轨迹信息输出到指定文件中。如果不指定该参数,则默认输出到控制台。

借助于traceback模块的帮助,我们可以使用except块捕获异常,并在其中打印异常传播信息,包括把它输出到文件中。例如如下程序:

# 导入trackback模块
import traceback
class SelfException(Exception): passdef main():firstMethod()
def firstMethod():secondMethod()
def secondMethod():thirdMethod()
def thirdMethod():raise SelfException("自定义异常信息")
try:main()
except:# 捕捉异常,并将异常传播信息输出控制台traceback.print_exc()# 捕捉异常,并将异常传播信息输出指定文件中traceback.print_exc(file=open('log.txt', 'a'))

上面程序第一行先导入了traceback模块,接下来程序使用except捕获程序的异常,并使用tracebackprint_exc()方法输出异常传播信息,分别将它输出到控制台和指定文件中。

运行上面程序,同样可以看到在控制台输出异常传播信息,而且在程序目录下生成了一个log.txt文件,该文件中同样记录了异常传播信息。

自定义异常类及用法

前面的例子里充斥了很多Python内置的异常类型,读者也许会问,我可以创建自己的异常类型吗?

答案是肯定的,Python允许用户自定义异常类型。实际开发中,有时候系统提供的异常类型不能满足开发的需求。这时就可以创建一个新的异常类来拥有自己的异常。

其实,在前面章节中,已经涉及到了异常类的创建,例如:

class SelfExceptionError(Exception):pass
try:raise SelfExceptionError()
except SelfExceptionError as err:print("捕捉到自定义异常")

运行结果为:

	捕捉到自定义异常

可以看到,此程序中就自定义了一个名为SelfExceptionError的异常类,只不过该类是一个空类。

由于大多数Python内置异常的名字都以"Error"结尾,所以实际命名时尽量跟标准的异常命名一样。

需要注意的是,自定义一个异常类,通常应继承自Exception类(直接继承),当然也可以继承自那些本身就是从Exception继承而来的类(间接继承Exception)。

https://blog-picture-1259089570.cos.ap-guangzhou.myqcloud.com/2-1ZQ9154321244.gif

图 1

Python

异常类继承图

注意,虽然所有类同时继承自BaseException,但它是为系统退出异常而保留的,假如直接继承 BaseException,可能会导致自定义异常不会被捕获,而是直接发送信号退出程序运行,脱离了我们自定义异常类的初衷。

另外,系统自带的异常只要触发会自动抛出(比如NameErrorValueError等),但用户自定义的异常需要用户自己决定什么时候抛出。也就是说,自定义的异常需要使用raise手动抛出。

下面也是自定义的异常类,和上面的异常类相比,其内部实现了__init__()方法和__str__()方法:

class InputError(Exception):'''当输出有误时,抛出此异常'''#自定义异常类型的初始化def __init__(self, value):self.value = value# 返回异常类对象的说明信息def __str__(self):return ("{} is invalid input".format(repr(self.value)))try:raise InputError(1) # 抛出 MyInputError 这个异常
except InputError as err:print('error: {}'.format(err))

运行结果为:

error: 1 is invalid input

注意,只要自定义的类继承自Exception,则该类就是一个异常类,至于此类中包含的内容,并没有做任何规定。

异常机制使用细则

前面介绍了使用异常处理的优势、便捷之处,本节将进一步从程序性能优化、结构优化的角度给出异常处理的一般规则。

成功的异常处理应该实现如下 4 个目标:

  1. 使程序代码混乱最小化。
  2. 捕获并保留诊断信息。
  3. 通知合适的人员。
  4. 采用合适的方式结束异常活动。

下面介绍达到这些效果的基本准则。

不要过度使用异常

不可否认,Python的异常机制确实方便,但滥用异常机制也会带来一些负面影响。过度使用异常主要表现在两个方面:

  1. 把异常和普通错误混淆在一起,不再编写任何错误处理代码,而是以简单地引发异常来代苦所有的错误处理。
  2. 使用异常处理来代替流程控制。

熟悉了异常使用方法后,程序员可能不再愿意编写烦琐的错误处理代码,而是简单地引发异常。实际上这样做是不对的,对于完全己知的错误和普通的错误,应该编写处理这种错误的代码,增加程序的健壮性。只有对于外部的、不能确定和预知的运行时错误才使用异常。

对比前面五子棋游戏中,处理用户输入坐标点己有棋子的两种方式。如果用户试图下棋的坐标点己有棋子:

#如果要下棋的点不为空
if board[int(y_str) - 1) [int(x_str) - 1] !="╋" :inputStr = input ("您输入的坐标点己有棋子了,请重新输入\n")continue

上面这种处理方式检测到用户试图下棋的坐标点己经有棋子,立即打印一条提示语句,并重新开始下一次循环。这种处理方式简洁明了、逻辑清晰,程序的运行效率也很好程序进入if块后,即结束了本次循环。

如果将上面的处理机制改为如下方式:

#如果要下棋的点不为空
if board[int(y_str) - 1) [int(x_str) - 1) != "╋":#引发默认的RuntimeError 异常raise

上面这种处理方式没有提供有效的错误处理代码,当程序检测到用户试图下棋的坐标点己经有棋子时,并没有提供相应的处理,而是简单地引发一个异常。这种处理方式虽然简单,但Python解释器接收到这个异常后,还需要进入相应的except块来捕获该异常,所以运行效率要差一些。而且用户下棋重复这个错误完全是可预料的,所以程序完全可以针对该错误提供相应的处理,而不是引发异常。

必须指出,异常处理机制的初衷是将不可预期异常的处理代码和正常的业务逻辑处理代码分离,因此绝不要使用异常处理来代替正常的业务逻辑判断。

另外,异常机制的效率比正常的流程控制效率差,所以不要使用异常处理来代替正常的程序流程控制。例如,对于如下代码:

#定义一个字符串列表
my_list =["Hello", "Python", "Spring"]
#使用异常处理来遍历arr数组的每个元素
try:i = 0while True:print (my_list [i])i += 1
except:pass

运行上面程序确实可以实现遍历 my_list 列表的功能,但这种写法可读性较差,而且运行效率也不高。程序完全有能力避免产生indexError异常,程序“故意”制造这种异常,然后使用except块去捕获该异常,这是不应该的。将程序改为如下形式肯定要好得多:

i = 0
while i < len(my_list):print(my_list[i])i += 1

注意,异常只应该用于处理非正常的情况,不要使用异常处理来代替正常的流程控制。对于一些完全可预知,而且处理方式清楚的错误,程序应该提供相应的错误处理代码,而不是将其笼统地称为异常。

不要使用过于庞大的try

很多初学异常机制的读者喜欢在try块里放置大量的代码,这看上去很“简单”,但这种“简单”只是一种假象,只是在编写程序时看上去比较简单。但因为try块里的代码过于庞大,业务过于复杂,就会造成try块中出现异常的可能性大大增加,从而导致分析异常原因的难度也大大增加。

而且当时块过于庞大时,就难免在try块后紧跟大量的except块才可以针对不同的异常提供不同的处理逻辑。在同一个 try 块后紧跟大量的except块则需要分析它们之间的逻辑关系,反而增加了编程复杂度。

正确的做法是,把大块的try块分割成多个可能出现异常的程序段落,并把它们放在单独的try块中,从而分别捕获并处理异常。

不要忽略捕获到的异常

不要忽略异常!既然己捕获到异常,那么except块理应做些有用的事情,及处理并修复异常。except块整个为空,或者仅仅打印简单的异常信息都是不妥的!

except块为空就是假装不知道甚至瞒天过海,这是最可怕的事情,程序出了错误,所有人都看不到任何异常,但整个应用可能已经彻底坏了。仅在except块里打印异常传播信息稍微好一点,但仅仅比空白多了几行异常信息。通常建议对异常采取适当措施,比如:

  • 处理异常。对异常进行合适的修复,然后绕过异常发生的地方继续运行;或者用别的数据进行计算,以代替期望的方法返回值;或者提示用户重新操作……总之,程序应该尽量修复异常,使程序能恢复运行。
  • 重新引发新异常。把在当前运行环境下能做的事情尽量做完,然后进行异常转译,把异常包装成当前层的异常,重新传给上层调用者。
  • 在合适的层处理异常。如果当前层不清楚如何处理异常,就不要在当前层使用except语句来捕获该异常,让上层调用者来负责处理该异常。

logging模块

无论使用哪种编程语言,最常用的调试代码的方式是:使用输出语句(比如C语言中使用printfPython中使用print()函数)输出程序运行过程中一些关键的变量的值,查看它们的值是否正确,从而找到出错的地方。

启用logging模块很简单,直接将下面的代码复制到程序开头:

import logging
logging.basicConfig(level=logging.DEBUG,format=' %(asctime)s - %(levelname)s - %(message)s')

假如我们编写了如下一个函数,其设计的初衷是用来计算一个数的阶乘,但该函数有些问题,需要调试:

import logging
logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s - %(levelname)s - %(message)s')
logging.debug('Start of program')
def factorial(n):logging.debug('Start of factorial(%s%%)' % (n))total = 1for i in range(n + 1):total *= ilogging.debug('i is ' + str(i) + ', total is ' + str(total))logging.debug('End of factorial(%s%%)' % (n))return total
print(factorial(5))
logging.debug('End of program')"""运行结果
2019-09-11 14:14:56,928 - DEBUG - Start of program
2019-09-11 14:14:56,945 - DEBUG - Start of factorial(5%)
2019-09-11 14:14:56,959 - DEBUG - i is 0, total is 0
2019-09-11 14:14:56,967 - DEBUG - i is 1, total is 0
2019-09-11 14:14:56,979 - DEBUG - i is 2, total is 0
2019-09-11 14:14:56,991 - DEBUG - i is 3, total is 0
2019-09-11 14:14:57,000 - DEBUG - i is 4, total is 0
2019-09-11 14:14:57,013 - DEBUG - i is 5, total is 0
2019-09-11 14:14:57,024 - DEBUG - End of factorial(5%)
0
2019-09-11 14:14:57,042 - DEBUG - End of program
"""

可以看到,通过logging.debug()函数可以打印日志信息,这个 debug() 函数将调用basicConfig()打印一行信息,这行信息的格式是在basicConfig()函数中指定的,并且包括传递给debug()的消息。

分析程序的运行结果,factorial(5)返回 0 作为 5 的阶乘的结果,这显然是不对的。for 循环应该用从 1 到 5 的数,乘以total的值,但logging.debug()显示的日志信息表明,i 变量从 0 开始,而不是 1。因为 0 乘任何数都是 0,所以接下来的迭代中,total的值都是错的。日志消息提供了可以追踪的痕迹,帮助我们弄清楚程序运行过程哪里不对。

logging日志级别

“日志级别”提供了一种方式,按重要性对日志消息进行分类。

级别对应的函数描述
DEBUGlogging.debug()最低级别,用于小细节,通常只有在诊断问题时,才会关心这些消息。
INFOlogging.info()用于记录程序中一般事件的信息,或确认一切工作正常。
WARNINGlogging.warning()用于表示可能的问题,它不会阻止程序的工作,但将来可能会。
ERRORlogging.error()用于记录错误,它导致程序做某事失败。
CRITICALlogging.critical()最高级别,用于表示致命的错误,它导致或将要导致程序完全停止工作。

日志消息将会作为一个字符串,传递给这些函数。另外,日志级别只是一种建议,归根到底还是由程序员自己来决定日志消息属于哪一种类型。

举个例子:

>>>import logging
>>> logging.basicConfig(level=logging.DEBUG,format=' %(asctime)s - %(levelname)s - %(message)s')
>>> logging.debug('Some debugging details.')
2019-09-11 14:32:34,249 - DEBUG - Some debugging details.
>>> logging.info('The logging module is working.')
2019-09-11 14:32:47,456 - INFO - The logging module is working.
>>> logging.warning('An error message is about to be logged.')
2019-09-11 14:33:02,391 - WARNING - An error message is about to be logged.
>>> logging.error('An error has occurred.')
2019-09-11 14:33:14,413 - ERROR - An error has occurred.
>>> logging.critical('The program is unable to recover!')
2019-09-11 14:33:24,071 - CRITICAL - The program is unable to recover!

日志级别的好处在于,我们可以改变想看到的日志消息的优先级。比如说,向basicConfig()函数传入logging.DEBUG作为level关键字参数,这将显示所有级别为 DEBUG的日志消息。当开发了更多的程序后,我们可能只对错误感兴趣,在这种情况下,可以将basicConfig()level参数设置为logging.ERROR,这将只显示ERRORCRITICAL消息,跳过DEBUGINFOWARNING消息。

将日志消息输出到文件中

将日志消息输出到文件中的实现方法很简单,只需要设置logging.basicConfig()函数中的filename关键字参数即可,例如:

>>> import logging
>>> logging.basicConfig(filename='demo.txt', level=logging.DEBUG,format='%(asctime)s - %(levelname)s - %(message)s')

此程序中,将日志消息存储到了demo.txt文件中,该文件就位于运行的程序文件所在的目录。


http://www.ppmy.cn/news/85768.html

相关文章

数据结构学习之——线性表

1、线性表的定义和基本操作 1.1、线性表的定义 线性表是具有相同数据类型的n个数据元素的有限序列&#xff0c;其中n为表长&#xff0c;当n0时线性表是一个空表。若以L命名&#xff0c;则表示为 L ( a 1 , a 2 , , ⋯ , a i , a i 1 , ⋯ , a n ) L\left(a_{1}, a_{2,},\c…

Java String split()方法详细教程

Java String split方法详细教程 1、内部实现2、语法3、参数4、返回值5、抛出异常6、Java String split()方法示例7、Java String split()方法与正则表达式和长度示例8、Java String split()方法与正则表达式和长度示例2 Java String类的split()方法根据给定的正则表达式拆分字符…

SpringCloud Sleuth+Zipkin

SpringCloud SleuthZipkin 官网 https://github.com/spring-cloud/spring-cloud-sleuth Sleuth/Zipkin 是什么&#xff1f; 概述(两张图) 在微服务框架中&#xff0c;一个由客户端发起的请求在后端系统中会经过多个不同的的服务节点调用, 来协同产生最后的请求结果&#x…

SelFlow: Self-Supervised Learning of Optical Flow

本文提出了一种用于光流的自监督学习方法。该方法从非遮挡像素中提取可靠的流估计&#xff0c;并使用这些预测作为真值来学习幻觉遮挡的光流。 本文设计了一个简单的 CNN&#xff0c;以利用 来自多个帧的时间信息 来更好地进行流估计。本方法可以在 MPI Sintel、KITTI 2012 和 …

优维低代码实践:页面编排优化与数据联调

优维低代码技术专栏&#xff0c;是一个全新的、技术为主的专栏&#xff0c;由优维技术委员会成员执笔&#xff0c;基于优维7年低代码技术研发及运维成果&#xff0c;主要介绍低代码相关的技术原理及架构逻辑&#xff0c;目的是给广大运维人提供一个技术交流与学习的平台。 优维…

LLMs开源模型们的分布式训练和量化

前一篇博文整理了&#xff1a; LLMs开源模型们和数据集简介 这篇博文主要整理一下目前流行的训练方法和量化。 &#xff08;图自Towards a Unified View of Parameter-Efficient Transfer Learning&#xff09; Tuning Strategies 使通用LLMs适应下游任务的最常见方法是微调…

机器学习算法入门与编程实践课后题及答案(唐四新等编著)

目录 习题1 习题2 习题3 习题4 习题5 习题6 习题7 习题8 习题1 1.无监督学习的两个主要任务是(多选)(BD)。 A.回归 B.降维 C.分类 D.聚类 2.下列对无监督学习描述错误的是(C)。 A.无标签 B.核心是聚类 C.不需要降…

汇编十四、51单片机汇编代码规范

1、代码规范的意义 (1)提高源程序的质量和可维护性&#xff0c;从而提高生产力。 2、51汇编开头字母的使用 (1)常量C&#xff1b; (2)变量R&#xff1b; (3)位变量B&#xff1b; (4)标号L&#xff1b; (5)子程序F&#xff1b; (6)表T&#xff1b; (7)中断T&#x…