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

本文深入探讨Python中的错误处理机制,包括错误代码、try-except语句、调用栈、记录错误和抛出错误的方法。同时,介绍了调试技巧,如使用print()、断言、logging、pdb以及IDE进行程序调试,确保代码的正确性和稳定性。

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

  • 错误:
    1、程序编写有问题造成的:这种错误我们通常称之为bug,bug是必须修复的。
    2、用户输入造成的:这种错误可以通过检查用户输入来做相应的处理。
    3、完全无法在程序运行过程中预测的:比如写入文件的时候,磁盘满了,写不进去了,或者从网络抓取数据,网络突然断掉了。这类错误也称为异常,在程序中通常是必须处理的,否则,程序会因为各种问题终止并退出。
  • 解决办法
    1、跟踪程序的执行,查看变量的值是否正确,这个过程称为调试。
    2、Python的pdb可以让我们以单步方式执行代码。

1. 错误处理

1.1 错误代码

  • 程序运行中,若发生错误,可以事先约定返回一个错误代码,例如:打开文件的函数open(),成功时返回文件描述符(一个整数),出错时返回-1
  • 但是用这种的方式表示是否出错十分不便,因为函数本身应该返回的正常结果和错误代码混在一起:
def foo():
    r = some_function()
    if r==(-1):
        return (-1)
    # do something
    return r

def bar():
    r = foo()
    if r==(-1):
        print('Error')
    else:
        pass

1.2 try

  • 按照上面错误代码的方式很容易出错,故大部分语言都内置了一套try...except...finally...的错误处理机制:
# 当我们认为某些代码可能会报错,就将这段代码放入try中来运行:
try:
    print('try...')
    r = 10 / 0
    print('result:', r)
# 如果执行出错,后续代码不会运行,而是直接跳转至错误处理代码,即:except语句块:
except ZeroDivisionError as e:
    print('except:', e)
# 执行完except语句后,如果有finally语句,则执行finally语句块,至此,执行完毕;
finally:
    print('finally...')
print('END')


# 运行结果:
try...
except: division by zero
finally...
END

# 若将10 / 0 改为 10 / 2,则得:
try...
result: 5.0
finally...
END

# 可能同时有多种错误,那就需要由不同的except语句块捕获不同的错误:
try:
    print('try...')
    r = 10 / int('a')
    print('result:', r)
except ValueError as e:
    print('ValueError:', e)
except ZeroDivisionError as e:
    print('ZeroDivisionError:', e)
finally:
    print('finally...')
print('END')

# 运行结果
try...
ValueError: invalid literal for int() with base 10: 'a'
finally...
END

# 若没有错误,可在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中错误类型其实也是calss,左右的错误类型都继承自BaseException,
# 所以在使用except时,它不但捕获该类型的错误,还将其子类也一网打尽,如:
try:
    foo()
except ValueError as e:
    print('ValueError')
except UnicodeError as e:  # 此except永远捕获不到UnicodeError,因为UnicodeError是ValueError的子类
    print('UnicodeError')

# Python所有的错误都是从BaseException类派生的:
# [常见错误类型和继承关系](https://docs.python.org/3/library/exceptions.html#exception-hierarchy)

# 跨越多层调用
# 函数main()调用bar(),bar()调用foo(),结果foo()出错,这时,只要main()捕获到,就可以处理,
# 也就是说只需要在合适的层次捕获错误即可,大大减少了写try...except...finally的麻烦:
def foo(s):
    return 10 / int(s)

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

def main():
    try:
        bar('0')
    except Exception as e:
        print('Error:', e)
    finally:
        print('finally...')

1.3 调用栈

  • 如果错误没有被捕获,它就会一直往上抛,最后被Python解释器捕获,打印一个错误信息,然后程序退出。来看看 err.py
# 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 <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: division by zero
  • 出错并不可怕,可怕的是不知道哪里出错了。解读错误信息是定位错误的关键。我们从上往下可以看到整个错误的调用函数链:

  • 错误信息第1行:

Traceback (most recent call last):
  • 告诉我们这是错误的跟踪信息,第2~3行:
File "err.py", line 11, in <module>
main()```
- 调用`main()`出错了,在代码文件`err.py`的第11行代码,但原因是第9行:
```python
File "err.py", line 9, in main
bar('0')
  • 调用bar('0')出错了,在代码文件err.py的第9行代码,但原因是第6行:
File "err.py", line 6, in bar
return foo(s) * 2
  • 原因是return foo(s) * 2这个语句出错了,但这还不是最终原因,继续往下看:
File "err.py", line 3, in foo
  return 10 / int(s)
  • 原因是return 10 / int(s)这个语句出错了,这是错误产生的源头,因为下面打印了:
ZeroDivisionError: integer division or modulo by zero
  • 根据错误类型ZeroDivisionError,我们判断,int(s)本身并没有出错,但是int(s)返回0,在计算10 / 0时出错,至此,找到错误源头。

1.4 记录错误

  • 既能捕获错误,同时将错误堆栈打印出来,让程序继续执行下去:
# Python内置的logging模块可以非常容易的记录错误信息:
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')

# 同样是出错,但程序打印完错误信息后会继续执行,并正常退出:

runfile('E:/4_Programe/1_Python/3_Code/1_LiaoDaDa/15_logging.py', wdir='E:/4_Programe/1_Python/3_Code/1_LiaoDaDa')
ERROR:root:division by zero
Traceback (most recent call last):
  File "E:/4_Programe/1_Python/3_Code/1_LiaoDaDa/15_logging.py", line 11, in main
    bar('0')
  File "E:/4_Programe/1_Python/3_Code/1_LiaoDaDa/15_logging.py", line 7, in bar
    return foo(s) * 2
  File "E:/4_Programe/1_Python/3_Code/1_LiaoDaDa/15_logging.py", line 4, in foo
    return 10 / int(s)
ZeroDivisionError: division by zero
END
  • 通过配置,logging还可以把错误记录到日志文件里,方便事后排查。

1.5 抛出错误

  • 错误是class,捕获一个错误就是捕获到该class的一个实例:
# 定义一个错误的class,选择好继承关系,用raise语句抛出一个错误的实例:
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 <module>
    foo('0')
  File "err_throw.py", line 8, in foo
    raise FooError('invalid value: %s' % s)
__main__.FooError: invalid value: 0
  • 另外一种错误处理的方式:
# 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()

2. 调试

  • 写程序时难免会出现bug,故需要一整套调试程序来修复bug:

2.1 print()

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

def main():
	foo('0')

main()

# 运行结果
>>> n = 0
Traceback (most recent call last):
  ...
ZeroDivisionError: integer division or modulo by zero

# 此方法会使程序变得十分臃肿,同时运行结果也会包含很多垃圾信息

2.2 断言

  • 程序中凡是用print()来辅助查看的地方,都可使用断言(assert)来替代:
def foo(s):
	n = int(s)
	assert n != 0, 'n is zero!'  # 此句意思为表达式 n != 0 应该是True,否则根据程序运行的逻辑,后面的代码肯定出错
	return 10 / n

def main():
	foo('0')

main()

# 运行结果
Traceback (most recent call last):
  ...
AssertionError: n is zero!   # 如果断言失败,assert语句本身会抛出AssertionError
  • 若程序中到处充斥着assert,和print()相比好不到那里去,故python中允许使用-0参数来关闭assert
$ python -O err.py
Traceback (most recent call last):
  ...
ZeroDivisionError: division by zero
# 关闭后,你可以将所有的assert语句当做pass来看

2.3 logging

  • print()替换为logging,和assert相比,logging不会抛出错误,而且可以输出到文件:
  • logging可以指定记录信息的级别,有debuginfowarningerror几个级别,从低到高,当我们指定level=INFO时,logging.debug就不起作用了,这样,我们就可以放心的输出不同级别的信息,也不用删除,最后统一控制输出那个级别的信息:
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

2.4 pdb

  • 第四种方式是启动python的调试器pdb,让程序以单步方式运行:
# err.py
s = '0'
n = int(s)
print(10 / n)
  • 然后启动:
E:\4_Programe\1_Python\3_Code\1_LiaoDaDa>python -m pdb err.py
> e:\4_programe\1_python\3_code\1_liaodada\err.py(38)<module>()
-> s = '0'
  • 以参数-m pdb启动后,pdb定位到下一步要执行的代码-> s = '0'。输入命令l来查看代码:
(Pdb) l
  1     # test.py
  2  -> s = '0'
  3     n = int(s)
  4     print(10 / n)
  • 输入命令n可以单步执行代码:
(Pdb) n
> e:\4_programe\1_python\3_code\1_liaodada\test.py(3)<module>()
-> n = int(s)
(Pdb) n
> e:\4_programe\1_python\3_code\1_liaodada\test.py(4)<module>()
-> print(10 / n)
  • 任何时候都可以输入命令p 变量名来查看变量:
(Pdb) p s
'0'
(Pdb) p n
0
  • 输入命令q结束调试,退出程序

2.5 pdb.set_trace()

  • 此方法也是pdb,不过不需要单步执行,只需在可能出错的地方放一个pdb.set_trace(),就可以设置一个断点:
import pdb

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

# 运行代码,程序会在pdb.set_trace()暂停并进入pdb调试环境:
runfile('E:/4_Programe/1_Python/3_Code/1_LiaoDaDa/18_err.py', wdir='E:/4_Programe/1_Python/3_Code/1_LiaoDaDa')
> e:\4_programe\1_python\3_code\1_liaodada\18_err.py(51)<module>()
-> print(10 / n)
(Pdb) >? p n
0
(Pdb) >? c
Traceback (most recent call last):
  File "18_err.py", line 51, in <module>
    print(10 / n)
ZeroDivisionError: division by zero
# 此方式虽然比单步pdb效率高,但也不是最佳选择

2.6 IDE

3. 单元测试

  • 单元测试是用来对一个模块,一个函数或者一个类来进行正确性检测的测试工作:
  • 比如对函数abs(),我们可以编写出以下几个测试用例:
    1. 输入正数,比如11.20.99,期待返回值与输入相同;

    2. 输入负数,比如-1-1.2-0.99,期待返回值与输入相反;

    3. 输入0,期待返回0

    4. 输入非数值类型,比如None[]{},期待抛出TypeError

  • 可以把上面的测试用例放到一个测试模块里,就是一个完整的单元测试;

3.1 编写单元测试

# 编写一个Dict类,类的行为和dict一致,可以通过属性来访问:
>>> d = Dict(a=1, b=2)
>>> d['a']
1
>>> d.a
1

# mydict.py代码如下:
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
import unittest

from mydict import Dict

class TestDict(unittest.TestCase):   # 编写一个测试类,从unittest.TestCase继承

# 以test开头的方法就是测试方法,否则不是,在测试的时候不会被执行

# 对每一类测试都需要编写一个test.xxx()方法,unittest.TestCase提供了很多内置的条件判断
# 只需要调用这些方法就可以断言输出是否是我们所期望的,最常用的断言是assertEqual()

    def test_init(self):
        d = Dict(a = 1, b = 'test')
        self.assertEqual(d.a, 1)  # self.assertEqual(abs(-1), 1) # 断言函数返回的结果与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):  # 此断言是期待抛出指定类型的Error
            value = d['empty']   # 通过d['empty']访问不存在的key时,断言会抛出KeyError

    def test_attrerror(self):
        d = Dict()
        with self.assertRaises(AttributeError):
            value = d.empty   # 通过d.empty访问不存在的key时,抛出AttributError

3.2 运行单元测试

  • 19_mydict_test.py的最后加上:
if __name__ = '__main__'
	unittest.main()
  • 运行
# 把 19_mydict_test.py 当做正常的python脚本运行:
E:\4_Programe\1_Python\3_Code\1_LiaoDaDa>python 20_mydict_test.py
.....
----------------------------------------------------------------------
Ran 5 tests in 0.001s

OK

# 或者在命令行通过参数 -m unittest 直接运行单元测试:
E:\4_Programe\1_Python\3_Code\1_LiaoDaDa>python -m unittest 19_mydict_test
.....
----------------------------------------------------------------------
Ran 5 tests in 0.001s

OK

3.3 setUptearDown

  • 可以在单元测试中编写两个特殊的setUp()tearDown()方法。这两个方法会分别在每调用一个测试方法的前后分别被执行。
class TestDict(unittest.TestCase):

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

    def tearDown(self):
        print('tearDown...')
  • 运行结果:
E:\4_Programe\1_Python\3_Code\1_LiaoDaDa>python -m unittest 19_mydict_test
setUp...
tearDown...
.setUp...
tearDown...
.setUp...
tearDown...
.setUp...
tearDown...
.setUp...
tearDown...
.
----------------------------------------------------------------------
Ran 5 tests in 0.001s

OK

3.4 EX

  • 对Student类编写单元测试,结果发现测试不通过,请修改Student类,让测试通过:
# -*- coding: utf-8 -*-
import unittest

class Student(object):
    def __init__(self, name, score):
        self.name = name
        self.score = score
    def get_grade(self):
        if self.score >= 60:
            return 'B'
        if self.score >= 80:
            return 'A'
        return 'C'
  • 进行如下修改:
# -*- coding: utf-8 -*-
import unittest

class Student(object):
    def __init__(self, name, score):
        self.name = name
        self.score = score
    def get_grade(self):
        if 80 <= self.score <= 100:
            return 'A'
        elif 60 <= self.score <= 80:
            return 'B'
        elif 0 <= self.score < 60:
            return 'C'
        else:
            raise ValueError
class TestStudent(unittest.TestCase):

    def test_80_to_100(self):
        s1 = Student('Bart', 80)
        s2 = Student('Lisa', 100)
        self.assertEqual(s1.get_grade(), 'A')
        self.assertEqual(s2.get_grade(), 'A')

    def test_60_to_80(self):
        s1 = Student('Bart', 60)
        s2 = Student('Lisa', 79)
        self.assertEqual(s1.get_grade(), 'B')
        self.assertEqual(s2.get_grade(), 'B')

    def test_0_to_60(self):
        s1 = Student('Bart', 0)
        s2 = Student('Lisa', 59)
        self.assertEqual(s1.get_grade(), 'C')
        self.assertEqual(s2.get_grade(), 'C')

    def test_invalid(self):
        s1 = Student('Bart', -1)
        s2 = Student('Lisa', 101)
        with self.assertRaises(ValueError):
            s1.get_grade()
        with self.assertRaises(ValueError):
            s2.get_grade()

if __name__ == '__main__':
    unittest.main()
  • 运行结果:
.... 
---------------------------------------------------------------------- 
Ran 4 tests in 0.000s 

OK

4. 文档测试

4.1 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(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 20_doctest.py,发现什么输出都没有,这说明我们编写的doctest运行是正确的。如果把__getattr__方法注释掉,再次运行:
E:\4_Programe\1_Python\3_Code\1_LiaoDaDa> python 20_doctest.py
**********************************************************************
File "20_doctest.py", line 7, in __main__.Dict
Failed example:
    d1.x
Exception raised:
    Traceback (most recent call last):
      File "E:\1_Install_Total\8_Anaconda\lib\doctest.py", line 1329, in __run
        compileflags, 1), test.globs)
      File "<doctest __main__.Dict[2]>", line 1, in <module>
        d1.x
    AttributeError: 'Dict' object has no attribute 'x'
**********************************************************************
File "20_doctest.py", line 13, in __main__.Dict
Failed example:
    d2.c
Exception raised:
    Traceback (most recent call last):
      File "E:\1_Install_Total\8_Anaconda\lib\doctest.py", line 1329, in __run
        compileflags, 1), test.globs)
      File "<doctest __main__.Dict[6]>", line 1, in <module>
        d2.c
    AttributeError: 'Dict' object has no attribute 'c'
**********************************************************************
1 items had failures:
   2 of   9 in __main__.Dict
***Test Failed*** 2 failures.
  • 注意到最后3行代码。当模块正常导入时,doctest不会被执行。只有在命令行直接运行时,才执行doctest。所以,不必担心doctest会在非测试环境下执行。

4.2 EX

  • 对函数fact(n)编写doctest并执行:
def fact(n):
    '''
    Calculate 1*2*...*n

    >>> fact(1)
    1
    >>> fact(10)
    ?
    >>> fact(-1)
    ?
    '''
    if n < 1:
        raise ValueError()
    if n == 1:
        return 1
    return n * fact(n - 1)
if __name__ == '__main__':
    import doctest
    doctest.testmod()
  • 修改之后:
def fact(n):
    '''
    Calculate 1*2*...*n

    >>> fact(1)
    1
    >>> fact(10)
    3628800
    >>> fact(-1)
    Traceback (most recent call last):
        ...
    ValueError: n must be >= 1
    '''
    if n < 1:
        raise ValueError()
    if n == 1:
        return 1
    return n * fact(n - 1)
    
if __name__ == '__main__':
    import doctest
    doctest.testmod()

# 运行结果
********************************************************************** 
File "C:\Users\Jack_ZD\AppData\Local\Temp\learn_python_zaw1p1zl_py\test_1.py", line 10, in __main__.fact 
Failed example: 
    fact(-1) 
Expected: 
    Traceback (most recent call last): 
        ... 
    ValueError: n must be >= 1 
Got: 
    Traceback (most recent call last): 
      File "E:\1_Install_Total\8_Anaconda\lib\doctest.py", line 1329, in __run 
        compileflags, 1), test.globs) 
      File "<doctest __main__.fact[2]>", line 1, in <module> 
        fact(-1) 
      File "C:\Users\Jack_ZD\AppData\Local\Temp\learn_python_zaw1p1zl_py\test_1.py", line 16, in fact 
        raise ValueError() 
    ValueError 
********************************************************************** 
1 items had failures: 
   1 of   3 in __main__.fact 
***Test Failed*** 1 failures.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值