Python学习笔记:错误、调试和测试

Python学习笔记:错误、调试和测试

学自廖雪峰巨佬的Python3教程:https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/001431913726557e5e43e1ee8d54ee486bddc3f607afb75000

1.错误处理

可以使用错误码来表示出错,比如平时的Python程序运行成功返回0,出错返回-1,但是一旦出错,还要一级一级上报,直到某个函数可以处理该错误(比如,给用户输出一个错误信息)。

因此有try..except..finally的错误处理机制

当我们认为某些代码可能会出错时,就可以用try来运行这段代码,如果执行出错,则后续代码不会继续执行,而是跳转到except代码块,执行完except代码块后,如果有finally语句块,则执行finally语句块,至此执行完毕。注意,就算try语句块没有报错,finally语句块也一定会被执行。except语句块也可以有多个,用于处理不同类型的错误,如果没有错误发生,可以在except语句块后面加一个else,当没有错误发生时,会自动执行else语句:

try:
    print('try...')
    r = 10 / int('2')
    print('result:', r)
except ValueError as e:
    print('ValueError:', e)
except ZeroDivisionError as e:
    print('ZeroDivisionError:', e)
else:
    print('no error!')
finally:
    print('finally...')
print('END')

Python的错误其实也是class,所有的错误类型都继承自BaseException,常见的错误类型和继承关系看这里:https://docs.python.org/3/library/exceptions.html#exception-hierarchy

而且,不需要在每个可能出错的地方捕获错误,只要在合适的层次去捕获错误就可以了

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

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

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

def main():
    bar('0')

main()

$ python3 err.py
Traceback (most recent call last):
  File "err.py", line 11, in 
    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: division by zero

错误信息的第一行表示这是错误的跟踪信息
2~3行,表示调用main()出错,在代码文件err.py的第11行代码,但原因是第9行
调用bar('0')出错,在代码的第9行代码,但原因是第6行
调用return foo(s)*2出错,原因是return 10 / int(s)语句出错,根据ZeroDivisionError,找到错误源头

出错的时候,一定要分析错误的调用栈信息,才能定位错误的位置。

Python学习笔记:错误、调试和测试_第1张图片(这图太骚了我一定要贴过来)

除了让Python解释器打印出错误的调用栈信息,我们还能用logging模块记录错误信息,前者打印异常信息之后,程序也会被结束;后者打印完信息后还会继续执行,并正常退出,而且通过配置,logging还可以把错误记录到日志文件里

# err_logging.py

import logging

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

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

def main():
    try:
        bar('0')
    except Exception as e:
        logging.exception(e)

main()
print('END')

除了捕获系统错误,还可以抛出错误,如果一个方法出现异常,但没有能力处理这种异常,就可以抛出异常。如果要抛出错误,首先根据需要,可以定义一个错误的class,选择好继承关系,然后用raise语句抛出一个错误的实例

# err_raise.py
class FooError(ValueError):
    pass

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

foo('0')

$ python3 err_raise.py 
Traceback (most recent call last):
  File "err_throw.py", line 11, in 
    foo('0')
  File "err_throw.py", line 8, in foo
    raise FooError('invalid value: %s' % s)
__main__.FooError: invalid value: 0

一般选择Python内置的错误类型,只有在必要的时候才定义我们自己的错误类型

还有另一种错误处理的方式:

# err_reraise.py

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

def bar():
    try:
        foo('0')
    except ValueError as e:
        print('ValueError!')
        raise

bar()

在bar()中,明明已经捕获了错误,打印了提示,但是又把错误通过raise往上抛了?这是因为捕获错误的目的只是记录一下,便于后续追踪,但是,由于当前函数不知道应该怎么处理该错误,所以,最恰当的方式是继续往上抛,让顶层调用者去处理,而raise语句不带参数的话,就会把当前错误原样抛出

Python学习笔记:错误、调试和测试_第2张图片

Python学习笔记:错误、调试和测试_第3张图片

从最后的报错可以发现,int()是无法将字符'7.6'转换成整型数字的

2.调试

调试的第一种方法简单粗暴,就是把可能有问题的变量打印出来看看
但是凡是可以用print()来查看变量的地方,都可以用断言(assert)来替代,assert的格式是assert condition,information,意思是,assert后面跟的条件如果不成立,就抛出错误,如果有带附加信息(字符串),则输出附加信息。

logging可以替代assert,不会抛出错误,还可以输出到文件,还可以指定记录信息的级别,级别有INFO,DEBUG,WARNING,ERROR几个级别

import logging
logging.basicConfig(level=logging.INFO)

3.单元测试

单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。下面编写了一个Dict类,这个类的行为和dict一致,但是可以通过属性来访问

class Dict(dict):

    def __init__(self, **kw):
        super().__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


>>> d = Dict(a=1, b=2)
>>> d['a']
1
>>> d.a
1

引入Python自带的unittest模块,编写单元测试如下

import unittest

from mydict 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

从上面的代码可以看到,编写单元测试时,需要编写一个继承类,从unittest.TestCase继承,测试方法命名必须以test开头,否则方法不会被执行。assertEqual断言用于判断里面的两个参数是否相等,assertTrue用于判断内部表达式是否为真。另一种重要的断言就是期待抛出指定类型的Error,比如通过d['empty']访问不存在的key时,断言会抛出KeyError,而通过d.empty访问不存在的key时,期待抛出AttributeError

with self.assertRaises(KeyError):
    value = d['empty']

with self.assertRaises(AttributeError):
    value = d.empty

编写好单元测试之后,可以在后面加上两行代码运行

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

另一种方法是在命令行通过参数-m unittest直接运行单元测试

可以在单元测试中编写两个特殊的的setUp()和tearDown()方法,这两个方法会分别在每调用一个测试方法的前后分别被执行,比如测试需要启动一个数据库,这时,就可以在setUp()方法中连接数据库,在tearDown()方法中关闭数据库,这样,不必在每个测试方法中重复相同的代码

class TestDict(unittest.TestCase):

    def setUp(self):
        print('setUp...')

    def tearDown(self):
        print('tearDown...')

Python学习笔记:错误、调试和测试_第4张图片

修改后的代码如下:

class Student(object):
    def __init__(self, name, score):
        self.name = name
        self.score = score

    def get_grade(self):
        if self.score > 100 or self.score <0:
            raise ValueError('invalid value: %s' % self.score)
        if self.score >= 80:
            return 'A'
        if self.score >= 60:
            return 'B'
        return 'C'

其实问题就是if语句的顺序,还有就是异常数据没有抛

unittest真香

4.文档测试

通过Python内置的文档测试(doctest)模块可以直接提取注释中的代码并执行测试

# mydict2.py
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__':
    import doctest
    doctest.testmod()

如果运行正常,说明编写的程序是正确的,就会什么输出都没有。

Python学习笔记:错误、调试和测试_第5张图片

代码如下:

#!/usr/bin/env python
# -*- coding: utf-8 -*-


def fact(n):

    '''
    Calculate 1*2*...*n

    >>> fact(1)
    1
    >>> fact(10)
    3628800
    >>> fact(-1)
    Traceback (most recent call last):
    ...
    ValueError: no number!
    '''

    if n < 1:
        raise ValueError('no number!')
    if n == 1:
        return 1
    return n * fact(n - 1)


if __name__ == '__main__':
    import doctest
    doctest.testmod()

 

你可能感兴趣的:(Python)