Python入门之程序bug的产生及调试程序

7.4 调试程序

7.4.1 bug是什么

通过上面的学习我们知道,写好Python代码无法通过解释器的是错误,而在通过了解释器运行时遇到的问题叫异常。
对于错误,我们只要根据解释器给出的提示进行修改即可。这类的错误一般都比较简单,很容易就修复。
而对于异常,我们可以通过Python反馈的异常信息进行处理,要么规避掉程序报错,要么通过捕获异常,再进行处理。
Python程序在解释和运行时除了会遇到错误和异常外还会出现bug。所谓bug,是指在程序运行中因为程序本身有问题或设计有缺陷而造成的功能不正常、死机、数据丢失、非正常中断、无法达到预期等现象。
早期的计算机由于体积非常庞大,有些小虫子可能会钻入机器内部,造成计算机工作失灵。史上的第一只Bug,真的是因为一只飞蛾意外走入一电脑而引致故障,因此Bug从原意为臭虫引申为程序错误。
比如,让某个用户输入一个数字当成年龄并做运算,用户可能会输入一个越界的年龄(超过1000岁),此时你的程序可能就会出现bug。
在网上有个非常流行的段子,用来描述如果涉及用户操作的程序是多么容易出现bug:

一个顾客走进一家酒吧,要了一杯啤酒
一个顾客走进一家酒吧,要了一杯咖啡
一个顾客走进一家酒吧,要了0.7杯啤酒
一个顾客走进一家酒吧,要了-1杯啤酒
一个顾客走进一家酒吧,要了2^32杯啤酒
一个顾客走进一家酒吧,要了一杯洗脚水
一个顾客走进一家酒吧,要了一杯蜥蜴
一个顾客走进一家酒吧,要了一份asdfQwer@24dg!&*(@
一个顾客走进一家酒吧,什么也没要
一个顾客走进一家酒吧,又走出去又从窗户进来又从后门出去从下水道钻进来
一个顾客走进一家酒吧,又走出去又进来又出去又进来又出去,最后在外面把老板打了一顿
一个顾客走进一家酒吧,要了一杯烫烫烫的锟斤拷
一个顾客走进一家酒吧,要了NaN杯Null
一万个顾客冲进一家酒吧,要了500万瓶啤酒、咖啡、洗脚水、野猫、狼牙棒、奶茶
一万个顾客走进一家酒吧,然后把酒吧拆了
一个顾客化妆成老板走进一家酒吧,要了500杯啤酒,并且不付钱
一万个顾客在酒吧外呼啸而过
一个顾客走进一家酒吧,点了一份老板,结果酒吧炸了

当我们编写的程序出现bug时,就需要进行调试了。

7.4.2 测试和调试

程序测试(program testing)是指对一个完成了全部或部分功能、模块的计算机程序在正式使用前的检测,以确保该程序能按预定的方式正确地运行。
程序的正确性尚未得到根本地解决,程序测试仍是发现程序错误和缺陷的主要手段。为了发现程序中的错误,应竭力设计能暴露错误的测试用例。
测试用例是由测试数据和预期结果构成的。一个好的测试用例是极有可能发现目前为止尚未发现的错误。
程序调试是将编写的程序投入实际运行前,用手工或编译程序等方法进行测试,修正在测试阶段发现的语法错误和逻辑错误的过程。这是保证程序正确运行的必不可少的步骤。编写完计算机程序,必须进行程序测试。根据测试时所发现的错误,进一步诊断,找出原因和具体的位置进行修正。

7.4.3 print(pprint)调试

print函数应该算是Python开发者最常用也是最简单的一种调试手段了。一般调试是想要得知某个变量在运行到某处时的状态,这个时候用print函数进行输出是一种非常简单、方便的方式。
一般就是在某行代码之后加上一句print(变量)。或者在用if判断为真时print(变量)。
这种调试的方式比较简单,就不多做介绍了。
print调试的优点在于简单方便,但是缺点也很简单,那就是如果调试的内容比较多,会在代码的多处留下print语句,最后调试完之后还得一行行地删除。典型的用时方便,清理时难受。
pprint类似print,不过pprint可以使得打印的内容更加漂亮且直观。通过下面的代码可以看到这两者的区别。

import sys
import pprint

print(sys.path)
print('-' * 50)
pprint.pprint(sys.path)

[‘D:\Soft_Installed\PyCharm_2021.3.3_pro\plugins\python\helpers\pycharm_display’, ‘C:\Program Files\Python3102\python310.zip’, ‘C:\Program Files\Python3102\DLLs’, ‘C:\Program Files\Python3102\lib’, ‘C:\Program Files\Python3102’, ‘C:\Users\思必得\AppData\Roaming\Python\Python310\site-packages’, ‘C:\Program Files\Python3102\lib\site-packages’, ‘C:\Program Files\Python3102\lib\site-packages\win32’, ‘C:\Program Files\Python3102\lib\site-packages\win32\lib’, ‘C:\Program Files\Python3102\lib\site-packages\Pythonwin’, ‘D:\Soft_Installed\PyCharm_2021.3.3_pro\plugins\python\helpers\pycharm_matplotlib_backend’]
[‘D:\Soft_Installed\PyCharm_2021.3.3_pro\plugins\python\helpers\pycharm_display’,
‘C:\Program Files\Python3102\python310.zip’,
‘C:\Program Files\Python3102\DLLs’,
‘C:\Program Files\Python3102\lib’,
‘C:\Program Files\Python3102’,
‘C:\Users\思必得\AppData\Roaming\Python\Python310\site-packages’,
‘C:\Program Files\Python3102\lib\site-packages’,
‘C:\Program Files\Python3102\lib\site-packages\win32’,
‘C:\Program Files\Python3102\lib\site-packages\win32\lib’,
‘C:\Program Files\Python3102\lib\site-packages\Pythonwin’,
‘D:\Soft_Installed\PyCharm_2021.3.3_pro\plugins\python\helpers\pycharm_matplotlib_backend’]

7.4.4 icecream调试

print调试虽然简单方便,但是每次都只会print我们给定的变量的内容,变量的其它信息不能直接print出来。如下:

num1 = 2
num2 = 4
num3 = 8

print(num3, num1, num2)

8 2 4

这里输出的内容如果不参照前面的变量名以及print语句就分辨不出对应的变量的值是多少。print语句越多越容易出现这种问题。虽然我们可以在print语句中添加字符串说明来解决,但是这么做就变得麻烦起来了。
icecream库就可以将这些变得简单,icecream相当于一个加强版的print函数。接下来让我们一起学习icecream的用法吧。
安装icecream库
pip install icecream
icecream打印变量

from icecream import ic
num1 = 2
num2 = 4
num3 = 8

ic(num3, num1, num2)

ic| num3: 8, num1: 2, num2: 4

from icecream import ic

for i in range(2):
    for j in range(2):
        print(f'i:{
      
      i}\t\tj:{
      
      j}')
        ic(i, j)

i:0 j:0
i:0 j:1
i:1 j:0
i:1 j:1
ic| i: 0, j: 0
ic| i: 0, j: 1
ic| i: 1, j: 0
ic| i: 1, j: 1

如上,用ic取代print进行输出,我们可以很清楚地看到变量名以及变量的值。并且在IDE中,输出的变量的值是有颜色的,这样就一眼可以看清变量的值了。

icecream打印函数

from icecream import ic


def exp(n):
    return n ** 2


print(exp(3))
print(exp(5))
ic(exp(3))
ic(exp(5))

9
25
ic| exp(3): 9
ic| exp(5): 25

我们可以对比查看print和ic用以打印函数,可以看到print语句只打印了函数的返回值,而ic不但打印了函数的值,还能看到函数名以及参数的取值。同上面打印变量一样,函数的参数以及函数的返回值在IDE中用彩色进行突出显示了。

icecream检查代码执行位置
很多时候我们在调试代码打印一些关键位置结果时,希望可以快速找到对应结果在代码中的位置,利用ic(),不传递任何参数时,会自动打印出所在位置、所属父级函数等信息:

from icecream import ic


def mf():
    for i in range(2):
        for j in range(2):
            # print(f'i:{i}\t\tj:{j}')
            if i == 1 and j == 1:
                ic()


mf()

ic| main.py:9 in mf() at 10:50:36.926

在上面打印的信息中,我们可以看到模块名、代码所在行数、函数名以及执行的时间。从而让我们可以对程序的执行有个更清楚的认识。

自定义前缀
在上面icecream的输出内容中可以很清楚的看到每次的输出都会自带一个前缀,那就是ic| ,这个也就是icecream.py源码中的DEFAULT_PREFIX常量代表的值。有时候我们会特别关心运行的时间,或者有别的需求,需要修改前缀的,那么我们就可以通过修改这个常量。
前缀修改成别的常量比较简单,直接修改DEFAULT_PREFIX即可。当然如果是要修改成带上运行的时间或者一个变量则可以如下操作:

from icecream import ic


def time_format():
import time
    return f'{
      
       time.strftime("%H:%M:%S", time.localtime())}|> '


ic.configureOutput(prefix=time_format)

for i in range(2):
    time.sleep(1)
    ic(i)

11:08:35|> i: 0
11:08:36|> i: 1

获得更多的上下文信息
除了了解负责输出的代码之外,你可能还想知道代码执行的行和文件来自哪个行。要了解代码的上下文,只需要将includeecontext = True添加到ic.configureOutput ()中即可。

from icecream import ic


def exp(n):
    return n ** 2


ic(exp(3))
ic.configureOutput(includeContext=True)
ic(exp(5))

ic| exp(3): 9
ic| test1.py:10 in - exp(5): 25

7.4.5 PySnooper调试函数

PySnooper是一个比print更加强大的调试第三方库,用来对函数进行调试。
PySnooper的安装
pip install PySnooper
PySnooper调试的简单示例1

import pysnooper


@pysnooper.snoop()
def myfunc():
    lst = [1, 2, 3]
    lst.extend([0, 0])
    print(lst)
    lst.pop()
    print(lst)

    return lst


myfunc()

[1, 2, 3, 0, 0]
[1, 2, 3, 0]
Source path:… E:\BaiduNetdiskWorkspace\FrbPythonFiles\Study\main.py
13:21:12.872073 call 5 def myfunc():
13:21:12.872073 line 6 lst = [1, 2, 3]
New var:… lst = [1, 2, 3]
13:21:12.872073 line 7 lst.extend([0, 0])
Modified var:… lst = [1, 2, 3, 0, 0]
13:21:12.872073 line 8 print(lst)
13:21:12.872073 line 9 lst.pop()
Modified var:… lst = [1, 2, 3, 0]
13:21:12.872073 line 10 print(lst)
13:21:12.872073 line 12 return lst
13:21:12.872073 return 12 return lst
Return value:… [1, 2, 3, 0]
Elapsed time: 00:00:00.000000

pysnooper的用法很简单,就是在要进行调试的函数前面添加一个装饰器: pysnooper.snoop()用于监视函数的执行情况。我们可以通过上面的打印情况看到:
pysnooper可以指出每一行代码的行号,是什么操作:call、line、return
pysnooper会指出每个变量的产生、修改等操作,以及值是多少
pysnooper会显示函数的返回值以及函数的执行时长

PySnooper调试的简单示例2

import pysnooper


@pysnooper.snoop()
def myfunc():
    for i in range(2):
        for j in range(2):
            ...


myfunc()

Source path:… E:\BaiduNetdiskWorkspace\FrbPythonFiles\Study\test1.py
13:27:40.672240 call 5 def myfunc():
13:27:40.673240 line 6 for i in range(2):
New var:… i = 0
13:27:40.673240 line 7 for j in range(2):
New var:… j = 0
13:27:40.673240 line 8 …
13:27:40.673240 line 7 for j in range(2):
Modified var:… j = 1
13:27:40.673240 line 8 …
13:27:40.673240 line 7 for j in range(2):
13:27:40.673240 line 6 for i in range(2):
Modified var:… i = 1
13:27:40.673240 line 7 for j in range(2):
Modified var:… j = 0
13:27:40.673240 line 8 …
13:27:40.673240 line 7 for j in range(2):
Modified var:… j = 1
13:27:40.673240 line 8 …
13:27:40.674239 line 7 for j in range(2):
13:27:40.674239 line 6 for i in range(2):
13:27:40.674239 return 6 for i in range(2):
Return value:… None
Elapsed time: 00:00:00.002998

PySnooper的其它更加强大功能及详细使用方法参照:
PySnooper

7.4.6 objprint调试自定义对象

objprint是一个第三方库,这个第三方库主要是用来弥补系统的自带print函数对于自定义对象的调试不友好而开发的。
objprint的安装
pip install objprint
objprint打印自定义对象属性

from objprint import op


class Person:
    gender = 'man'

    def __init__(self):
        self.name = '张三'
        self.age = 20
        self.gfs = ['赵静', '王玲']
        self.scores = {
    
    '语文': 30, '数学': 50}

    def say(self):
        print('这边鱼塘被我承包了')


zs = Person()
op(zs)

<Person 0x1315b2323b0
.age = 20,
.gfs = [‘赵静’, ‘王玲’],
.name = ‘张三’,
.scores = {‘数学’: 50, ‘语文’: 30}

通过上面op我们可以看到打印了实例对象的所有属性。

7.4.7 logging日志

logging模块是Python的内置库,可以很方便地记录程序在运行中的日志。上述介绍的所有的调试库,比如print、icecream等,都有着其缺点:
1、如果要查看变量的值,都必须在代码中添加输出代码实现,并且每次在运行程序后都会进行输出。
2、不能对要输出的内容进行分级。
Python自带的logging日志模块可以很好地解决这2个问题。logging模块就是用来进行日志的,它将日志分为5个级别,分别是:
debug、info、warning、error以及critical。
从左到右表示的级别越来越高。
debug:10级,调试级别,一般是用来输出内容方便开发者进行调试的。
info:20级,信息级别,一般用来输出信息给用户查看的。
warning:30级,警告级别,一般用来输出警告信息给用户查看的。
error:40级,错误级别,一般用来输出程序中的错误信息供开发者查看的。
critical:50级,严重错误级别,一般是表示程序在运行时遇到了严重的错误,面临崩溃的边缘。
开发者可以通过修改日志输出级别从而过滤掉不需要的日志。如果将日志输出级别调整成info级别,那么程序就只会输出info及以上级别的日志,而debug级别的日志则不会输出。通过此种方法就可以灵活地输出想要的日志了。
输出日志到控制台

import logging

logger = logging.getLogger(__name__)
logger.info("Start print log")
logger.debug("Do something")
logger.warning("Something maybe fail.")
logger.info("Finish")
logger.critical('critical')
logger.error('err')

Something maybe fail.
critical
err

如上输出结果,我们对照的话可以看到warning、error以及critical级别的日志都输出了,但是debug以及info级别的日志却没有输出。并且输出内容前面并没有日志级别的标识,不方便查看,接下来我们对代码进行改造。
其实只要对logging的参数进行修改:
输出指定级别的日志

import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
logger.info("Start print log")
logger.debug("Do something")
logger.warning("Something maybe fail.")
logger.info("Finish")
logger.critical('critical')
logger.error('err')

INFO:main:Start print log
WARNING:main:Something maybe fail.
INFO:main:Finish
CRITICAL:main:critical
ERROR:main:err

如上,我们看到在输出的内容前面有对应日志的级别了。并且,通过设置日志的level,可以查看到指定级别的日志。
不过,日志的内容还是不够详细,因为只能看到内容和级别,一般我们比较关心日志输出的时间,那么就可以进行以下改造:
输出带时间的日志

import logging

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s: %(message)s')
logger = logging.getLogger(__name__)
logger.info("Start print log")
logger.debug("Do something")
logger.warning("Something maybe fail.")
logger.info("Finish")
logger.critical('critical')
logger.error('err')

2022-10-02 11:30:09,692 - INFO: Start print log
2022-10-02 11:30:09,692 - WARNING: Something maybe fail.
2022-10-02 11:30:09,692 - INFO: Finish
2022-10-02 11:30:09,692 - CRITICAL: critical
2022-10-02 11:30:09,692 - ERROR: err

通过对输出内容进行格式化,就可以得到我们想要的输出的样式了。其中logging.basicConfig中参数format可以设置输出格式。%(asctime)s代表的就是系统时间。%(message)s代表的是日志内容。
其它样式如下:
logging.basicConfig函数format参数说明:
%(levelno)s: 打印日志级别的数值
%(levelname)s: 打印日志级别的名称
%(pathname)s: 打印当前执行程序的路径,其实就是sys.argv[0]
%(filename)s: 打印当前执行程序名
%(funcName)s: 打印日志的当前函数
%(lineno)d: 打印日志的当前行号
%(asctime)s: 打印日志的时间
%(thread)d: 打印线程ID
%(threadName)s: 打印线程名称
%(process)d: 打印进程ID
%(message)s: 打印日志信息

程序在调试或者开发的时候我们除了需要在控制台输出日志等信息,有时候还需要将日志进行持久化处理,即将日志以文件形式保存起来以便日后可以对程序进行后续的追踪及分析。
可以通过修改logging.basicConfig函数中的filename参数指定日志保存的文件名。
将日志输出到文件中

import logging

logging.basicConfig(filename='log.txt')
logger = logging.getLogger(__name__)
logger.info("Start print log")
logger.debug("Do something")
logger.warning("Something maybe fail.")
logger.info("Finish")
logger.critical('critical')
logger.error('err')

我们在上面指定了filename=‘log.txt’,这样输出的日志内容就全都会添加到指定的log.txt中了。
logging.basicConfig函数除了filename参数和format参数外,还有其它的参数,如下:
logging.basicConfig函数各参数:
filename:指定日志文件名;
filemode:和open函数意义相同,指定日志文件的打开模式,‘w’或者’a’;
format:指定输出的格式和内容;
datefmt:指定时间格式,同time.strftime();
level:设置日志级别,默认为logging.WARNNING;
stream:指定日志的输出流,可以指定输出到sys.stderr,sys.stdout或者文件,默认输出到sys.stderr,当stream和filename同时指定时,stream被忽略;

7.4.8 IDE(Pycharm)调试

一般IDE工具自带调试功能。通过IDE自带的调试功能也可以对程序进行调试。以下调试的讲解以Pycharm这款IDE示例。
1、在用IDE进行调试代码时,会在要查看的代码的左侧进行点击,添加断点(可添加多个),这样就会在代码的左侧出现一个红色的圆圈,代表该行代码是一个断点:

2、通过右键中的菜单调试或者点击Pycharm右上方一只小爬虫图标即可进入调试模式。此时会执行代码并在第一次运行到第一个断点处暂停。此时我们可以在代码处看到一些变量的值:

3、在Pycharm的下方也会自动打开调试选项卡,左边为调试器和控制台,右边为变量:

4、我们可以在右边的变量处查看目前所有变量的值。也可以点击+添加监视。

5、如果想要继续执行代码,可以点击在控制台右边的那一排按钮,比如:步过、步入等,也可以评估表达式。

6、切换到控制台时,还可以在控制台中输入相应的Python代码,此时输入并运行的代码是建立在当前程序运行环境中的。比如此时n==10,那么我们输入print(2*n)则会返回20而不会报无法识别变量n。

好了,Pycharm的简单的调试功能就介绍到这里,Pycharm中调试的其它功能大家可以慢慢摸索。

猜你喜欢

转载自blog.csdn.net/crleep/article/details/129764716