程序员之间流传着一个老笑话:“编码占了编程工作量的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 异常 |
style | Python 3.2中新添加的配置项。指定format格式字符串的风格,可取值为'%'、'{'和'$',默认为'%' |
handlers | Python 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就记录到了文件