Python Cookbook 14测试、调试和异常

目录

 

14.1 怎么测试输出结果是否正确

14.3 怎么判断程序里某个预计异常是否被抛出

14.4 将测试输出用日志记录到文件中

14.5 忽略或期望 测试失败

14.6 处理多个异常

14.7 捕获全部的异常

14.8 创建自定义异常

14.9 捕获异常后抛出另外的异常

14.11 怎么在自己的程序能生成警告信息

14.12 调试

14.13 测试程序运行所花费时间并做性能测试

14.14 加速程序运行


14.1 怎么测试输出结果是否正确

sys.stdin,sys.stdout,sys.stderr是解释器分别用于标准输入,输出和错误的文件对象

  • stdin用于所有交互式输入(包括对的调用 input());

  • stdout用于输出print()表达 语句和用于的提示input();

  • 解释器自己的提示及其错误消息转到stderr

问题:程序会有 print 文本打印,(默认情况下 print 函数会将输出发送到 sys.stdout),怎么测试输出结果是否正确?

比如A1就是这个程序:

#A1.py

def f():
	'''中间过程省略,最终有 print文本打印
	'''
	s = 'The output message'
	print(s)

unittest.mock.patch(target,new = DEFAULT)

patch()充当函数装饰器、类装饰器或上下文管理器。在函数体或with语句中,用 target 修补目标。当函数/with语句退出时,patch 被取消。

对A1的 f()函数的单元测试:

#A2.py

from A1 import f
from unittest import TestCase
from unittest.mock import patch
from io import StringIO

class TestA1(TestCase):
	def test_A1_print(self):
		expected_s = 'The output message\n'

		with patch('sys.stdout',new=StringIO()) as fake_out:
			f()
			return self.assertEqual(fake_out.getvalue(),expected_s)

if __name__ == '__main__':
	e = TestA1()
	e.test_A1_print()

数据读写不一定是文件,也可以在内存中读写。StringIO就是在内存中读写str。StringIO.getvalue()方法返回包含缓冲区全部内容的str。5.6 字符串的IO操作

unittest.mock.patch() 函数被用作一个上下文管理器,使用 StringIO 对象来代替 sys.stdout . fake_out 变量是在该进程中被创建的模拟对象。 在with语句中使用它可以执行各种检查。当with语句结束时,patch 会将所有东西恢复到测试开始前的状态。

单元测试用断言判断 StringIO.getvalue(),即实际输出,和预测输出结果是否相同,实现了stdout输出的测试。

14.2 在单元测试中给对象打补丁

unittest.mock.patch()

14.3 怎么判断程序里某个预计异常是否被抛出

怎么判断程序里某个预计异常是否被抛出?

assertRaisesexceptioncallable* args** kwds 

exception 是预计抛出的异常;callable 是可调用对象;* args** kwds 是 callable 的参数。

assertRaises 断言该程序执行会抛出指定的异常。

示例1:

>>> int('gyf')
Traceback (most recent call last):
  File "<pyshell#67>", line 1, in <module>
    int('gyf')
ValueError: invalid literal for int() with base 10: 'gyf'

>>> import unittest
>>> t = unittest.TestCase()
>>> t.assertRaises(ValueError,int,'gyf')
>>> 
>>> t.assertRaises(ValueError,int,'22')
Traceback
......
AssertionError: ValueError not raised by int

示例2:

>>> class Raises(unittest.TestCase):
	def raises(self,exception,callable,*args):
		self.assertRaises(exception,callable,*args)


>>> e = Raises()
>>> e.raises(ValueError,int,'gyf')
>>> 
>>> e.raises(ValueError,int,1)
Traceback (most recent call last):
......
AssertionError: ValueError not raised by int

 assertRaises 能被当做上下文管理器使用:

>>> with t.assertRaises(ValueError):
	int('gyf')

	
>>> with t.assertRaises(ValueError):
	int('11')

	
11
Traceback (most recent call last):
......
AssertionError: ValueError not raised

assertRaises() 测试不了异常具体的值是多少。

为了测试异常具体的值,可以使用 assertRaisesRegex() 方法, 它可同时测试异常的存在以及通过正则式匹配异常的字符串表示。 

assertRaisesRegexexceptionregexcallable* args** kwds 

regex 为异常具体的

示例:

>>> import unittest
>>> t = unittest.TestCase()

>>> exception_value = 'invalid literal .*'
>>> t.assertRaisesRegex(ValueError,exception_value,int,'gyf')
>>> t.assertRaisesRegex(ValueError,exception_value,int,'22')
Traceback (most recent call last):
......
AssertionError: ValueError not raised by int

14.4 将测试输出用日志记录到文件中

怎么将单元测试的输出写到到某个文件中去,而不是打印到标准输出?

class unittest.TextTestRunner(stream=Nonedescriptions=Trueverbosity=1failfast=Falsebuffer=Falseresultclass=Nonewarnings=None*tb_locals=False)

将结果输出到 stream的基本测试运行器。stream 表示输出的测试报告路径,如果stream为None,则默认使用sys.stderr作为输出流。

run(test) 是TextTestRunner的主公共接口。此方法接受 TestSuite或TestCase实例,通过调用makeResult()创建TestResult,运行测试并将结果打印到stdout。

示例:

import sys

def main(out=sys.stderr, verbosity=2):
    loader = unittest.TestLoader()
    suite = loader.loadTestsFromModule(sys.modules[__name__])
    unittest.TextTestRunner(out,verbosity=verbosity).run(suite)

if __name__ == '__main__':
    with open('testing.out', 'w') as f:
        main(f)

unittest 模块首先会组装一个测试套件。 一旦套件组装完成,它所包含的测试就可以被执行了。

示例代码解析:

unittest.TestLoader 实例被用来组装测试套件。

 loadTestsFromModule() 是它定义的方法之一,用来收集测试用例。 它会为 TestCase 类扫描某个模块并将其中的测试方法提取出来。 如果你想进行细粒度的控制, 可以使用 loadTestsFromTestCase() 方法来从某个继承TestCase的类中提取测试方法。

 TextTestRunner 类是一个测试运行类的例子, 这个类的主要用途是执行某个测试套件中包含的测试方法。 这个类跟执行 unittest.main() 函数所使用的测试运行器是一样的。

这里把文件对象通过 main()传给了 unittest.TextTestRunner(),则测试输出到了该文件中。

14.5 忽略或期望 测试失败

@unittest.skipreason

无条件跳过装饰测试。 原因应说明为何跳过测试。

@unittest.skipIfconditionreason

如果条件为真,则跳过修饰的测试。

@unittest.skipUnlessconditionreason

除非条件为真,否则跳过装饰性测试。

skipIf() 和 skipUnless() 对于你只想在某个特定平台或Python版本或其他依赖成立时才运行测试的时候非常有用。

@unittest.expectedFailure

将测试标记为预期的失败。如果测试失败,则视为成功。如果测试通过,则视为失败。

14.6 处理多个异常

一个代码片段可能会抛出多个不同的异常,怎样才能不创建大量重复代码就能处理所有的可能异常呢?

try/except

示:1:

try:
    client_obj.get_url(url)
except (URLError, ValueError):
    client_obj.remove_url(url)
except SocketTimeout:
    client_obj.handle_url_timeout(url)

可以将多个异常放入一个元组中,元祖中任何一个异常发生时都会执行。

示例2:

try:
    f = open(filename)
except OSError as e:
    if e.errno == errno.ENOENT:
        logger.error('File not found')
    elif e.errno == errno.EACCES:
        logger.error('Permission denied')
    else:
        logger.error('Unexpected error: %d', e.errno)

很多异常有层级关系,可以使用一个基类异常来捕获所有的异常。

可以使用 as 关键字来获得被抛出异常的引用,e变量指向一个被抛出的 OSError 异常实例,进一步分析这个异常。

注意 except 语句是顺序检查的,第一个匹配的会执行。

可以通过查看异常的 __mro__ 属性得到异常的层级关系,示例:

>>> FileNotFoundError.__mro__
(<class 'FileNotFoundError'>, <class 'OSError'>, <class 'Exception'>, <class 'BaseException'>, <class 'object'>)

14.7 捕获全部的异常

想要捕获所有的异常,可以直接捕获 Exception 即可,这将会捕获除了 SystemExit 、 KeyboardInterrupt 和 GeneratorExit 之外的所有异常。 如果你还想捕获这三个异常,将 Exception 改成 BaseException 即可。

要确保打印正确的诊断信息或将异常传播出去,这样不会丢失掉异常。

14.8 创建自定义异常

在程序中引入自定义异常可以使得你的代码更具可读性,能清晰显示谁应该阅读这个代码。 还有一种设计是将自定义异常通过继承组合起来。在复杂应用程序中, 使用基类来分组各种异常类也是很有用的,它可以让用户捕获一个范围很窄的特定异常。

示例:

class ProtocolError(NetworkError):
    pass


try:
    s.send(msg)
except ProtocolError:
    ...

自定义异常类应该总是继承自内置的 Exception 类, 或者是继承自那些本身就是从 Exception 继承而来的类。

尽管所有类同时也继承自 BaseException ,但你不应该使用这个基类来定义新的异常。 BaseException 是为系统退出异常而保留的,比如 KeyboardInterrupt 或 SystemExit 以及其他那些会给应用发送信号而退出的异常。 因此,捕获这些异常本身没什么意义。 这样的话,假如你继承 BaseException 可能会导致你的自定义异常不会被捕获而直接发送信号退出程序运行。

关于创建自定义异常的更多信息,请参考`Python官方文档 <https://docs.python.org/3/tutorial/errors.html>`_

14.9 捕获异常后抛出另外的异常

在设计代码时,在另外一个 except 代码块中使用 raise 语句的时候你要特别小心了。 大多数情况下,这种 raise 语句都应该被改成 raise from 语句。

如果使用  raise ,Traceback 没有很清晰的说明这个异常到底是内部异常还是某个未知的编程错误。

示例:

>>> def f1():
	try:
		int('gyf')
	except ValueError:
		raise RuntimeError('A parsing error occurred')

	
>>> f1()
Traceback (most recent call last):
  File "<pyshell#20>", line 3, in f1
    int('gyf')
ValueError: invalid literal for int() with base 10: 'gyf'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<pyshell#21>", line 1, in <module>
    f1()
  File "<pyshell#20>", line 5, in f1
    raise RuntimeError('A parsing error occurred')
RuntimeError: A parsing error occurred

而使用 raise from 语句,将异常原因链接起来,也就是说,DifferentException 是直接从 SomeException 衍生而来。 这种关系可以从回溯结果中看出来。

示例:

>>> def f():
	try:
		int('gyf')
	except ValueError as e:
		raise RuntimeError('A parsing error occurred') from e

	
>>> f()
Traceback (most recent call last):
  File "<pyshell#8>", line 3, in f
    int('gyf')
ValueError: invalid literal for int() with base 10: 'gyf'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<pyshell#9>", line 1, in <module>
    f()
  File "<pyshell#8>", line 5, in f
    raise RuntimeError('A parsing error occurred') from e
RuntimeError: A parsing error occurred

可以看到区别:

示例1 Traceback : 在处理上述异常期间,发生了另一个异常

示例2 Traceback : 上述异常是以下异常的直接原因

14.10 重新抛出被捕获的异常

怎么在捕获异常后执行某个操作(比如记录日志、清理等),再重新抛出这个异常?

简单的使用一个单独的 rasie 语句即可,示例:

>>> def f():
	try:
		int('gyf')
	except ValueError:
		print('There can process exception information')
		raise

	
>>> f()
There can process exception information
Traceback (most recent call last):
  File "<pyshell#38>", line 1, in <module>
    f()
  File "<pyshell#37>", line 3, in f
    int('gyf')
ValueError: invalid literal for int() with base 10: 'gyf'

14.11 怎么在自己的程序能生成警告信息

怎么在自己的程序能生成警告信息?

在维护软件时,提示用户某些信息,但是又不需要将其上升为异常级别,输出警告信息就很合适了。

warnings.warn(messagecategory=Nonestacklevel=1source=None)

message 参数是警告消息

category 参数是警告类别;默认为UserWarning。警告类有如下几种:UserWarning, DeprecationWarning, SyntaxWarning, RuntimeWarning, ResourceWarning, 或 FutureWarning.

示例:

>>> import warnings
>>> def deprecation(message):
    warnings.warn(message, DeprecationWarning, stacklevel=2)


>>> deprecation('A example...')

Warning (from warnings module):
  File "__main__", line 1
DeprecationWarning: A example...

14.12 调试

不要将调试弄的过于复杂化。

一些简单的错误只需要观察程序堆栈信息就能知道了, 实际的错误一般是堆栈的最后一行。

在开发的时候,也可以在你需要调试的地方插入一下 print() 函数来诊断信息(只需要最后发布的时候删除这些打印语句即可)。

调试器的一个常见用法是观测某个已经崩溃的函数中的变量。 知道怎样在函数崩溃后进入调试器是一个很有用的技能。

如果程序因为某个异常而崩溃,运行 python3 -i someprogram.py 可执行简单的调试。 -i 选项可让程序结束后打开一个交互式shell,便于调试。

当需要解剖一个非常复杂的程序,底层的控制逻辑不是很清楚的时候, 插入 pdb.set_trace() 这样的语句就很有用了。程序会一直运行到碰到 set_trace() 语句位置,然后立马进入调试器。

可参考  https://www.liaoxuefeng.com/wiki/897692888725344/923056208268256

14.13 测试程序运行所花费时间并做性能测试

#1 已经知道代码运行时在少数几个函数中花费了绝大部分时间。 对于这些函数的性能测试,可以使用一个简单的装饰器

#1. 计时功能的装饰器

#2 要测试某个代码块运行时间,你可以定义一个上下文管理器

13.13 实现一个计时器

8.3 让对象支持上下文管理协议

#3 对于测试很小的代码片段运行性能,使用 timeit 模块会很方便

timeit.timeit(stmt='pass'setup='pass'timer=<default timer>number=1000000globals=None)

使用给定语句、 setup 代码和 timer 函数创建一个 Timer 实例,并执行 number 次其 timeit() 方法。可选的 globals 参数指定用于执行代码的命名空间。

示例,(参数分别为给定测试语句,运行测试之前配置环境,循环执行次数):

>>> from timeit import timeit

>>> timeit('sqrt(100)','from math import sqrt',number=100)
1.275100112252403e-05

14.14 加速程序运行

问题:程序运行太慢,怎么在不使用复杂技术(比如C扩展或JIT编译器)的情况下加快程序运行速度?

首先得使用14.13小节的技术先对它进行性能测试,找到问题所在。通常会发现程序在少数几个热点地方花费了大量时间, 比如内存的数据处理循环。

一旦定位到这些点,就可以进行优化了。

在优化之前,有必要先研究下使用的算法。 选择一个复杂度为 O(n log n) 的算法要比你去调整一个复杂度为 O(n**2) 的算法所带来的性能提升要大得多。

如果你觉得你还是得进行优化,那么请从整体考虑。 作为一般准则,不要对程序的每一个部分都去优化,因为这些修改会导致代码难以阅读和理解。 你应该专注于优化产生性能瓶颈的地方,比如内部循环。

一些实用技术可以加速程序运行:

Python 加速程序运行的实用技术 https://mp.csdn.net/postedit/103633927

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值