python笔记-错误和测试

错误处理

try

1
2
3
4
5
6
7
8
9
10
11
12
13
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')

假如我们认为某些代码可能回出错,就可以用try来运行这段代码

如果执行出错,则后续代码不再执行,而是直接跳转至错误捕获代码,即except语句块,

else会在try执行完毕且没有抛出异常时执行

不论有无异常抛出,finally都会执行,它会在try…except…else…都执行完后再执行

错误

python的错误也是类,所有错误类型都继承自BaseException,所以在使用except时,它会把定义的错误类型以及其所有子类一并捕获

1
2
3
4
5
6
try:
foo()
except ValueError as e:
print('ValueError')
except UnicodeError as e:
print('UnicodeError')

这里UnicodeErrorValueError的子类,所以第二个except是永远都捕获不到的,因为第一个except已经把父类给捕获了.

点击查看常见错误类型及继承关系

跨层调用

1
2
3
4
5
6
7
8
9
10
11
12
13
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...')

比如上面bar()调用foo(),main()调用bar().假如foo()出错,在main()中可以直接捕获到.

异常栈

1
2
3
4
5
6
7
8
9
10
11
# err.py:
def foo(s):
return 10 / int(s)

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

def main():
bar('0')

main()

假如错误没有被捕获,它会一直往上抛,知道被python解释器捕获,然后打印错误信息并退出程序

1
2
3
4
5
6
7
8
9
10
Traceback (most recent call last): # 告诉我们这是错误的跟踪信息
File "err.py", line 11, in <module> # 调用main()出错,在代码文件err.py的第11行,但原因在第9行
main()
File "err.py", line 9, in main # 调用bar('0')出错,在代码文件err.py的第9行,但原因在第6行
bar('0')
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: division by zero # 根据错误类型ZeroDivisionError,我们判断,int(s)本身并没有出错,但是int(s)返回0,在计算10 / 0时出错,至此,找到错误源头。

这就是异常栈,出错了向大佬提问时记得把完整异常栈附上.

记录错误

python内置的logging模块可以轻松记录错误信息,而且程序出错后会继续执行,正常退出.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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')
1
2
3
4
5
6
7
8
9
10
11
# 执行后输出如下
ERROR:root:division by zero
Traceback (most recent call last):
File "err_logging.py", line 13, in main
bar('0')
File "err_logging.py", line 9, in bar
return foo(s) * 2
File "err_logging.py", line 6, in foo
return 10 / int(s)
ZeroDivisionError: division by zero
END

具体例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# enconding=utf-8
import logging
import sys

logging.basicConfig(filename='app.log', level=logging.ERROR,
format='%(asctime)s - %(levelname)s - %(message)s', encoding='utf-8')

def divide(x, y):
try:
result = x / y
return result
except Exception as e:
print(f"发生异常: {e}", file=sys.stderr) # 获取错误并输出到console的标准错误
logging.error(f"发生异常: {e}", exc_info=True) # 记录到日志文件
return None

# 示例用法
num1 = 10
num2 = 0

result = divide(num1, num2)

if result is not None:
print(f"结果: {result}")

print('Done')
1
2
Done
发生异常: division by zero

raise

抛出自己定义的错误

错误也是class,捕获一个错误本质上就是捕获该class的一个实例,因此错误并不是凭空产生的,而是有意创建并抛出的.python的内置函数会抛出很多类型的错误,我们也可以自己抛出错误.(但一般来说,尽量使用python内置的错误类型)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class FooError(ValueError):
pass

def foo(s):
n = int(s)
if n==0:
raise FooError(f'invalid value: {s}')
raise 10 / n

foo('0')

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

向上抛出错误

捕获错误的目的只是记录一下,便于后续追踪.但是如果当前函数不知道如何处理这个错误,最恰当的当时继续往上抛,让顶层调用者处理.(员工处理不了的问题抛个老板,老板处理不了抛个CEO).这是很常见的一个做法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 # raise不带参数,就会把当前错误原样抛出

bar()

exceptraise一个error,还可以转换错误的类型

1
2
3
4
try: 
10 / 0
except ZeroDivisionError:
raise ValueError('input error!') # 将ZeroDivisionError转换成ValueError

调试

有时候遇到一些复杂的bug,需要知道出错时,哪些变量的值食正确的,哪些是错误的.此时就需要一套调试手段来修复.

print

最简单,最常见的手法,直接print出来

断言

用断言来代替print

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

assert: 表达式n != 0应该是true,否则则断言失败,抛出AssertionError

1
2
3
Traceback (most recent call last):
...
AssertionError: n is zero!

执行时可以用-O来关闭断言

1
2
3
4
5
$ python -O err.py
Traceback (most recent call last):
...
ZeroDivisionError: division by zero
# 关闭后,所有assert语句都可以看成pass

logging

通过写日志的方式记录错误

1
2
3
4
5
6
7
8
9
10
11
12
13
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

pdb

python的调试器,让程序以单步方式运行,客户以随时查看运行状态.

1
2
3
4
# 比如有这么一个程序
s = '0'
n = int(s)
print(10 / n)

然后通过pdb启动

1
2
3
python -m pdb err.py
> /Users/michael/Github/learn-python3/samples/debug/err.py(2)<module>()
-> s = '0' # 这是pdb定位到的下一步要执行的代码

输入l来查看代码

1
2
3
4
5
(Pdb) l
1 # err.py
2 -> s = '0'
3 n = int(s)
4 print(10 / n)

输入n单步执行代码

1
2
3
4
5
6
(Pdb) n
> /Users/michael/Github/learn-python3/samples/debug/err.py(3)<module>()
-> n = int(s)
(Pdb) n
> /Users/michael/Github/learn-python3/samples/debug/err.py(4)<module>()
-> print(10 / n)

任何时候输入p <变量名>来查看变量的值

1
2
3
4
(Pdb) p s
'0'
(Pdb) p n
0

q退出

pdb.set_trace()

1
2
3
4
5
6
import pdb

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

运行到pdb.set_trace()暂停并进入pdb调试环境,可以用命令p查看变量,或者命令c继续

1
2
3
4
5
6
7
8
9
10
$ python err.py 
> /Users/michael/Github/learn-python3/samples/debug/err.py(7)<module>()
-> print(10 / n)
(Pdb) p n
0
(Pdb) c
Traceback (most recent call last):
File "err.py", line 7, in <module>
print(10 / n)
ZeroDivisionError: division by zero

IDE

上面的方法都是土方法,最方便的就是用IDE自带的调试功能.

以vscode为例:

设置断点: 在代码行号左侧单击,出现一个红色的圆点,表示设置了一个断点.

launch.json: vscode 自动生成一个launch.json文件,用来配置环境.用来设置调试类型,启动程序,参数等.

F5启动用调试

继续F5执行到下一个断点

单步跳过F10: 逐行执行代码,遇到函数调用时跳过函数内部

单步调试F11: 逐行执行代码,遇到函数调用时进入函数内部

跳出Shift+F11: 跳出当前函数

重新开始调试Shift+Cmd+F11

结束调试Shit+F5

单元测试

测试驱动开发(TDD: Test-Driven Development)

单元测试是对一个模块,一个函数或者一个类来进行正确性检验的测试工作.

比如对函数abs(),我们可以编写出以下几个测试用例.

  1. 输入正数,比如1, 1.2, 0.99,期待返回值与输入相同
  2. 输入负数,比如-1,-1.2,-0.99,期待返回值与输入值相反
  3. 输入0,期待返回0
  4. 输入非数值类型,比如None,[],{},期待抛出TypeError

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

单元测试通过,说明我们测试的这个函数能够正常工作.单元测试不通过,要么函数有bug,要么测试条件不正确,总之需要修复使单元测试通过.

下面编写一个Dict类,行为与dict一致,但是可以通过属性来访问:

1
2
3
4
5
>>> d = Dict(a=1, b=2)
>>> d['a']
1
>>> d.a
1
1
2
3
4
5
6
7
8
9
10
11
12
class Dict(dict):
def __init__(self, **kw):
super.__init__(**kw)

def __getattr__(self, key):
try:
return self[key]
except KeyError:
raise AttributeError(f"'Dict' object has no attribute {key}")

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

要编写单元测试,需要用到python自带的unittest模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# mydict_test.py
import unittest
from mydict import Dict

# 写单元测试,需要写一个测试类,继承自unittest.TestCase
class TestDict(unittest.TestCase):
# 以test开头的就是测试方法,不以test开头的不被认为是测试方法,测试时不会被执行
def test_init(self):
d = Dict(a=1, b='test')
self.assertEqual(d.a, 1) # 断言函数返回结果与1相等
self.assertEqual(d.b, 'test')
self.assertTrue(isinstance(d, dict)) # 类型检查断言,检查返回值类型
# self.assertIsInstance(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']

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

unittest.TestCase内置很多断言,常用的有:

  • self.assertEqual(abs(-1), 1): 断言函数返回结果是否与预期的一致
  • self.assertTrue: 验证条件是否为True,True则通过,False则不通过并抛出AssertionError
  • self.assertIsInstance: 类型检查断言
  • with self.assertRaises: 检查抛出的错误是否与预期的一致

运行单元测试的方式推荐使用如下命令:

1
2
3
4
5
6
python -m unittest mydict_test
.....
----------------------------------------------------------------------
Ran 5 tests in 0.000s

OK

可以一次批量运行多个单元测试.

setUp and tearDown

单元测试中两个特殊方法:

  • setUp(): 调用一个测试方法执行
  • tearDown: 调用一个测试方法执行

实际使用例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import unittest

class TestUserService(unittest.TestCase):
def setUp(self):
# 测试前准备,比如创建测试数据库连接
self.db = Database()
self.user_service = UserService(self.db)

def tearDown(self):
# 测试后清理,比如关闭连接
self.db.close()

def test_create_user(self):
# 可以进行多个断言
user = self.user_service.create_user("张三", "zhangsan@example.com")
self.assertEqual(user.name, "张三")
self.assertTrue(user.id > 0)
self.assertRegex(user.email, r'^[\w\.-]+@[\w\.-]+\.\w+$')

@unittest.skipIf(ENV == 'production', "不在生产环境运行")
def test_sensitive_operation(self):
# 可以跳过某些测试
pass

组织结构

1
2
3
4
5
6
7
8
# tests/
# __init__.py
# test_models/
# test_user.py
# test_order.py
# test_services/
# test_auth.py
# test_payment.py

与测试工具集成

1
2
3
4
5
6
7
8
9
10
11
# 与 pytest 集成使用
import pytest

class TestPayment:
@pytest.fixture
def payment_service(self):
return PaymentService()

def test_payment(self, payment_service):
with pytest.raises(InsufficientBalance):
payment_service.process_payment(-100)

文档测试

python有个doctest模块,可以自动提取代码中的注释的示例代码来实现测试.对于注释它要符合基本格式规则才能识别:

  1. 三引号文档字符串中编写
  2. 使用>>>开头表示python提示符
  3. 期望的输出直接写在新行上(不带提示符)
  4. 如果有多行输出,需要确保空白完全匹配
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
def add(a, b):
"""返回两个数的和

>>> add(1, 2)
3
>>> add(-1, 1)
0
>>> add(1.5, 2.5)
4.0
"""
return a + b

class MyList:
"""自定义列表类

>>> lst = MyList([1, 2, 3])
>>> lst.append(4)
>>> lst.get_all()
[1, 2, 3, 4]

>>> lst.pop()
4
>>> lst.get_all()
[1, 2, 3]
"""

def __init__(self, items):
self.items = list(items)

def append(self, item):
self.items.append(item)

def pop(self):
return self.items.pop()

def get_all(self):
return self.items

# 运行测试的方法1:在模块末尾添加 (不推荐)
if __name__ == "__main__":
import doctest
doctest.testmod()

推荐的运行测试的方法与unittest类似:

1
python -m doctest your_script.py

python笔记-错误和测试
http://example.com/2024/07/28/python-error-n-test/
作者
Peter Pan
发布于
2024年7月28日
许可协议