[Python学习笔记]调试

编码占了编程工作量的90%,调试占了另外90%,这是一个流传着的笑话。调试在编程中占有很大的分量,即使专业的程序员也一直在制造缺陷。

抛出异常

抛出异常相当于是说:“停止运行这个函数中的代码,将程序执行转到except 语句”。抛出异常使用raise 语句,在代码中,raise 语句包含以下部分:

  • raise 关键字;
  • 对Exception 函数的调用;
  • 传递给Exception 函数的字符串,包含有用的出错信息。

通常是调用该函数的代码知道如何处理异常,而不是该函数本身。所以常常会看到raise 语句在一个函数中,try 和except 语句在调用该函数的代码中。例如:

def boxPrint(symbol, width, height):
    if len(symbol) != 1:
        raise Exception('symbol must be a single character string.')
    if width <= 2:
        raise Exception('Width must be greater than 2.')
    print(symbol * width)
    for i in range(height - 2):
        print(symbol + (' ' * (width - 2)) + symbol)
    print(symbol * width)


for sym, w, h in (('*', 4, 4), ('0', 20, 5), ('x', 1, 3), ('ZZ', 3, 3)):
    try:
        boxPrint(sym, w, h)
    except Exception as err:
        print('An exception happened: ' + str(err))

这个代码的功能是调用一个名为boxPint()的函数,该函数的功能是打印一个盒子,其使用的字符为symbol ,形状为width * height 。如果出现参数不合法,则抛出异常,运行上面的代码,其输出如下:

****
*  *
*  *
****
00000000000000000000
0                  0
0                  0
0                  0
00000000000000000000
An exception happened: Width must be greater than 2.
An exception happened: symbol must be a single character string.

取得反向跟踪的字符串

Python 遇到错误时产生的错误信息称为“反向跟踪”。反向跟踪包含了出错消息、导致该错误的代码行号和导致该错误的函数调用的序列。这个序列称为“调用栈”。例如:

def spam():
    bacon()


def bacon():
    raise Exception('This is an err message.')


spam()

运行上面的代码,输出如下:

Traceback (most recent call last):
  File "f:\Learn\Automate-the-Boring-Stuff-with-Python-Solutions\p10_debug\temp_pracitce.py", line 9, in <module>
    spam()
  File "f:\Learn\Automate-the-Boring-Stuff-with-Python-Solutions\p10_debug\temp_pracitce.py", line 2, in spam
    bacon()
  File "f:\Learn\Automate-the-Boring-Stuff-with-Python-Solutions\p10_debug\temp_pracitce.py", line 6, in bacon
    raise Exception('This is an err message.')
Exception: This is an err message.

通过反向追踪,可以看到该错误发生在第6行,在bacon() 函数中。这次特定的bacon() 调用来自第二行,在spam() 函数中,它又是在第9行被调用的。

如果我们想让程序在出现异常时继续运行,则可以将反向跟踪信息写入一个日志文件,并让程序继续运行。稍后,在准备调试程序时,再检查该日志文件。

import traceback

try:
    raise Exception("This is an err message.")
except Exception:
    errFile = open('errInfo.txt', 'w')
    errFile.write(traceback.format_exc())
    errFile.close()
    print('The traceback info was written the errInfo.txt.')

检查该日志文件内容:

Traceback (most recent call last):
  File "f:\Learn\Automate-the-Boring-Stuff-with-Python-Solutions\p10_debug\temp_pracitce.py", line 4, in <module>
    raise Exception("This is an err message.")
Exception: This is an err message.

断言

断言保证特定的条件正确,针对的是程序员的错误,而不是用户的错误。对于那些可以恢复的错误(诸如文件没有找到,或用户输入了无效的数据),请抛出异常,而不是用assert 语句检测它。

states = "open"
assert states == "open", "The states must be open."
states = "others"
assert states == "open", "The states must be open."

运行上面的语句,输出如下:

Traceback (most recent call last):
  File "f:\Learn\Automate-the-Boring-Stuff-with-Python-Solutions\p10_debug\temp_pracitce.py", line 4, in <module>
    assert states == "open", "The states must be open."
AssertionError: The states must be open.

在交通灯模拟中使用断言

假定你在编写一个交通信号灯的模拟程序。代表路口信号灯的数据结构是一个字典,以“ns”和”ew“为键,分别表示南北向和东西向的信号灯。这些键的值可以是“yellow”,“green”或“red”其中之一。

fStreet_1 = {'ns': 'green', 'ew': 'red'}
gStreet_5 = {'ns': 'red', 'ew': 'green'}

上面的两个字典中存储了f 街第1路口和g 街第5路口的实时红绿灯数据,假如我们要编写一个函数,它接收一个路口参数,进行红绿灯的切换,该函数如下:

def switchLights(stoplight):
    for key in stoplight.keys():
        if stoplight[key] == 'green':
            stoplight[key] = 'yellow'
        elif stoplight[key] == 'yellow':
            stoplight[key] = 'red'
        elif stoplight[key] == 'red':
            stoplight[key] = 'green'
            
            
switchLights(fStreet_1)

运行这段代码,不会出现任何报错,但是这段代码明显是有问题的,即有可能出现一个方向是绿灯,一个方向是黄灯。为了保证不会出现这种路口失控的情况,我们可以添加一个断言,确保始终有一个路口的信号灯是红灯。我们可以添加下面这个断言:

assert 'red' in stoplight.values(), "Neither light is red! " + str(stoplight)

这样再运行程序就会报错并崩溃:

Traceback (most recent call last):
  File "f:\Learn\Automate-the-Boring-Stuff-with-Python-Solutions\p10_debug\temp_pracitce.py", line 15, in <module>
    switchLights(fStreet_1)
  File "f:\Learn\Automate-the-Boring-Stuff-with-Python-Solutions\p10_debug\temp_pracitce.py", line 13, in switchLights
    assert 'red' in stoplight.values(), "Neither light is red! " + str(stoplight)
AssertionError: Neither light is red! {'ns': 'yellow', 'ew': 'green'}

禁用断言

在运行Python时传入-O 参数,可以禁用断言。

日志

记日志是一种很好的方式,可以理解程序中发生的事,以及事情发生的顺序。Python 的logging 模块使得我们可以很容易创建自定义的消息记录。这些日志消息将描述程序执行何时到达日志函数调用,并列出你指定的任何变量当时的值。另一方面,确实日志信息表明有一部分代码被跳过,从未执行。

使用日志模块

要启用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 = 1
    for i in range(1, 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-08-14 16:09:09,950 - DEBUG - Start of program
 2019-08-14 16:09:09,950 - DEBUG - Start of factorial(5)
 2019-08-14 16:09:09,950 - DEBUG - i is 1, total is 1
 2019-08-14 16:09:09,950 - DEBUG - i is 2, total is 2
 2019-08-14 16:09:09,951 - DEBUG - i is 3, total is 6
 2019-08-14 16:09:09,951 - DEBUG - i is 4, total is 24
 2019-08-14 16:09:09,951 - DEBUG - i is 5, total is 120
 2019-08-14 16:09:09,951 - DEBUG - End of factorial(5)
120
 2019-08-14 16:09:09,952 - DEBUG - End of program

可以看到,logging.debug()在打印消息时,会自动加上时间戳和日志级别。

为什么不使用print() 调试

输入import logging logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s - %(levelname)s - %(message)s')可能有一点不方便。但是这样带来的好处是,只需在顶部加上一句logging.disable(logging.CRITICAL)就可以禁用所有日志输出。而使用print() 调试完在删除时,存在工作量大且可能误删正常print()语句的烦恼。

日志级别

“日志级别”提供了一种方式,按重要性对日志消息进行分类。5个日志级别如下表所示:

级别 日志函数 描述
DEBUG logging.debug() 最低级别。用于小细节。
INFO logging.info() 用于记录程序一般事件,确认一切工作正常。
WARNING logging.warning() 用于表示可能的问题,不会阻止程序工作,但将来可能会。
ERROR logging.error() 用于记录错误,将导致程序做某事失败。
CRITICAL logging.critical() 最高级别,将导致程序崩溃完全停止工作。

当然,日志级别是一种建议,具体使用哪个日志函数,完全由程序员来决定。

logging.basicConfig(level=logging.DEBUG)中的level=logging.DEBUG可以控制日志的最低显示级别,如果在后期你只关注较高级别的消息,就可以设置level=logging.ERROR这样就只会显示ERROR 和CRITICAL 级别的消息了。例如:

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

logging.debug("This is a debug message.")
logging.info("I successfully be used.")
logging.warning("I should be int.")
logging.error("I must greater than 13.")
logging.critical("program crashed.")

运行上面代码,输出如下,可以看到ERROR 以下级别的日志消息都没有被输出了。

2019-08-15 11:25:29,254 - ERROR - I must greater than 13.
2019-08-15 11:25:29,254 - CRITICAL - program crashed.

禁用日志

在调试完成后,如果我们不再需要任何的日志消息,可以在头部添加logging.disable(logging.CRITICAL)这将禁止CRITICAL 及其以下级别的所有日志消息输出,即全部日志消息。通过给logging.disable()传入一个级别参数,将禁止该级别和更低级别的所有日志消息输出。

将日志记录到文件

除了将日志消息显示在屏幕上,还可以将他们写入文本文件。logging.basicConfig()函数接收filename 关键字作为参数,如下:

import logging
logging.basicConfig(filename='projectLog.txt', level=logging.ERROR, format=' %(asctime)s - %(levelname)s - %(message)s')

IDLE的调试器

太简单了,此处不介绍。

[Python学习笔记]系列是我在学习《Python编程快速上手——让繁琐工作自动化(Automate The Boring Stuff With Python)》这本书时的学习笔记。通过自己再手敲一遍概念和代码,方便自己记忆和日后查阅。如果对你有帮助,那就更好了!

posted @ 2019-08-15 11:34  Dereen  阅读(374)  评论(0编辑  收藏  举报