Python07

调试

程序总会有各种各样的bug需要修正。有的bug很简单,看看异常信息就知道,有的bug很复杂,我们需要知道出错时,哪些变量的值是正确的,哪些变量的值是异常的,因此,需要一整套调试程序的手段来修复bug。

print

最简单的调试方法就是用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 CodePyCharm
虽然用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():用于判断输入是否为True
    • assertRaises():用于判断抛出的异常是否为指定类型的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运行正确。

tag(s): Python 
show comments · back · home