Python编程-让繁琐的工作自动化(十)调试

程序员之间流传着一个老笑话:“编码占了编程工作量的90%,调试占了另外90%。”

计算机只会做你告诉它的事情,它不会读懂你的心思,做你想要它做的事情。及时专业的程序员也一直在制造缺陷,如果你的程序有问题,不必感到沮丧,试着调试它。

1.抛出异常

当Python师徒执行无效的代码时,就会抛出异常。抛出异常相当于是:停止运行这个函数中的代码,如果你使用了try-except捕捉异常,那么程序将转到except语句,否则会中断运行并报错。

请看以下代码:


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

def do_boxprint():
    for sb,wd, hg in (('*',4, 4), ('0', 20, 5), ('x', 1, 3), ('ZZ', 3, 3)):
        try:
            boxprint(sb,wd, hg)
        except Exception as err:
           print('An exception happend: ' + str(err))

正常执行的结果是:

****
*  *
*  *
****
00000000000000000000
0                  0
0                  0
0                  0
00000000000000000000
An exception happend: Width must be greater than 2.
An exception happend: Symbol must be a sigle character string.

如果不适用try-except语句,则结果是:

****
*  *
*  *
****
00000000000000000000
0                  0
0                  0
0                  0
00000000000000000000
Traceback (most recent call last):
  File "debug.py", line 28, in <module>
    do_boxprint()
  File "debug.py", line 19, in do_boxprint
    boxprint(sb,wd, hg)
  File "debug.py", line 8, in boxprint
    raise Exception('Width must be greater than 2.')
Exception: Width must be greater than 2.

可见如果不通过try-except捕捉抛出的异常,会直接中断在第一个抛出的异常。

使用try和except语句,你可以更优雅得处理错误,而不是让整个程序崩溃。

2.取得反向跟踪的字符串

import traceback

函数:traceback.format_exc() 

只要抛出的异常没有被处理,Python就会显示反向跟踪,如果你希望取得异常的反向跟踪自妇产,并且也希望expect语句优雅地处理该异常,这个函数就很有用。

#! /usr/bin/python3
import traceback

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

def do_boxprint():
    errorfile = open('err.txt', 'w')
    for sb,wd, hg in (('*',4, 4), ('0', 20, 5), ('x', 1, 3), ('ZZ', 3, 3)):
        try:
            boxprint(sb,wd, hg)
        except:
            #将错误日志记录到日志文件
            errorfile.write(traceback.format_exc())
            
            print('the tracebak info was written to error.txt')
    errorfile.close()

该程序输出结果如下:

写入文件内容如下:

Traceback (most recent call last):
  File "debug.py", line 20, in do_boxprint
    boxprint(sb,wd, hg)
  File "debug.py", line 8, in boxprint
    raise Exception('Width must be greater than 2.')
Exception: Width must be greater than 2.
Traceback (most recent call last):
  File "debug.py", line 20, in do_boxprint
    boxprint(sb,wd, hg)
  File "debug.py", line 6, in boxprint
    raise Exception ('Symbol must be a sigle character string.')
Exception: Symbol must be a sigle character string.


3.断言

断言是一个正常预测的检查,确保代码没有做什么明显错误得事情。这些预测有assert语句执行。如果检查失败,就会抛出异常。在代码中,assert语句包含以下部分

1> assert关键字

2> 条件(即求值为True或False的表达式)

3> 逗号

4>当条件为False时显示的字符串

def testassert():
    a,b=input('input 2 number to do divisor:').split()
    a = int(a)
    b = int(b)
    assert b != 0, 'Divisor cannot be zero'
    try:
        c = a / b
        print('%d / %d = %d' %(a,b,c))
    except:
        print(traceback.format_exc())

程序运行:

input 2 number to do divisor:2 0
Traceback (most recent call last):
  File "debug.py", line 44, in <module>
    testassert()
  File "debug.py", line 32, in testassert
    assert b != 0, 'Divisor cannot be zero'
AssertionError: Divisor cannot be zero

断言针对的是程序员的错误,而不是用户的错误。对于那些可以恢复的错误,诸如文件没有知道,或用户输入了无效的数据集,请抛出异常,而不是用assert语句检测,因为这会直接中断程序。

3.1 使用断言的另一个例子:交通灯

假定你要编写一个交通信号灯的模拟程序,代表路口信号灯的数据是一个字典,以‘ns’和‘ew’为键。分别代表南北和东西的信号灯。这些键的值可以是'green','yellow'或'red‘之一,代码看起来像这样:

market_2nd = {'ns': 'green', 'ew':'red'}
missing_16th = {'ns':'red', 'ew':'green'}

这两个变量将针对market街和第2街路口,以及missing街和第16街路口。作为项目启动,你希望编写一个switchlights()函数,它接受一个路口字典作为参数,并切换红绿灯。开始你认为,switchlights()只需要将每一种灯按顺序切换到下一种颜色:green-> yellow->red->green 这样重复。实际代码类似这样:不带asset语句。

def switchlight(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'
    assert 'red' in stoplight.values(),'Neither light is red!' + str(stoplight)

def trafficlight():
    market_2nd = {'ns': 'green', 'ew':'red'}
    missing_16th = {'ns':'red', 'ew':'green'}
    switchlight(market_2nd)

在实际运行中,你也许不会注意到,当南北向为黄灯时,东西向为绿灯,两个方向都可以通行,这样就发生事故了。你可以在切换完灯以后,用assert检查是否至少一个方向是红灯。如上断言部分。

运行结果:

Traceback (most recent call last):
  File "debug.py", line 59, in <module>
    trafficlight()
  File "debug.py", line 52, in trafficlight
    switchlight(market_2nd)
  File "debug.py", line 47, in switchlight
    assert 'red' in stoplight.values(),'Neither light is red!' + str(stoplight)
AssertionError: Neither light is red!{'ns': 'yellow', 'ew': 'green'}

3.2 禁用断言

在运行Python时传入-O选项,可以禁用断言。如果已经完成了程序的编写和测试,不希望执行多余得到正确预测断言,从而减慢程序的速度,这样就很好(尽管大多数时候断言所耗费的时间,不会让你觉察到速度的差异)断言是针对开发的,不是针对最终产品,当你的程序交给其他人运行时,它应该没有缺陷。

4. 日志

如果你曾经在代码中加入print语句,在程序运行时输出某些变量的值,你就使用了记录日志的方式来调试代码。记录日志是一种很好的方式,可以理解程序中发生的事情,以及事情发生的顺序。Python的logging模块是的你很容易创建自定义的消息记录。这些日志消息将描述程序执行何时到达日志函数调用,并列出你指定的任何变量的值。另一方面,确实日志信息表明有一部分代码被跳过,从未执行。

41. 使用日志模块

要启用logging模块,在程序运行时将日志信息 显示到屏幕上,需要将下面的代码复制到程序顶端(在Python的#!之下)

注意关键字不要写错

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

使用日志的一个例子:

打印中间值

def factorial(n):
    logging.debug('Start of factorial(%d)' %(n))
    total = 1
    for i in range(1,n+1):
        total *= i
        #logging.debug('i = ' + str(i) + 'total = ' + str(total) )
        logging.debug('i = %d total = %d '  %(i, total) )
    logging.debug('End of factorial(%d)' %(n) )
    return total

 2019-07-25 22:57:35,742 - DEBUG - Start of factorial(5)
 2019-07-25 22:57:35,743 - DEBUG - i = 1 total = 1
 2019-07-25 22:57:35,743 - DEBUG - i = 2 total = 2
 2019-07-25 22:57:35,748 - DEBUG - i = 3 total = 6
 2019-07-25 22:57:35,751 - DEBUG - i = 4 total = 24
 2019-07-25 22:57:35,752 - DEBUG - i = 5 total = 120
 2019-07-25 22:57:35,752 - DEBUG - End of factorial(5)
5! = 120

4.2 不要使用print()调试

输入import logging和相关的日志格式配置有点不方便,你可能想使用print调用代替,但不要屈服于这种诱惑。在调试完成后,你需要花很多时间,从代码中清除每条日志消息的print()调用。你甚至可能不小心删除一些print()调用,而他们不是用来产生日志消息的。日志消息的好处在于,你可以随心所欲的再陈旭中想加多少就加多少。稍后只要加入一次logging.disable(logging.CRITICAL) 调用,就可以禁止日志。logging模块在显示和隐藏日志信息之间的切换变得很容易。日志是给程序员用的,不是给用户的。用户不会因为你便于调试,而向看字典的内容。请将日志信息用于类似调试的目的。对于用户希望看到的消息,如“文件未找到”或“无效的输入”这类提示,应该使用print()。

4.3 日志级别

logging模块默认定义了以下几个日志等级,它允许开发人员自定义其他日志级别,但是这是不被推荐的,尤其是在开发供别人使用的库时,因为这会导致日志级别的混乱。

日志等级(level)描述
DEBUG最详细的日志信息,典型应用场景是 问题诊断
INFO信息详细程度仅次于DEBUG,通常只记录关键节点信息,用于确认一切都是按照我们预期的那样进行工作
WARNING当某些不期望的事情发生时记录的信息(如,磁盘可用空间较低),但是此时应用程序还是正常运行的
ERROR由于一个更严重的问题导致某些功能不能正常运行时记录的信息
CRITICAL

当发生严重错误,导致应用程序不能继续运行时记录的信息

 

 

级别何时使用
DEBUG详细信息,典型地调试问题时会感兴趣。 详细的debug信息。
INFO证明事情按预期工作。 关键事件。
WARNING表明发生了一些意外,或者不久的将来会发生问题(如‘磁盘满了’)。软件还是在正常工作。
ERROR由于更严重的问题,软件已不能执行一些功能了。 一般错误消息。
CRITICAL严重错误,表明软件已不能继续运行了。
NOTICE不是错误,但是可能需要处理。普通但是重要的事件。
ALERT需要立即修复,例如系统数据库损坏。
EMERGENCY紧急情况,系统不可用(例如系统崩溃),一般会通知所有用户。

开发应用程序或部署开发环境时,可以使用DEBUG或INFO级别的日志获取尽可能详细的日志信息来进行开发或部署调试;应用上线或部署生产环境时,应该使用WARNING或ERROR或CRITICAL级别的日志来降低机器的I/O压力和提高获取错误日志信息的效率。日志级别的指定通常都是在应用程序的配置文件中进行指定的。

4.4 logging模块定义的模块级别的常用函数

函数说明
logging.debug(msg, *args, **kwargs)创建一条严重级别为DEBUG的日志记录
logging.info(msg, *args, **kwargs)创建一条严重级别为INFO的日志记录
logging.warning(msg, *args, **kwargs)创建一条严重级别为WARNING的日志记录
logging.error(msg, *args, **kwargs)创建一条严重级别为ERROR的日志记录
logging.critical(msg, *args, **kwargs)创建一条严重级别为CRITICAL的日志记录
logging.log(level, *args, **kwargs)创建一条严重级别为level的日志记录
logging.basicConfig(**kwargs)对root logger进行一次性配置

注意,向logging.basicConfig传入日志等级后,只会显示指定等级及以上等级的消息,如设置了等级为ERROR,那么只会显示ERROR和CRITICAL的消息,跳过DEBUG,INFO和WARNING。

4.5 logging.basicConfig()函数包含参数说明

参数名称                描述
filename指定日志输出目标文件的文件名(可以写文件名也可以写文件的完整的绝对路径,写文件名日志放执行文件目录下,写完整路径按照完整路径生成日志文件),指定该设置项后日志信心就不会被输出到控制台了
filemode指定日志文件的打开模式,默认为'a'。需要注意的是,该选项要在filename指定时才有效
format指定日志格式字符串,即指定日志输出时所包含的字段信息以及它们的顺序。logging模块定义的格式字段下面会列出。
datefmt指定日期/时间格式。需要注意的是,该选项要在format中包含时间字段%(asctime)s时才有效
level指定日志器的日志级别
stream指定日志输出目标stream,如sys.stdout、sys.stderr以及网络stream。需要说明的是,stream和filename不能同时提供,否则会引发 ValueError异常
stylePython 3.2中新添加的配置项。指定format格式字符串的风格,可取值为'%'、'{'和'$',默认为'%'
handlersPython 3.3中新添加的配置项。该选项如果被指定,它应该是一个创建了多个Handler的可迭代对象,这些handler将会被添加到root logger。需要说明的是:filename、stream和handlers这三个配置项只能有一个存在,不能同时出现2个或3个,否则会引发ValueError异常。

4.6 logging模块中定义好的可以用于format格式字符串说明

字段/属性名称使用格式描述
asctime%(asctime)s将日志的时间构造成可读的形式,默认情况下是‘2016-02-08 12:00:00,123’精确到毫秒
name%(name)s所使用的日志器名称,默认是'root',因为默认使用的是 rootLogger
filename%(filename)s调用日志输出函数的模块的文件名; pathname的文件名部分,包含文件后缀
funcName%(funcName)s由哪个function发出的log, 调用日志输出函数的函数名
levelname%(levelname)s日志的最终等级(被filter修改后的)
message%(message)s日志信息, 日志记录的文本内容
lineno%(lineno)d当前日志的行号, 调用日志输出函数的语句所在的代码行
levelno%(levelno)s该日志记录的数字形式的日志级别(10, 20, 30, 40, 50)
pathname%(pathname)s完整路径 ,调用日志输出函数的模块的完整路径名,可能没有
process%(process)s当前进程, 进程ID。可能没有
processName%(processName)s进程名称,Python 3.1新增
thread%(thread)s当前线程, 线程ID。可能没有
threadName%(thread)s线程名称
module%(module)s调用日志输出函数的模块名, filename的名称部分,不包含后缀即不包含文件后缀的文件名
created%(created)f当前时间,用UNIX标准的表示时间的浮点数表示; 日志事件发生的时间--时间戳,就是当时调用time.time()函数返回的值
relativeCreated%(relativeCreated)d输出日志信息时的,自Logger创建以 来的毫秒数; 日志事件发生的时间相对于logging模块加载时间的相对毫秒数
msecs%(msecs)d日志事件发生事件的毫秒部分。logging.basicConfig()中用了参数datefmt,将会去掉asctime中产生的毫秒部分,可以用这个加上

 

4.7 禁用日志

如果想要禁用日志,只用在程序中添加 logging.disable(logging.CRITICAL)。因为logging.disable()将禁用它之后的所有消息,因此你可能希望将它添加到接近import logging 代码行的位置类似这样

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

4.8 将日志记录到文件

除了将日志消息显示在屏幕上,还可以将它们写入文本文件。

如下配置,设置日志写入的文件路径及文件名

import logging,os
'''
#简单版日志
logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s - %(levelname)s - %(message)s')
'''
LOG_FORMAT = "%(asctime)s %(name)s %(levelname)s %(pathname)s %(message)s "#配置输出日志格式
DATE_FORMAT = '%Y-%m-%d  %H:%M:%S %a ' #配置输出时间的格式,注意月份和天数不要搞乱了
LOG_PATH = os.path.join(os.getcwd(),'debugging.log')
logging.basicConfig(level=logging.DEBUG,
                    format=LOG_FORMAT,
                    datefmt = DATE_FORMAT ,
                    filename=LOG_PATH #有了filename参数就不会直接输出显示到控制台,而是直接写入文件
                    )

使用以上格式,前面的阶乘的logging就记录到了文件

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值