【Python学习教程】Python异常处理机制

2023-05-16

文章目录

  • 什么是异常处理,Python常见异常类型(入门必读)
    • Python语法错误
    • Python运行时错误
  • Python异常处理机制到底有什么用?
  • Python try except异常处理详解(入门必读)
    • 获取特定异常的有关信息
  • Python异常处理机制的底层实现
  • Python try except else详解
  • Python try except finally:资源回收
  • Python异常处理机制结构详解
  • Python logging模块用法快速攻略
    • Python logging日志级别
    • Python logging禁用日志
    • 将日志消息输出到文件中
  • Python assert调试程序

转载于http://c.biancheng.net/python/

程序运行时常会碰到一些错误,例如除数为 0、年龄为负数、数组下标越界等,这些错误如果不能发现并加以处理,很可能会导致程序崩溃。

和 C++、Java 这些编程语言一样,Python 也提供了处理异常的机制,可以让我们捕获并处理这些错误,让程序继续沿着一条不会出错的路径执行。

可以简单的理解异常处理机制,就是在程序运行出现错误时,让 Python 解释器执行事先准备好的除错程序,进而尝试恢复程序的执行。

借助异常处理机制,甚至在程序崩溃前也可以做一些必要的工作,例如将内存中的数据写入文件、关闭打开的文件、释放分配的内存等。

Python 异常处理机制会涉及 try、except、else、finally 这 4 个关键字,同时还提供了可主动使程序引发异常的 raise 语句,本章都会为你一一讲解。

什么是异常处理,Python常见异常类型(入门必读)

开发人员在编写程序时,难免会遇到错误,有的是编写人员疏忽造成的语法错误,有的是程序内部隐含逻辑问题造成的数据错误,还有的是程序运行时与系统的规则冲突造成的系统错误,等等。

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

Python语法错误

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

print “Hello,World!”

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

SyntaxError: Missing parentheses in call to ‘print’

语法错误多是开发者疏忽导致的,属于真正意义上的错误,是解释器无法容忍的,因此,只有将程序中的所有语法错误全部纠正,程序才能执行。

Python运行时错误

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

a = 1/0

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

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

以上运行输出结果中,前两段指明了错误的位置,最后一句表示出错的类型。在 Python 中,把这种运行时产生错误的情况叫做异常(Exceptions)。这种异常情况还有很多,常见的几种异常情况如表 1 所示。

异常类型含义实例
AssertionError当 assert 关键字后的条件为假时,程序运行会停止并抛出 AssertionError 异常>>> demo_list = [‘C语言中文网’] >>> assert len(demo_list) > 0 >>> demo_list.pop() ‘C语言中文网’ >>> assert len(demo_list) > 0 Traceback (most recent call last): File “<pyshell#6>”, line 1, in assert len(demo_list) > 0 AssertionError
AttributeError当试图访问的对象属性不存在时抛出的异常>>> demo_list = [‘C语言中文网’] >>> demo_list.len Traceback (most recent call last): File “<pyshell#10>”, line 1, in demo_list.len AttributeError: ‘list’ object has no attribute ‘len’
IndexError索引超出序列范围会引发此异常>>> demo_list = [‘C语言中文网’] >>> demo_list[3] Traceback (most recent call last): File “<pyshell#8>”, line 1, in demo_list[3] IndexError: list index out of range
KeyError字典中查找一个不存在的关键字时引发此异常>>> demo_dict={‘C语言中文网’:“c.biancheng.net”} >>> demo_dict[“C语言”] Traceback (most recent call last): File “<pyshell#12>”, line 1, in demo_dict[“C语言”] KeyError: ‘C语言’
NameError尝试访问一个未声明的变量时,引发此异常>>> C语言中文网 Traceback (most recent call last): File “<pyshell#15>”, line 1, in C语言中文网 NameError: name ‘C语言中文网’ is not defined
TypeError不同类型数据之间的无效操作>>> 1+‘C语言中文网’ Traceback (most recent call last): File “<pyshell#17>”, line 1, in 1+‘C语言中文网’ TypeError: unsupported operand type(s) for +: ‘int’ and ‘str’
ZeroDivisionError除法运算中除数为 0 引发此异常>>> a = 1/0 Traceback (most recent call last): File “<pyshell#2>”, line 1, in a = 1/0 ZeroDivisionError: division by zero

提示:表中的异常类型不需要记住,只需简单了解即可。

当一个程序发生异常时,代表该程序在执行时出现了非正常的情况,无法再执行下去。默认情况下,程序是要终止的。如果要避免程序退出,可以使用捕获异常的方式获取这个异常的名称,再通过其他的逻辑代码让程序继续运行,这种根据异常做出的逻辑处理叫作异常处理。

开发者可以使用异常处理全面地控制自己的程序。异常处理不仅仅能够管理正常的流程运行,还能够在程序出错时对程序进行必的处理。大大提高了程序的健壮性和人机交互的友好性。

那么,应该如何捕获和处理异常呢?可以使用 try 语句来实现。有关 try 语句的语法和用法,会在后续章节继续详解。

Python异常处理机制到底有什么用?

异常处理是现代编程语言不可或缺的能力,它已经成为衡量一门编程语言是否成熟和健壮的标准之一,C++、Java、C#、Python 等高级语言都提供了异常处理机制。

无论你是多么优秀的程序员,你都不能保证自己的程序永远不会出错。就算你的程序没有错,用户也不一定按照你设定的规则来使用你的程序,总有一些小白或者极客会“玩弄”你的程序。

除此以外,你也不能保证程序的运行环境永远稳定,比如操作系统可能崩溃,网络可能无法连接,内存可能突然坏掉……

总之,你基本什么都保证不了。但是,作为一个负责任的程序员,我们要让自己的程序尽可能的健壮,尽可能保证在恶劣环境下还能正常运行,或者给用户提示错误,让用户决定是否退出。

例如有一个五子棋程序,当用户输入落子的坐标时,程序既要判断输入格式是否正确(横坐标和纵坐标之间由逗号分隔),还要判断坐标是否在合法的范围内。一般我们都会这样来处理:

if 坐标包含了除逗号之外的其它非数字字符:
alert 坐标只能是数值
goto retry
elif 坐标不包含逗号:
alert 必须使用逗号分隔横坐标和纵坐标
goto retry
elif 坐标落在了棋盘外:
alert 坐标必须位于棋盘之内
goto retry
elif 作为位置已有其它棋子:
alert 只能在没有棋子的位置落子
goto retry
else:
#正常的业务代码

上面的代码并没有涉及所有出错情形,只是考虑了四种可能出错的情形,代码量就已经急剧增加了。

在实际开发中,不可预料的情况呈数量级增长,甚至不能穷举,按照上面的逻辑来处理各种错误简直让人抓狂。

如果每次在实现真正的业务逻辑之前,都需要不厌其烦地考虑各种可能出错的情况,针对各种错误情况给出补救措施,这是多么乏味的事情啊。程序员喜欢解决问题,喜欢开发带来的“创造”快感,但不喜欢像一个“堵漏”工人,去堵那些由外在条件造成的“漏洞”。

对于构造大型、健壮、可维护的应用而言,错误处理是整个应用需要考虑的重要方面,程序员不能仅仅只做“对”的事情,程序员开发程序的过程,是一个创造的过程,这个过程需要有全面的考虑,仅做“对”的事情是远远不够的。

对于上面的错误处理机制,主要有如下两个缺点:

  • 无法穷举所有的异常情况。因为人类知识的限制,异常情况总比可以考虑到的情况多,总有“漏网之鱼”的异常情况,所以程序总是不够健壮。
  • 错误处理代码和业务实现代码混杂。这种错误处理和业务实现混杂的代码严重影响程序的可读性,会增加程序维护的难度。

程序员希望有一种强大的机制来解决上面的问题,能够将上面程序改成如下的形式:

if 用户输入不合法:
alert 输入不合法
goto retry
else :
#正常的业务代码

上面伪码提供了一个非常强大的“if 块”,即程序不管输入错误的原因是什么,只要用户输入不满足要求,程序就一次处理所有的错误。这种处理方法的好处是,使得错误处理代码变得更有条理,只需在一个地方处理错误。

现在的问题是,“用户输入不合法”这个条件怎么定义?当然,对于这个简单的要求,可以使用正则表达式对用户输入进行匹配,当用户输入与正则表达式不匹配时即可判断“用户输入不合法”。但对于更复杂的情形,就没有这么简单了。使用 Python 的异常处理机制就可以解决这个问题,例如:

try:
if(用户输入不合理):
raise 异常
except Exception:
alert 输入不合法
goto retry
#正常的业务代码

此程序中,通过在 try 块中判断用户的输入数据是否合理,如果不合理,程序受 raise 的影响会进行到 except 代码块,对用户的错误输出进行处理,然后会继续执行正常的业务代码;反之,如果用户输入合理,那么程序将直接执行正常的业务代码。

try except 是 Python 实现异常处理机制的核心结构,其具体用法会在后续章节做详细介绍。

显然,使用 Python 异常处理机制,可以让程序中的异常处理代码和正常业务代码分离,使得程序代码更加优雅,并可以提高程序的健壮性。

Python try except异常处理详解(入门必读)

Python 中,用try except语句块捕获并处理异常,其基本语法结构如下所示:

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

该格式中,[] 括起来的部分可以使用,也可以省略。其中各部分的含义如下:

  • (Error1, Error2,…) 、(Error3, Error4,…):其中,Error1、Error2、Error3 和 Error4 都是具体的异常类型。显然,一个 except 块可以同时处理多种异常。
  • [as e]:作为可选参数,表示给异常类型起一个别名 e,这样做的好处是方便在 except 块中调用异常类型(后续会用到)。
  • [Exception]:作为可选参数,可以代指程序可能发生的所有异常情况,其通常用在最后一个 except 块。

try except的基本语法格式可以看出,try 块有且仅有一个,但 except 代码块可以有多个,且每个 except 块都可以同时处理多种异常。

当程序发生不同的意外情况时,会对应特定的异常类型,Python 解释器会根据该异常类型选择对应的 except 块来处理该异常。

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

  1. 首先执行 try 中的代码块,如果执行过程中出现异常,系统会自动生成一个异常类型,并将该异常提交给 Python 解释器,此过程称为捕获异常。
  2. 当 Python 解释器收到异常对象时,会寻找能处理该异常对象的 except 块,如果找到合适的 except 块,则把该异常对象交给该 except 块处理,这个过程被称为处理异常。如果 Python 解释器找不到处理异常的 except 块,则程序运行终止,Python 解释器也将退出。

事实上,不管程序代码块是否处于 try 块中,甚至包括 except 块中的代码,只要执行该代码块时出现了异常,系统都会自动生成对应类型的异常。但是,如果此段程序没有用 try 包裹,又或者没有为该异常配置处理它的 except 块,则 Python 解释器将无法处理,程序就会停止运行;反之,如果程序发生的异常经 try 捕获并由 except 处理完成,则程序可以继续执行。

举个例子:

try:
    a = int(input("输入被除数:"))
    b = int(input("输入除数:"))
    c = a / b
    print("您输入的两个数相除的结果是:", c )
except (ValueError, ArithmeticError):
    print("程序发生了数字格式异常、算术异常之一")
except :
    print("未知异常")
print("程序继续运行")

程序运行结果为:

输入被除数:a
程序发生了数字格式异常、算术异常之一
程序继续运行

上面程序中,第 6 行代码使用了(ValueError, ArithmeticError)来指定所捕获的异常类型,这就表明该 except 块可以同时捕获这 2 种类型的异常;第 8 行代码只有 except 关键字,并未指定具体要捕获的异常类型,这种省略异常类的 except 语句也是合法的,它表示可捕获所有类型的异常,一般会作为异常捕获的最后一个 except 块。

除此之外,由于 try 块中引发了异常,并被 except 块成功捕获,因此程序才可以继续执行,才有了“程序继续运行”的输出结果。

获取特定异常的有关信息

通过前面的学习,我们已经可以捕获程序中可能发生的异常,并对其进行处理。但是,由于一个 except 可以同时处理多个异常,那么我们如何知道当前处理的到底是哪种异常呢?

其实,每种异常类型都提供了如下几个属性和方法,通过调用它们,就可以获取当前处理异常类型的相关信息:

  • args:返回异常的错误编号和描述字符串;
  • str(e):返回异常信息,但不包括异常信息的类型;
  • repr(e):返回较全的异常信息,包括异常信息的类型。

举个例子:

try:
    1/0
except Exception as e:
    # 访问异常的错误编号和详细信息
    print(e.args)
    print(str(e))
    print(repr(e))

输出结果为:

(‘division by zero’,)
division by zero
ZeroDivisionError(‘division by zero’,)

除此之外,如果想要更加详细的异常信息,可以使用 traceback 模块。有兴趣的读者,可自行查阅资料学习。

从程序中可以看到,由于 except 可能接收多种异常,因此为了操作方便,可以直接给每一个进入到此 except 块的异常,起一个统一的别名 e。

在 Python 2.x 的早期版本中,除了使用 as e 这个格式,还可以将其中的 as 用逗号(,)代替。

Python异常处理机制的底层实现

前面章节中,我们详细介绍了try except异常处理的用法,简单来说,当位于 try 块中的程序执行出现异常时,会将该种异常捕获,同时找到对应的 except 块处理该异常,那么这里就有一个问题,它是如何找到对应的 except 块的呢?

我们知道,一个 try 块也可以对应多个 except 块,一个 except 块可以同时处理多种异常。如果我们想使用一个 except 块处理所有异常,就可以这样写:

try:
    #...
except Exception:
    #...

这种情况下,对于 try 块中可能出现的任何异常,Python 解释器都会交给仅有的这个 except 块处理,因为它的参数是 Exception,表示可以接收任何类型的异常。

注意,对于可以接收任何异常的 except 来说,其后可以跟 Exception,也可以不跟任何参数,但表示的含义都是一样的。

这里就要详细介绍一下 Exception。要知道,为了表示程序中可能出现的各种异常,Python 提供了大量的异常类,这些异常类之间有严格的继承关系,图 1 显示了 Python 的常见异常类之间的继承关系。

Python 的常见异常类之间的继承关系
图 1 Python 的常见异常类之间的继承关系

从图 1 中可以看出,BaseException 是 Python 中所有异常类的基类,但对于我们来说,最主要的是 Exception 类,因为程序中可能出现的各种异常,都继承自 Exception。

因此,如果用户要实现自定义异常,不应该继承 BaseException ,而应该继承 Exception 类。关于如何自定义一个异常类,可阅读《Python自定义异常类》一节。

当 try 块捕获到异常对象后,Python 解释器会拿这个异常类型依次和各个 except 块指定的异常类进行比较,如果捕获到的这个异常类,和某个 except 块后的异常类一样,又或者是该异常类的子类,那么 Python 解释器就会调用这个 except 块来处理异常;反之,Python 解释器会继续比较,直到和最后一个 except 比较完,如果没有比对成功,则证明该异常无法处理。

图 2 演示了位于 try 块中的程序发生异常时,从捕获异常到处理异常的整个流程。

Python 异常捕获流程示意图
图 2 Python 异常捕获流程示意图

下面看几个简单的异常捕获的例子:

try:
    a = int(input("输入 a:"))
    b = int(input("输入 b:"))
    print( a/b )
except ValueError:
    print("数值错误:程序只能接收整数参数")
except ArithmeticError:
    print("算术错误")
except Exception:
    print("未知异常")

该程序中,根据用户输入 a 和 b 值的不同,可能会导致 ValueError、ArithmeticError 异常:

  1. 如果用户输入的 a 或者 b 是其他字符,而不是数字,会发生 ValueError 异常,try 块会捕获到该类型异常,同时 Python 解释器会调用第一个 except 块处理异常;
  2. 如果用户输入的 a 和 b 是数字,但 b 的值为 0,由于在进行除法运算时除数不能为 0,因此会发生 ArithmeticError 异常,try 块会捕获该异常,同时 Python 解释器会调用第二个 except 块处理异常;
  3. 当然,程序运行过程中,还可能由于其他因素出现异常,try 块都可以捕获,同时 Python 会调用最后一个 except 块来处理。

当一个 try 块配有多个 except 块时,这些 except 块应遵循这样一个排序规则,即可处理全部异常的 except 块(参数为 Exception,也可以什么都不写)要放到所有 except 块的后面,且所有父类异常的 except 块要放到子类异常的 except 块的后面。

Python try except else详解

在原本的try except结构的基础上,Python 异常处理机制还提供了一个 else 块,也就是原有 try except 语句的基础上再添加一个 else 块,即try except else结构。

使用 else 包裹的代码,只有当 try 块没有捕获到任何异常时,才会得到执行;反之,如果 try 块捕获到异常,即便调用对应的 except 处理完异常,else 块中的代码也不会得到执行。

举个例子:

try:
    result = 20 / int(input('请输入除数:'))
    print(result)
except ValueError:
    print('必须输入整数')
except ArithmeticError:
    print('算术错误,除数不能为 0')
else:
    print('没有出现异常')
print("继续执行")

可以看到,在原有 try except 的基础上,我们为其添加了 else 块。现在执行该程序:

请输入除数:4
5.0
没有出现异常
继续执行

如上所示,当我们输入正确的数据时,try 块中的程序正常执行,Python 解释器执行完 try 块中的程序之后,会继续执行 else 块中的程序,继而执行后续的程序。

读者可能会问,既然 Python 解释器按照顺序执行代码,那么 else 块有什么存在的必要呢?直接将 else 块中的代码编写在 try except 块的后面,不是一样吗?

当然不一样,现在再次执行上面的代码:

请输入除数:a
必须输入整数
继续执行

可以看到,当我们试图进行非法输入时,程序会发生异常并被 try 捕获,Python 解释器会调用相应的 except 块处理该异常。但是异常处理完毕之后,Python 解释器并没有接着执行 else 块中的代码,而是跳过 else,去执行后续的代码。

也就是说,else 的功能,只有当 try 块捕获到异常时才能显现出来。在这种情况下,else 块中的代码不会得到执行的机会。而如果我们直接把 else 块去掉,将其中的代码编写到 try except 的后面:

try:
    result = 20 / int(input('请输入除数:'))
    print(result)
except ValueError:
    print('必须输入整数')
except ArithmeticError:
    print('算术错误,除数不能为 0')
print('没有出现异常')
print("继续执行")

程序执行结果为:

请输入除数:a
必须输入整数
没有出现异常
继续执行

可以看到,如果不使用 else 块,try 块捕获到异常并通过 except 成功处理,后续所有程序都会依次被执行。

Python try except finally:资源回收

Python 异常处理机制还提供了一个 finally 语句,通常用来为 try 块中的程序做扫尾清理工作。

注意,和 else 语句不同,finally 只要求和 try 搭配使用,而至于该结构中是否包含 except 以及 else,对于 finally 不是必须的(else 必须和 try except 搭配使用)。

在整个异常处理机制中,finally 语句的功能是:无论 try 块是否发生异常,最终都要进入 finally 语句,并执行其中的代码块。

基于 finally 语句的这种特性,在某些情况下,当 try 块中的程序打开了一些物理资源(文件、数据库连接等)时,由于这些资源必须手动回收,而回收工作通常就放在 finally 块中。

Python 垃圾回收机制,只能帮我们回收变量、类对象占用的内存,而无法自动完成类似关闭文件、数据库连接等这些的工作。

读者可能会问,回收这些物理资源,必须使用 finally 块吗?当然不是,但使用 finally 块是比较好的选择。首先,try 块不适合做资源回收工作,因为一旦 try 块中的某行代码发生异常,则其后续的代码将不会得到执行;其次 except 和 else 也不适合,它们都可能不会得到执行。而 finally 块中的代码,无论 try 块是否发生异常,该块中的代码都会被执行。

举个例子:

try:
    a = int(input("请输入 a 的值:"))
    print(20/a)
except:
    print("发生异常!")
else:
    print("执行 else 块中的代码")   
finally :
    print("执行 finally 块中的代码")

运行此程序:

请输入 a 的值:4
5.0
执行 else 块中的代码
执行 finally 块中的代码

可以看到,当 try 块中代码为发生异常时,except 块不会执行,else 块和 finally 块中的代码会被执行。

再次运行程序:

请输入 a 的值:a
发生异常!
执行 finally 块中的代码

可以看到,当 try 块中代码发生异常时,except 块得到执行,而 else 块中的代码将不执行,finally 块中的代码仍然会被执行。

finally 块的强大还远不止此,即便当 try 块发生异常,且没有合适和 except 处理异常时,finally 块中的代码也会得到执行。例如:

try:
    #发生异常
    print(20/0)
finally :
    print("执行 finally 块中的代码")

程序执行结果为:

执行 finally 块中的代码
Traceback (most recent call last):
File “D:\python3.6\1.py”, line 3, in
print(20/0)
ZeroDivisionError: division by zero

可以看到,当 try 块中代码发生异常,导致程序崩溃时,在崩溃前 Python 解释器也会执行 finally 块中的代码。

Python异常处理机制结构详解

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

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

try:
#业务实现代码
except Exception1 as e:
#异常处理块1

except Exception2 as e:
#异常处理块2

#可以有多个 except

else:
#正常处理块
finally :
#资源回收块

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

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

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

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

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

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

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

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

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

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

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

False

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

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

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

Python logging模块用法快速攻略

无论使用哪种编程语言,最常用的调试代码的方式是:使用输出语句(比如 C 语言中使用 printf,Python 中使用 print() 函数)输出程序运行过程中一些关键的变量的值,查看它们的值是否正确,从而找到出错的地方。这种调试方法最大的缺点是,当找到问题所在之后,需要再将用于调试的输出语句删掉。

在 Python 中,有一种比频繁使用 print() 调试程序更简便的方法,就是使用 logging 模块,该模块可以很容易地创建自定义的消息记录,这些日志消息将描述程序执行何时到达日志函数调用,并列出指定的任何变量当时的值。

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

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

读者不需要关心这两行代码的具体工作原理,但基本上,当 Python 记录一个事件的日志时,它会创建一个 LogRecord 对象,保存关于该事件的信息。

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

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 = 1
    for i in range(n + 1):
        total *= i
        logging.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 的值都是错的。日志消息提供了可以追踪的痕迹,帮助我们弄清楚程序运行过程哪里不对。

将代码行 for i in range(n + 1):改为 for i in range(1,n + 1):,再次运行程序,输出结果为:

2019-09-11 14:21:18,047 - DEBUG - Start of program
2019-09-11 14:21:18,067 - DEBUG - Start of factorial(5%)
2019-09-11 14:21:18,072 - DEBUG - i is 1, total is 1
2019-09-11 14:21:18,082 - DEBUG - i is 2, total is 2
2019-09-11 14:21:18,087 - DEBUG - i is 3, total is 6
2019-09-11 14:21:18,093 - DEBUG - i is 4, total is 24
2019-09-11 14:21:18,101 - DEBUG - i is 5, total is 120
2019-09-11 14:21:18,106 - DEBUG - End of factorial(5%)
120
2019-09-11 14:21:18,123 - DEBUG - End of program

Python logging日志级别

“日志级别”提供了一种方式,按重要性对日志消息进行分类。5 个日志级别如表 1 所示,从最不重要到最重要。利用不同的日志函数,消息可以按某个级别记入日志。

级别对应的函数描述
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,这将只显示 ERROR 和 CRITICAL 消息,跳过 DEBUG、INFO 和 WARNING 消息。

Python logging禁用日志

在调试完程序后,可能并不希望所有这些日志消息出现在屏幕上,这时就可以使用 logging.disable() 函数禁用这些日志消息,从而不必进入到程序中,手工删除所有的日志调用。

logging.disable() 函数的用法是,向其传入一个日志级别,它会禁止该级别以及更低级别的所有日志消息。因此,如果想要禁用所有日志,只要在程序中添加 logging.disable(logging.CRITICAL) 即可,例如:

>>> import logging
>>> logging.basicConfig(level=logging.INFO, format=’ %(asctime)s - %(levelname)s - %(message)s’)
>>> logging.critical(‘Critical error! Critical error!’)
2019-09-11 14:42:14,833 - CRITICAL - Critical error! Critical error!
>>> logging.disable(logging.CRITICAL)
>>> logging.critical(‘Critical error! Critical error!’)
>>> logging.error(‘Error! Error!’)

因为 logging.disable() 将禁用它之后的所有消息,所以可以将其添加到程序中更接近 import logging 的位置,这样更容易找到它,方便根据需要注释掉它,或取消注释,从而启用或禁用日志消息。

将日志消息输出到文件中

虽然日志消息很有用,但它们可能塞满屏幕,让你很难读到程序的输出。考虑到这种情况,可以将日志信息写入到文件,既能使屏幕保持干净,又能保存信息,一举两得。

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

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

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

Python assert调试程序

前面章节介绍了如何使用 IDLE 自身的调试工具调试程序,除此之外,Python 还提供了 assert 语句,也可以用来调试程序。

《Python assert断言》一节中,已经对 assert 的基本用法做了简单介绍,assert 语句的完整语法格式为:

assert 条件表达式 [,描述信息]

assert 语句的作用是:当条件表达式的值为真时,该语句什么也不做,程序正常运行;反之,若条件表达式的值为假,则 assert 会抛出 AssertionError 异常。其中,[,描述信息] 作为可选参数,用于对条件表达式可能产生的异常进行描述。

例如:

s_age = input("请输入您的年龄:")
age = int(s_age)
assert 20 < age < 80 , "年龄不在 20-80 之间"
print("您输入的年龄在20和80之间")

程序运行结果为:

请输入您的年龄:10
Traceback (most recent call last):
File “C:\Users\mengma\Desktop\1.py”, line 3, in
assert 20 < age < 80 , “年龄不在 20-80 之间”
AssertionError: 年龄不在 20-80 之间

通过运行结果可以看出,当 assert 中条件表达式的值为假时,程序将抛出异常,并附带异常的描述性信息,与此同时,程序立即停止执行。

通常情况下,assert 可以和 try except 异常处理语句配合使用,以前面代码为例:

try:
    s_age = input("请输入您的年龄:")
    age = int(s_age)
    assert 20 < age < 80 , "年龄不在 20-80 之间"
    print("您输入的年龄在20和80之间")
except AssertionError as e:
    print("输入年龄不正确",e)

程序运行结果为:

请输入您的年龄:10
输入年龄不正确 年龄不在 20-80 之间

通过在程序的适当位置,使用 assert 语句判断变量或表达式的值,可以起到调试代码的作用。

当在命令行模式运行 Python 程序时,传入 -O(注意是大写)参数,可以禁用程序中包含的 assert 语句。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【Python学习教程】Python异常处理机制 的相关文章

随机推荐