调试
程序总会有各种各样的bug需要修正。有的bug很简单,看看异常信息就知道,有的bug很复杂,我们需要知道出错时,哪些变量的值是正确的,哪些变量的值是异常的,因此,需要一整套调试程序的手段来修复bug。
最简单的调试方法就是用print()
函数打印出可能出问题的变量,然后在输出中查看变量值:
n = 0
print('n = %d' % n)
ans = 10 / n
# 运行结果
# n = 0
# Traceback (most recent call last):
...
# ZeroDivisionError: integer division or modulo by zero
使用print()
的缺点是调试完后需要删掉它,而且频繁地print()
,会导致运行结果包含很多垃圾信息。
断言
代码中print()
来辅助查看的地方,都可以用断言(assert
)来替代:
n = 0
assert n != 0, 'n is zero!'
ans = 10 / n
assert
语法:assert 判断条件 提示信息
:
- 判断条件为
True
时,继续执行后续代码 - 判断条件为
False
时,抛出AssertionError
,并输出提示信息:AssertionError: n is zero!
大量的assert
也会造成结果混乱,但是可以在启动Python解释器时,传入-O
参数来关闭assert
,关闭后assert
语句可以视为pass
语句:
Traceback (most recent call last):
...
ZeroDivisionError: division by zero
logging
使用logging不会抛出异常,并且可以输出到文件:
import logging
logging.basicConfig(level=logging.INFO)
s = '0'
n = int(s)
logging.info('n = %d' % n)
print(10 / n)
输出结果:
INFO:root:n = 0
Traceback (most recent call last):
File "err.py", line 8, in <module>
print(10 / n)
ZeroDivisionError: division by zero
logging允许指定记录信息的级别:
级别排序:CRITICAL > ERROR > WARNING > INFO > DEBUG
通过logging.basicConfig(level=logging.XX)
可以设置日志输出的最低等级,可以方便输出不同级别的信息,不用删除logging
语句,最后统一控制输出哪个级别的信息。
logging
通过简单的配置,一条语句可以同时输出到不同的地方,比如console和文件。
pdb
启动Python的调试器pdb,可以让程序以单步方式运行,随时查看运行状态,启动pdb的bash语句为:python -m pdb xxx.py
。
pdb操作:
- pdb启动后,会自动定位到下一步要执行的代码:
-> s = '0'
- 输入指令
l
可以查看整体代码 - 输入指令
n
可以单步执行代码 - 输入
p 变量名
可以查看变量 - 输入指令
q
可以结束调试
pdb.set_trace()
单步执行调试的效率过低,可以在程序中import pdb,然后使用pdb.set_trace()来设置断点:
# err.py
import pdb
s = '0'
n = int(s)
pdb.set_trace() # 运行到这里会自动暂停
print(10 / n)
运行代码后,程序会在pdb.set_trace()暂停并进入pdb调试环境,可以使用pdb指令,或使用指令c继续执行。
IDE
使用支持调试功能的IDE可以方便的设置断点及单步执行。
目前比较好的Python IDE有:Visual Studio Code、PyCharm
虽然用IDE调试起来比较方便,但是必要时,logging才是最优解。
单元测试
单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。
比如对函数abs()
,我们可以编写出以下几个测试用例:
- 输入正数,比如1、1.2、0.99,期待返回值与输入相同
- 输入负数,比如-1、-1.2、-0.99,期待返回值与输入相反
- 输入0,期待返回0
- 输入非数值类型,比如
None
、[]
、{}
,期待抛出TypeError
把测试用例放到一个测试模块里,就是一个完整的单元测试。
以测试为驱动的开发模式(TDD:Test-Driven Development)的好处是确保一个程序模块的行为符合设计的测试用例。在将来修改的时候,可以极大程度地保证该模块行为仍然是正确的。
测试Demo
编写一个Dict
类,该类的行为和dict
一致,额外添加属性访问:
d = Dict(a=1, b=2)
d['a'] # 1
d.a # 1
实现Dict类:
def Dict(dict):
def __init__(self, **kw):
super.__init__(**kw)
def __getattr__(self, k):
try:
return self[k]
except KeyError:
raise AttributeError(r"'Dict' object has no attribute '%s'" % k)
def __setattr__(self, k, v):
self[k] = v
编写单元测试
编写单元测试需要引入Python自带的unittest
模块。
编写单元测试:
- 需要编写测试类,并继承
unittest.TestCase
- 类中以test开头的方法就是测试方法
- 类中不以test开头的方法不被认为是测试方法,测试的时候不会被执行
- 对每一类测试都需要编写一个
test_xxx()
方法 unittest.TestCase
内置了很多条件判断,根据需要调用这些方法就可以断言输出是否符合。常用的断言方法有:assertEqual()
:用于判断两个值是否相等assertTrue()
:用于判断输入是否为TrueassertRaises()
:用于判断抛出的异常是否为指定类型的Error
import unittest
from unittest_dict import Dict
class TestDict(unittest.TestCase):
def test_init(self):
d = Dict(a=1, b='test')
self.assertEqual(d.a, 1)
self.assertEqual(d.b, 'test')
self.assertTrue(isinstance(d, dict))
def test_key(self):
d = Dict()
d['key'] = 'value'
self.assertEqual(d.key, 'value')
def test_attr(self):
d = Dict()
d.key = 'value'
self.assertTrue('key' in d)
self.assertEqual(d['key'], 'value')
def test_keyerror(self):
d = Dict()
with self.assertRaises(KeyError):
value = d['empty']
def test_attrerror(self):
d = Dict()
with self.assertRaises(AttributeError):
value = d.empty
运行单元测试
运行单元测试常用的方式有两种:
- 在test_dict.py的最后加上代码:
if __name__ == '__main__': unittest.main()
,把mydict_test.py当做正常的python脚本运行 - 在命令行通过参数
-m unittest
直接运行单元测试:python -m unittest mydict_test
运行结果:
.....
----------------------------------------------------------------------
Ran 5 tests in 0.000s
OK
一般使用第二种方法,这样可以一次批量运行很多单元测试,并且,有很多工具可以自动化运行这些单元测试。
setUp与tearDown
可以在单元测试中编写两个特殊的setUp()
和tearDown()
方法。这两个方法会分别在每调用一个测试方法的前后分别被执行。
比如:当测试需要启动一个数据库时,可以在setUp()
方法中连接数据库,在tearDown()
方法中关闭数据库,从而减少在每个测试方法中重复相同的代码:
class TestDict(unittest.TestCase):
def setUp(self):
print('setUp...')
def tearDown(self):
print('tearDown...')
文档测试
Python可能在注释中包含实例代码,用于更明确地告知调用者关于函数的期望输入及输出。
对于注释中的代码,Python内置的“文档测试”(doctest)模块可以提取注释中的代码并执行测试。
doctest
严格按照Python交互式命令行的输入和输出来判断测试结果是否正确。
只有测试异常的时候,可以用...表示中间一大段烦人的输出。
使用doctest
使用doctest
对上文的Dict类进行测试:
class Dict(dict):
'''
Simple dict but also support access as x.y style.
>>> d1 = Dict()
>>> d1['x'] = 100
>>> d1.x
100
>>> d1.y = 200
>>> d1['y']
200
>>> d2 = Dict(a=1, b=2, c='3')
>>> d2.c
'3'
>>> d2['empty']
Traceback (most recent call last):
...
KeyError: 'empty'
>>> d2.empty
Traceback (most recent call last):
...
AttributeError: 'Dict' object has no attribute 'empty'
'''
def __init__(self, **kw):
super().__init__(**kw)
def __getattr__(self, k):
try:
return self[k]
except KeyError:
raise AttributeError(r"'Dict' object has no attribute '%s'" % k)
def __setattr__(self, k, v):
self[k] = v
if __name__=='__main__':
import doctest
doctest.testmod()
没有输出表示编写的doctest
运行正确。