2.13 在优化期间进行单元测试保持代码的正确性
如果你不对你的代码进行单元测试,那么从长远来看你可能正在损害你的生产力。Ian(脸红)十分尴尬地提到有一次他花了一整天的时间优化他的代码,因为嫌麻烦所以他禁用了单元测试,最后却发现那个显著的速度提升只是因为他破坏了需要优化的那段算法。这样的错误你一次都不要犯。
除了单元测试,你还应该坚定地考虑使用coverage.py。它会检查有哪些代码行被你的测试所覆盖并找出那些没有被覆盖的代码。这可以让你迅速知道你是否测试了你想要优化的代码,那么在优化过程中可能潜伏的任何错误都会被迅速抓出来。
No-op的@profile修饰器
如果你的代码使用了line_profiler或者memory_profiler的@profile修饰器,那么你的单元测试会引发一个NameError异常并失败。原因是单元测试框架不会将@profile修饰器注入本地名字空间。no-op修饰器可以在这种时候解决问题。在你测试时把它加入你的代码块,并在你结束测试后移除它是在方便不过的事情了。
使用no-op修饰器,你可以运行你的测试而不需要修改你的代码。这意味着你可以在每次优化之后都运行你的测试,你将永远不会倒在一个出问题的优化步骤上。
如例2-20所示,假设我们有一个ex.py模块,它有一个测试用例(基于nosetests框架)和一个函数,这个函数我们正在用line_profiler或者memory_profiler进行性能分析。
例2-20 一个简单的函数和一个测试用例需要用到@profile
# ex.py
import unittest
@profile
def some_fn(nbr):
return nbr * 2
class TestCase(unittest.TestCase):
def test(self):
result = some_fn(2)
self.assertEquals(result, 4)
如果我们运行nosetests测试我们的代码就会得到一个NameError:
$ nosetests ex.py
E
======================================================================
ERROR: Failure: NameError (name 'profile' is not defined)
...
NameError: name 'profile' is not defined
Ran 1 test in 0.001s
FAILED (errors=1)
解决方法是在ex.py开头添加一个no-op修饰器(你可以在完成性能分析之后移除它)。如果在名字空间中寻找不到@profile修饰器(因为没有使用line_profiler或者memory_profiler),那么我们写的no-op版本的修饰器就会被加入名字空间。如果line_profiler或者memory_profiler已经将新的函数加入名字空间,那么我们no-op版本的修饰器就会被忽略。
对于line_profiler,我们可以加入例2-21的代码。
例2-21 在单元测试时在名字空间中加入针对line_profiler的no-op@profile修饰器
# line_profiler
if '__builtin__' not in dir() or not hasattr(__builtin__, 'profile'):
def profile(func):
def inner(*args, **kwargs):
return func(*args, **kwargs)
return inner
__builtin__
检查是针对nosetests的,hasattr则用来检查@profile修饰器是否已经被加入名字空间。现在可以在我们的代码上成功运行nosetests了:
$ kernprof.py -v -l ex.py
Line # Hits Time Per %%HTMLit % Time Line Contents
==============================================================
11 @profile
12 def some_fn(nbr):
13 1 3 3.0 100.0 return nbr * 2
$ nosetests ex.py
.
Ran 1 test in 0.000s
对于memory_profiler,我们使用例2-22的代码。
例2-22 在单元测试时在名字空间中加入针对memory_profiler的no-op@profile修饰器
# memory_profiler
if 'profile' not in dir():
def profile(func):
def inner(*args, **kwargs):
return func(*args, **kwargs)
return inner
期望产生的输出如下:
python -m memory_profiler ex.py
...
Line # Mem usage Increment Line Contents
================================================
11 10.809 MiB 0.000 MiB @profile
12 def some_fn(nbr):
13 10.809 MiB 0.000 MiB return nbr * 2
$ nosetests ex.py
.
Ran 1 test in 0.000
不使用这些修饰器可以节省你几分钟,但是一旦你在一个破坏你代码的错误优化上失去了好几个小时,你就会想要把这个加入你的工作流程了。