Python学习 (七 错误、调试和测试)

本文详细介绍了Python中的错误处理,包括错误类型、捕获和抛出错误。接着讲解了调试技巧,如断言、logging模块和pdb的使用。此外,还介绍了单元测试的重要性和如何编写及运行测试,以及文档测试的概念和doctest模块的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言:有的错误是程序编写有问题造成的,有的错误是用户输入造成的,还有一类错误是完全无法在程序运行过程中预测的,这类错误也称为异常。跟踪程序的执行,查看变量的值是否正确,这个过程称为调试,Python的pdb可以让我们以单步方式执行代码。良好的测试,可以在程序修改后反复运行,确保程序输出符合我们编写的测试。

7.1 错误处理

Python内置了try...except...finally的错误处理机制,避免逐级上报的麻烦。

try

try:
    print 'try...'
    r = 10 / 0
    print 'result:', r
except ZeroDivisionError, e: #e相当于一种赋值?
    print 'except:', e
finally:
    print 'finally...'
print 'END'

当我们认为某些代码可能会出错时,就可以用try来运行这段代码,如果执行出错,则后续代码不会继续执行,而是直接跳转至错误处理代码,即except语句块,执行完except后,如果有finally语句块,则执行finally语句块,至此,执行完毕。
概括就是,觉得可能发生错误,用tryexcept后写上错误类型和执行语句(e?),有finally必执行(不管有没有except),注意:使用!结果如下:

try...
except: integer division or modulo by zero
finally...
END

错误类型可以捕获,except后的自动识别,所以为了准确捕获,可以多加几个,也就是多几个except,至于错误类型有哪些,好好阅读官方文档吧= = 没有错误可以在except后加else,然而我觉得如果是个返回值的话,并没有什么必要,具体情况具体分析吧。
Python的错误是class,全部继承自BaseException,所以在使用错误类型时要注意,子类会被判断进去哦~如:

try:
    foo()
except StandardError, e:
    print 'StandardError'
except ValueError, e:
    print 'ValueError'

第二个except永远无法捕获到ValueError,因为它是StandError的子类,继承关系在这:https://docs.python.org/2/library/exceptions.html#exception-hierarchy
使用try...except一个巨大的好处就是可以跨越多层调用。就是说,不需要在每个可能出错的地方去捕获错误,只要在合适的层次去捕获错误就可以了。这样一来,就大大减少了写try...except...finally的麻烦。
要点:

  • try...except...finally语句执行顺序与规则
  • 错误是一种class,注意父类与子类
  • 跨多层调用,合适地方加入即可

调用堆栈

如果错误没有被捕获,它就会一直往上抛,最后被Python解释器捕获,打印一个错误信息,然后程序退出

# err.py:
def foo(s):
    return 10 / int(s)

def bar(s):
    return foo(s) * 2

def main():
    bar('0')

main()
$ python err.py
Traceback (most recent call last): 逐层抛错,直到可以识别的错误类型与最终源头
  File "err.py", line 11, in <module>
    main()
  File "err.py", line 9, in main
    bar('0')
  File "err.py", line 6, in bar
    return foo(s) * 2
  File "err.py", line 3, in foo
    return 10 / int(s)
ZeroDivisionError: integer division or modulo by zero

记录错误

Python内置的logging模块可以非常容易地记录错误信息,程序打印完错误信息后会继续执行,并正常退出。

import logging

def foo(s):
    return 10 / int(s)

def bar(s):
    return foo(s) * 2
#在这里犯了一个缩进的错误,使用Notepad++默认缩进跳格导致了unindent does not match any outer indentation level
def main():
    try:
        bar('0')
    except StandardError, e:
        logging.exception(e)

main()
print 'END'
$ python err.py
ERROR:root:integer division or modulo by zero
Traceback (most recent call last):
  File "err.py", line 12, in main
    bar('0')
  File "err.py", line 8, in bar
    return foo(s) * 2
  File "err.py", line 5, in foo
    return 10 / int(s)
ZeroDivisionError: integer division or modulo by zero
END

抛出错误

因为错误是class,捕获一个错误就是捕获到该class的一个实例。
如果要抛出错误,首先根据需要,可以定义一个错误的class,选择好继承关系,然后,用raise语句抛出一个错误的实例:

class FooError(StandardError):
    pass

def foo(s):
    n = int(s)
    if n==0:
        raise FooError('invalid value: %s' % s)
    return 10 / n

如果可以选择Python已有的内置的错误类型(比如ValueError,TypeError),尽量使用Python内置的错误类型。
另一种错误处理方式:

def foo(s):
    n = int(s)
    return 10 / n

def bar(s):
    try:
        return foo(s) * 2
    except StandardError, e:
        print 'Error!'
        raise  #不带参数,就会把当前错误原样抛出

def main():
    bar('0')

main()

执行结果是:

Error!
Traceback (most recent call last):
  File "test.py", line 17, in <module>
    main()
  File "test.py", line 15, in main
    bar('0')
  File "test.py", line 8, in bar
    return foo(s) * 2
  File "test.py", line 4, in foo
    return 10 / n
ZeroDivisionError: integer division or modulo by zero

exceptraise一个Error,还可以把一种类型的错误转化成另一种类型:

try:
    10 / 0
except ZeroDivisionError:
    raise ValueError('input error!')

raise有两种用法(茴香豆?):

  • 返回一个错误类型,如果是raise xxx的话
  • 返回当下的,也就是最近的错误,如果单独是rasie的话

7.2 调试

为了修复bug,所以需要调试。
有一种方法是使用print将可能出现错误的变量打印出来看看,缺点是冗杂,还带有大量的垃圾信息,所以这种方法可以不用考虑了。

def foo(s):
    n = int(s)
    print '>>> n = %d' % n
    return 10 / n

def main():
    foo('0')

main()

断言(assert

def foo(s):
    n = int(s)
    assert n != 0, 'n is zero!'
    return 10 / n

def main():
    foo('0')
main()

assert的意思是,表达式n != 0应该是True,否则,后面的代码就会出错。
如果断言失败,assert语句本身就会抛出AssertionError,也会把引号的内容打印出来。
然而,程序里到处都是assertprint差不多一个尿性,不好。不过启动Python时可以用-O(大写的欧)参数关闭assert,就可以看作是pass了。

$ python -O err.py

logging

assert比,logging不会抛出错误,而且可以输出到文件:

import logging
logging.basicConfig(level=logging.INFO)
s = '0'
n = int(s)
logging.info('n = %d' % n)
print 10 / n
$ python err.py #输出如下
INFO:root:n = 0
Traceback (most recent call last):
  File "err.py", line 8, in <module>
    print 10 / n
ZeroDivisionError: integer division or modulo by zero

这就是logging的好处,它允许你指定记录信息的级别,有debuginfowarningerror等几个级别,当我们指定level=INFO时,logging.debug就不起作用了。同理,指定level=WARNING后,debuginfo就不起作用了。这样一来,你可以放心地输出不同级别的信息,也不用删除,最后统一控制输出哪个级别的信息。然而这里并没有继续细讲了,所以之后需要好好研究,包括之前那个(e)的问题

pdb

启动Python的调试器pdb,让程序以单步方式运行,可以随时查看运行状态。打开Python后输入$ python -m pdb err.py,pdb定位到下一步要执行的代码,代码量稍微多一点儿,就可以GG了,所以这个只适合非常小规模的调试,不推荐。
用法:输入命令l来查看代码,输入命令n可以单步执行代码,输入命令p变量名来查看变量,输入命令q结束调试。

PS C:\Users\PGH\mystuff> python -m pdb test.py
> c:\users\pgh\mystuff\test.py(2)<module>()
-> s = '0'
(Pdb) #这个时候你就可以输入了

pdb.set_trace()

这个方法也是用pdb,但是不需要单步执行,我们只需要import pdb,然后,在可能出错的地方放一个pdb.set_trace(),就可以设置一个断点,可以用命令p查看变量,或者用命令c继续运行:

import pdb

s = '0'
n = int(s)
pdb.set_trace() # 运行到这里会自动暂停
print 10 / n

然而,和单步比起来也高不到哪里去 = =

IDE

如果要比较爽地设置断点、单步执行,就需要一个支持调试功能的IDE(Integrated Development Environment),集成开发环境。目前比较好的Python IDE有PyCharm。logging才是最终武器???

7.3 单元测试

单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。
这种以测试为驱动的开发模式最大的好处就是确保一个程序模块的行为符合我们设计的测试用例。在将来修改的时候,可以极大程度地保证该模块行为仍然是正确的。
编写一个Dict类,这个类的行为和dict一致,但是可以通过属性来访问:

class Dict(dict): #直接从dict类继承

    def __init__(self, **kw):
        super(Dict, self).__init__(**kw)

    def __getattr__(self, key):
        try:
            return self[key]
        except KeyError:
            raise AttributeError(r"'Dict' object has no attribute '%s'" % key)

    def __setattr__(self, key, value):
        self[key] = value

为了编写单元测试,我们需要引入Python自带的unittest模块,编写mydict_test.py如下:

import unittest #导入该模块

from mydict import Dict 

class TestDict(unittest.TestCase): #该测试类从unittest.TsetCase继承,这样以下开头是test的都是测试方法,测试时会被执行,没有就不是

    def test_init(self): #默认写法为test_xxx(),再用assertEquals(a,b)判断a和b是否相等
        d = Dict(a=1, b='test')
        self.assertEquals(d.a, 1)
        self.assertEquals(d.b, 'test')
        self.assertTrue(isinstance(d, dict))

    def test_key(self):
        d = Dict()
        d['key'] = 'value'
        self.assertEquals(d.key, 'value')

    def test_attr(self):
        d = Dict()
        d.key = 'value'
        self.assertTrue('key' in d)
        self.assertEquals(d['key'], 'value')

    def test_keyerror(self):
        d = Dict()
        with self.assertRaises(KeyError):  #期待抛出指定类型的Error,通过d['empty']访问不存在的key时,断言会抛出KeyError
            value = d['empty']

    def test_attrerror(self):
        d = Dict()
        with self.assertRaises(AttributeError): #用到了with的语句
            value = d.empty

运行单元测试

最简单的运行方式是在mydict_test.py的最后加上两行代码:

if __name__ == '__main__':
    unittest.main()

一种更常见的方法是在命令行通过参数-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...'

运行结果如下:

PS C:\Users\PGH\mystuff> python -m unittest mydict_test
setUp...
tearDown...
.setUp...
tearDown...
.setUp...
tearDown...
.setUp...
tearDown...
.setUp...
tearDown...
.
----------------------------------------------------------------------
Ran 5 tests in 0.003s

OK

注意事项:

  • 单元测试可以有效地测试某个程序模块的行为,是未来重构代码的信心保证。
  • 单元测试的测试用例要覆盖常用的输入组合、边界条件和异常。
  • 单元测试代码要非常简单,如果测试代码太复杂,那么测试代码本身就可能有bug。
  • 单元测试通过了并不意味着程序就没有bug了,但是不通过程序肯定有bug。

7.4 文档测试

执行写在注释中的这些代码:

def abs(n):
    '''
    Function to get absolute value of number.

    Example:

    >>> abs(1)
    1
    >>> abs(-1)
    1
    >>> abs(0)
    0
    '''
    return n if n >= 0 else (-n)

Python内置的“文档测试”(doctest)模块可以直接提取注释中的代码并执行测试。
doctest严格按照Python交互式命令行的输入和输出来判断测试结果是否正确。只有测试异常的时候,可以用…表示中间一大段烦人的输出。
比如用doctest来编写上面的my_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(Dict, self).__init__(**kw)

    def __getattr__(self, key):
        try:
            return self[key]
        except KeyError:
            raise AttributeError(r"'Dict' object has no attribute '%s'" % key)

    def __setattr__(self, key, value):
        self[key] = value

if __name__=='__main__':  #这块代码是说明当模块正常导入时,doctest不会被执行。只有在命令行运行时,才执行doctest。
    import doctest
    doctest.testmod()

按照上述运行是不会返回任何东西的,而一旦出错的话那就就会返回一大堆错误信息。
总而言之,doctest非常有用,不但可以用来测试,还可以直接作为示例代码。通过某些文档生成工具,就可以自动把包含doctest的注释提取出来。用户看文档的时候,同时也看到了doctest。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值