在单元测试中给对象打补丁
写的单元测试中需要给指定的对象打补丁,用来断言它们在测试中的期望行为(比如,断言被调用时的参数个数,访问指定的属性等)
unittest.mock.patch() 函数可被用来解决这个问题。 patch() 还可被用作一个装饰器、上下文管理器或单独使用,尽管并不常见。
将测试输出用日志记录到文件中
1 2 3 4 5 6 7
| import unittest class MyTest(unittest.TestCase): pass if __name__ == '__main__': unittest.main()
|
这样的话测试文件就是可执行的,并且会将运行测试的结果打印到标准输出上。如果你想重定向输出,就需要像下面这样修改 main() 函数:
1 2 3 4 5 6 7 8 9 10
| 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 模块有装饰器可用来控制对指定测试方法的处理
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
| import unittest import os import platform class Tests(unittest.TestCase): def test_0(self): self.assertTrue(True) @unittest.skip('skipped test') def test_1(self): self.fail('should have failed!') @unittest.skipIf(os.name=='posix', 'Not supported on Unix') def test_2(self): import winreg @unittest.skipUnless(platform.system() == 'Darwin', 'Mac specific test') def test_3(self): self.assertTrue(True) @unittest.expectedFailure def test_4(self): self.assertEqual(2+2, 5) if __name__ == '__main__': unittest.main()
|
kip() 装饰器能被用来忽略某个你不想运行的测试。 skipIf() 和 skipUnless()对于你只想在某个特定平台或 Python 版本或其他依赖成立时才运行测试的
时候非常有用。使用 @expected 的失败装饰器来标记那些确定会失败的测试,并且对这些测试你不想让测试框架打印更多信息。
处理多个异常
有一个代码片段可能会抛出多个不同的异常,可以用单个代码块处理不同的异常,可以将它们放入一个元组中
1 2 3 4
| try: client_obj.get_url(url) except (URLError, ValueError, SocketTimeout): client_obj.remove_url(url)
|
如果你想对其中某个异常进行不同的处理,可以将其放入另外一个 except 语句中:
1 2 3 4 5 6
| try: client_obj.get_url(url) except (URLError, ValueError): client_obj.remove_url(url) except SocketTimeout: client_obj.handle_url_timeout(url)
|
很多的异常会有层级关系,对于这种情况,你可能使用它们的一个基类来捕获所有的异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| try: f = open(filename) except (FileNotFoundError, PermissionError): pass try: f = open(filename) except OSError: pass ``` 使用 as 关键字来获得被抛出异常的引用 ```python 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)
|
捕获所有异常
捕获所有的异常,可以直接捕获 Exception 即可, 这个将会捕获除了 SystemExit 、 KeyboardInterrupt 和 GeneratorExit 之外的
所有异常。如果你还想捕获这三个异常,将 Exception 改成 BaseException 即可.
1 2 3 4 5
| try: ... except Exception as e: ... log('Reason:', e)
|
捕获异常后抛出另外的异常
链接异常,使用 raise from 语句来代替简单的 raise 语句, 同时保留两个异常的信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| >>> def example(): ... try: ... int('N/A') ... except ValueError as e: ... raise RuntimeError('A parsing error occurred') from e >>> example() Traceback (most recent call last): File "<stdin>", line 3, in example ValueError: invalid literal for int() with base 10: 'N/A' Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 5, in example RuntimeError: A parsing error occurred
|
输出警告信息
在你维护软件,提示用户某些信息,但是又不需要将其上升为异常级别,那么输出警告信息就会很有用了.
希望程序能生成警告信息(比如废弃特性或使用问题),可使用 warning.warn() 函数.
1 2 3 4 5
| import warnings def func(x, y, logfile=None, debug=False): if logfile is not None: warnings.warn('logfile argument deprecated', DeprecationWarning)
|
warn() 的参数是一个警告消息和一个警告类,警告类有如下几种:
UserWarning, DeprecationWarning, SyntaxWarning, RuntimeWarning, ResourceWarning, 或 Future-Warning.
对警告的处理取决于你如何运行解释器以及一些其他配置。例如,如果你使用 -W all 选项去运行 Python,你会得到如下的输出:
1 2 3
| bash % python3 -W all example.py example.py:5: DeprecationWarning: logfile argument is deprecated warnings.warn('logfile argument is deprecated', DeprecationWarning)
|
通常来讲,警告会输出到标准错误上。如果你想讲警告转换为异常,可以使用 -W error 选项:
1 2 3 4 5 6 7
| bash % python3 -W error example.py Traceback (most recent call last): File "example.py", line 10, in <module> func(2, 3, logfile='log.txt') File "example.py", line 5, in func warnings.warn('logfile argument is deprecated', DeprecationWarning) DeprecationWarning: logfile argument is deprecated
|
默认情况下,并不是所有警告消息都会出现。-W 选项能控制警告消息的输出。 -W all 会输出所有警告消息,-W ignore 忽略掉所有警告,-W error 将警告转换成异常。
另外一种选择,你还可以使用 warnings.simplefilter() 函数控制输出。 always 参数会让所有警告消息出现,`ignore 忽略调所有的警告,error 将警告转换成异常。
warnings 模块对过滤和警告消息处理提供了大量的更高级的配置选项。
调试基本的程序崩溃错误
程序奔溃后该怎样去调试它?
运行 python3 -i someprogram.py 可执行简单的调试。 -i 选项可让程序结束后打开一个交互式 shell。然后你就能查看环境.
可以在程序奔溃后打开 Python 的调试器
1 2 3 4 5 6 7 8 9 10 11 12 13
| >>> import pdb >>> pdb.pm() > sample.py(4)func() -> return n + 10 (Pdb) w sample.py(6)<module>() -> func('Hello') > sample.py(4)func() -> return n + 10 (Pdb) print n 'Hello' (Pdb) q >>>
|
代码所在的环境很难获取交互 shell(比如在某个服务器上面),通常可以捕获异常后自己打印跟踪信息
1 2 3 4 5 6 7
| import traceback import sys try: func(arg) except: print('**** AN ERROR OCCURRED ****') traceback.print_exc(file=sys.stderr)
|
要是你的程序没有奔溃,而只是产生了一些你看不懂的结果,你在感兴趣的地方插入一下 print() 语句也是个不错的选择。
不过,要是你打算这样做,有一些小技巧可以帮助你。首先,traceback.print stack() 函数会你程序运行到那个点的时候创建
一个跟踪栈。
另外,你还可以像下面这样使用 pdb.set trace() 在任何地方手动的启动调试器
1 2 3 4 5
| import pdb def func(arg): ... pdb.set_trace() ....
|
给程序做性能测试
测试程序运行所花费的时间并做性能测试。
只是简单的想测试下你的程序整体花费的时间,通常使用 Unix 时间函数就行了.
需要一个程序各个细节的详细报告,使用 cProfile 模块.
通常情况是介于这两个极端之间。比如你已经知道代码运行时在少数几个函数中花费了绝大部分时间。对于这些函数的性能测试,可以使用一个简单的装饰器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import time from functools import wraps def timethis(func): @wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() r = func(*args, **kwargs) end = time.perf_counter() print('{}.{} : {}'.format(func.__module__, func.__name__, end - start)) return r return wrapper >>> @timethis ... def countdown(n): ... while n > 0: ... n -= 1 ... >>> countdown(10000000) __main__.countdown : 0.803001880645752
|
要测试某个代码块运行时间,你可以定义一个上下文管理器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| from contextlib import contextmanager @contextmanager def timeblock(label): start = time.perf_counter() try: yield finally: end = time.perf_counter() print('{} : {}'.format(label, end - start)) >>> with timeblock('counting'): ... n = 10000000 ... while n > 0: ... n -= 1 ... counting : 1.5551159381866455
|
对于测试很小的代码片段运行性能,使用 timeit 模块会很方便
1 2 3 4 5 6 7 8 9 10
| >>> from timeit import timeit >>> timeit('math.sqrt(2)', 'import math') 0.1432319980012835 >>> timeit('sqrt(2)', 'from math import sqrt') 0.10836604500218527 >>> timeit('math.sqrt(2)', 'import math', number=10000000) 1.434852126003534 >>> timeit('sqrt(2)', 'from math import sqrt', number=10000000) 1.0270336690009572
|
timeit 会执行参数中语句 100 万次并计算运行时间。第二个参数是运行测
试之前配置环境。如果你想改变循环执行次数,可以设置 number 参数.
当执行性能测试的时候,需要注意的是你获取的结果都是近似值。time.perf_counter()
函数会在给定平台上获取最高精度的计时值。不过,它仍然
还是基于时钟时间,很多因素会影响到它的精确度,比如机器负载。如果你对于cpu执行时间更感兴趣,使用 time.process_time()
来代替它。
1 2 3 4 5 6 7 8 9 10 11
| from functools import wraps def timethis(func): @wraps(func) def wrapper(*args, **kwargs): start = time.process_time() r = func(*args, **kwargs) end = time.process_time() print('{}.{} : {}'.format(func.__module__, func.__name__, end - start)) return r return wrapper
|
加速程序运行
你的程序运行太慢,你想在不使用复杂技术比如 C 扩展或 JIT 编译器的情况下加快程序运行速度.
关于程序优化的第一个准则是“不要优化”,第二个准则是“不要优化那些无关紧要的部分”。
通常会发现你得程序在少数几个热点地方花费了大量时间
- 使用函数
定义在全局范围的代码运行起来要比定义在函数中运行慢的多。这种速度差异是由于局部变量和全局变量的实现方式(使用局部变量要更快些)。因此,如果你想让程序运行更快些,只需要将脚本语句放入函数中即可,速度的差异取决于实际运行的程序,不过根据经验,使用函数带来 15-30% 的性能提升是很常见的。
- 尽可能去掉属性访问
每一次使用点 (.) 操作符来访问属性的时候会带来额外的开销。它会触发特定的方法,比如 __getattribute__ ()
和 __getattr__ ()
,这些方法会进行字典操作操作。
可以使用 from module import name 这样的导入形式,以及使用绑定的方法.1 2 3 4 5
| import math math.sqrt(n) from math import sqrt
|
方式2消除了属性访问,用sqrt() 代替了 math.sqrt()
- 理解局部变量
局部变量会比全局变量运行速度快,在内部循环中,可以将某个需要频繁访问的属性放入到一个局部变量中.对于类中的属性访问也同样适用于这个原理。通常来讲,查找某个值比如
self.name 会比访问一个局部变量要慢一些。
- 避免不必要的抽象
任何时候当你使用额外的处理层(比如装饰器、属性访问、描述器)去包装你的代码时,都会让程序运行变慢.
- 使用内置的容器
内置的数据类型比如字符串、元组、列表、集合和字典都是使用 C 来实现的,运行起来非常快。如果你想自己实现新的数据结构(比如链接列表、平衡树等),那么要想在性能上达到内置的速度几乎不可能,因此,还是乖乖的使用内置的吧.
- 避免创建不必要的数据结构或复制
理解或信任 Python 的内存模型,不要滥用 copy.deepcopy() 之类的函数
作为一般准则,不要对程序的每一个部分都去优化, 因为这些修改回导致代码难以阅读和理解。你应该专注于优化产生性能瓶颈的地方,比如内部循环。
引用John Ousterhout 说过的话:“最好的性能优化时从不工作到工作状态的迁移”。直到你真的需要优化的时候再去考虑它。确保你程序正确的运行通常比让它运行更快要更重要一些(至少开始是这样的).