本章的主题为调试手段,这是程序开发必不可少的步骤,也是占用时间最多的环节。在程序员的正常开发工作中,调试工作至少占据1/3的时间,而实际编码工作相对占用实际比较少。因此,无论您是初学者,还是编程兴趣爱好者,调试手段一定要学习。
7.1 异常信息
在前面学习过程中,我们看到过经常出现异常打印,比如像下面的代码。代码中执行了Dic.viewkeys()的方法,但是解释器给出了一个Traceback提示,字典对象dict没有viewkeys()属性。意思很直白,就是字典没有viewkeys()这样的操作,那么我们就可以去确认是否有字母写错了,或者甚至确实没有该方法。顺着这样的思路,我们就能找到问题的原因,亦或者在百度中搜索下面的错误信息,看看是否其他也遇到过类似的错误。我们需要知道,我们正在走的路一定是别人走过的,别人也肯定遇到过我们类似的问题,从而少走弯路,浪费时间。
>>> Dic.viewkeys()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'dict' object has no attribute 'viewkeys'
此外,一旦程序运行过程中遇到这种错误输出,也是在提示我们问题发生的相关信息,以帮助我们定位问题。从下面的Traceback我们可以看到,问题出在tornado-main.py文件的第6行,通过对比代码,我们就能找到出错代码行,从而解决问题。
HTTPServerRequest(protocol='http', host='127.0.0.1:8000', method='GET', uri='/', version='HTTP/1.1', remote_ip='127.0.0.1')
Traceback (most recent call last):
File "C:\Users\tornado\web.py", line 1699, in _execute
result = await result
File "C:\Users\tornado\gen.py", line 191, in wrapper
result = func(*args, **kwargs)
File "F:/05_Github/tornado/tornado-main.py", line 16, in get
raise Exception("Debug")
Exception: Debug
那么,如果我们能够提前预判,可能会存在这样那样的异常,能否通过某种方法来捕获该异常,从而让程序正确的处理遇到的问题。以免程序直接崩溃,导致使用感受差。
接下来,先通过一个代码段来了解如何捕获异常,并进行必要的处理。代码中于Redis数据库建立连接,但是并不知道是否成功,因此需要通过ping()方法进行确认。但是如果连接建立失败了,在ping()的过程中就会抛出异常,那么可以通过try,except进行捕获异常,并进行相应的后处理。比如下面代码,当出现超时错误时,我们记录一个日志打印,并返回一个None给调用者。如若是超时之外的其他错误,则全部记录日志并返回None。这样程序使用者在遇到问题的时候,就能够知道到底是什么原因导致失败,从而及时正确的解决问题。
pool = redis.ConnectionPool(host=ip, port=port, decode_responses=True)
r = redis.Redis(connection_pool=pool)
logging.debug(r)
try:
r.ping()
logging.debug('ping success')
return r
except TimeoutError:
logging.error('redis connection timeout ' + ip)
return None
except:
logging.error("redis don't exist ")
return None
所以,如果我们提前知道某段程序可能会出现异常,那么让该段程序处于try之中。下面的代码段中,当检测到异常时,进行了一些后处理操作,删除了一些临时文件,并记录日志。在下次使用的时候,程序就能够从错误中恢复正常。
user_name = None
try:
s = shelve.open('user_data', writeback=True)
user_name = s['username']
s.close()
except:
path = Path.cwd() #Path('.')
for p in path.glob(f"user_data.*"):
p.unlink()
logging.error("get_user_name, cannot open user_data to get username")
return user_name
7.2 日志调试
日志调试是一个最基本的手段,可以让我们了解程序的执行过程,从而确定程序是否按照我们的预想中运行。一般在程序调试中,我们会通过print添加一些临时打印信息,以便及时确认问题,问题确认后再将其删除。
对于一些程序运行过程中的关键信息,我们可能希望即使程序发布了也要记录。那么,logging模块是一个非常不错的选择,可以分级记录日志,并记录到日志文件中。
7.2.1 print打印
print打印应该是程序调试过程中普遍使用的方法,添加很方便,也没有依赖,并能够及时的输出到控制台上。
try:
mail_server = smtplib.SMTP_SSL('smtp.office365.com')
mail_server.ehlo()
mail_server.starttls()
mail_server.ehlo()
mail_server.login(sender, PASS_WORD)
mail_server.send_message(msg)
mail_server.quit()
print("success")
except smtplib.SMTPRecipientsRefused:
print('邮件发送失败,收件人被拒绝')
except smtplib.SMTPAuthenticationError:
print('邮件发送失败,认证错误')
except smtplib.SMTPSenderRefused:
print('邮件发送失败,发件人被拒绝')
except smtplib.SMTPException:
print('邮件发送失败, ', e.message)
7.2.2 logging模块
logging模块正如其名,是用来记录日志的,可以灵活设置帮助记录程序运行过程或辅助定位问题。支持不同的日志等级,可以很方便的设置级别,使调试版本和发布给别人的版本使用不同的日志级别。可以将日志打印在窗口,也可以保存在日志文件中。还可以自定义日志信息的格式,使其更易于阅读。
我们的程序中将定义如下日志信息格式,及具体使用的方法。通过logging.debug(),logging.info()和logging.error()等不同接口来记录日志信息。我们这里设置的level为logging.INFO级别,因此DEBUG级别的信息不会在日志中出现。当需要调试定位问题时,可以将日志级别修改为DEBUG,则所有日志信息都会被保存。
下面代码是我们程序中使用logging的代码片段。
def repair_core_dump_file(self, dump_file, core_file):
sp = [0, 0, 0, 0]
lr = [0, 0, 0, 0]
pc = [0, 0, 0, 0]
self.get_reg_from_dump_file(dump_file, 13, sp)
self.get_reg_from_dump_file(dump_file, 14, lr)
self.get_reg_from_dump_file(dump_file, 15, pc)
logging.info("sp: 0x{}".format(''.join(sp)))
logging.info("lr: 0x{}".format(''.join(lr)))
logging.info("pc: 0x{}".format(''.join(pc)))
with open(core_file, 'rb') as coreHandle:
content_by_hex = coreHandle.read().hex()
rs = content_by_hex.rfind(''.join(sp))
if rs != -1:
content_by_hex = content_by_hex.replace(''.join(sp) + '0000000000000000',
''.join(sp) + ''.join(lr) + ''.join(pc))
with open('repaired-' + core_file, 'wb') as new_coreHandle:
content_by_binary = binascii.unhexlify(content_by_hex)
new_coreHandle.write(content_by_binary)
logging.info("Position:{} is {}".format(rs, content_by_hex[rs:rs + 8]))
logging.info("Position:{} is {}".format(rs, content_by_hex[rs + 8:rs + 16]))
logging.info("Position:{} is {}".format(rs, content_by_hex[rs + 16:rs + 24]))
logging.info("content_by_hex = {}".format(content_by_hex))
logging.info("SP000 hint count: {}".format(content_by_hex.count(''.join(sp) + '0000000000000000')))
logging.info("sp hint count: {}".format(content_by_hex.count(''.join(sp))))
logging模块也支持将日志信息输出到控制台,需要进行一些必要的配置。
下面再看一段日志文件内容,我们的程序日志文件就像下面这样。
2019-09-03 15:41:42 Tue root INFO login success
2019-09-03 15:41:42 Tue root INFO current_version: v2.0
2019-09-03 15:41:43 Tue root ERROR get_user_name, cannot open user_data to get username
2019-09-03 15:41:43 Tue root ERROR get_user_name, cannot open user_data to get username
2019-09-03 15:41:43 Tue root ERROR get_user_name, cannot open user_data to get username
2019-09-03 15:42:04 Tue root INFO v2.0, maybe server don't running.
2019-09-03 15:42:23 Tue root ERROR get_user_name, cannot open user_data to get username
2019-09-03 15:42:23 Tue root ERROR get_user_name, cannot open user_data to get username
2019-09-03 15:42:23 Tue root ERROR get_user_name, cannot open user_data to get username
7.3 IDE的调试器
在我们编写Python程序的过程中,通常都会使用一定的IDE,比如Pycharm,spyder或者其他类似Python程序开发IDE。都支持集成了调试器,在调试器模式下,我们可以给程序打断点。
如果我们希望程序运行到某行的时候暂停下来,那么可以在该行设置一个断点。同时也支持单步调试,能够看到程序执行过程中每个变量的值的变化,可以说是我们调试程序的终极手段。
由于每个IDE的调试器的具体调试大同小于,这里不针对具体的IDE进行介绍。读者在使用自己的IDE时,可以根据IDE的手册进行必要的学习。
7.4 小试牛刀
在程序开发调试的过程中,可能需要多种调试手段结合着使用。但在程序发布后,我们很难通过控制台打印或者IDE的调试器进行定位问题。那么,最好的选择就是通过日志文件记录程序行为,在问题发生后我们可以通过日志文件确认问题。
如下所示,可以将日志信息记录到日志文件中。程序出现异常后,可以通过日志文件来定位问题发生的原因。
log_name = 'booking-testline.log'
logging.basicConfig(level=logging.INFO, #这里表示INFO级别以上的日志信息会被存入日志文件
format="%(asctime)s %(name)s %(levelname)s %(message)s",
datefmt='%Y-%m-%d %H:%M:%S %a',
filename=log_name,
filemode='a')
logging.debug('ping success') #DEBUG级别的日志信息是不会被存入日志文件
logging.error("redis don't exist ") #ERROR级别的日志信息会被存入日志文件
7.5 本章小结
本章介绍了一些简单的程序调试手段,也是最基本的调试手段。通过添加print打印,查看Traceback信息,记录日志文件和IDE的调试器,多种手段来调试定位程序问题。正如开题提到的,程序的调试过程将占据整个程序开发周期的大多数时间,因此这是一个必备手段。任何程序都不可能一次编写成功,因此调式是必不可少的步骤。
欢迎关注,转发,收藏
Python实用案例编程入门:第一章 Python概述及为什么学Python