分析,调试和测试是开发过程的组成部分。 您可能熟悉单元测试的概念。 单元测试是程序员编写的用于测试其代码的自动测试。 例如,这些测试可以单独测试函数或函数的一部分。 每次测试仅测试一小部分代码。 这样做的好处是提高了对代码质量的信心,可重复进行的测试,以及副作用,使代码更清晰,更正确。 单元测试还促进了协作编辑,因为通常没有人会自己理解复杂项目中的所有代码,因此,单元测试可防止贡献者破坏现有代码。 Python 对单元测试有很好的支持。 NumPy 添加了numpy.testing
包,以帮助 NumPy 对单元测试进行编码。
本章的主题包括:
- 断言
- 性能分析
- 调试
- 单元测试
NumPy 测试包具有许多工具函数,用于测试前提条件是否为真。 下表列出了 NumPy 断言函数:
函数 | 描述 |
---|---|
assert_almost_equal |
如果两个数字在指定精度上不相等,则此引发异常 |
assert_approx_equal |
如果两个数字在一定重要性上不相等,则会引发异常 |
assert_array_almost_equal |
如果两个数组在指定精度上不相等,则会引发异常 |
assert_array_equal |
如果两个数组不相等,则此引发异常 |
assert_array_less |
如果两个数组的形状不同,并且第一个数组的元素严格小于第二个数组的元素,则会引发异常 |
assert_equal |
如果两个对象不相等,则此引发异常 |
assert_raises |
如果使用定义的参数调用的可调用函数未引发指定的异常,则此操作失败 |
assert_warns |
如果未引发指定的警告,则会失败 |
assert_string_equal |
断言两个字符串相等 |
由于浮点点号的性质及其在计算机中的表示方式,我们不能像整数一样总是声明相等性。 让我们使用assert_almost_equal
函数检查它们是否相等:
-
使用较低精度调用函数(最多七位小数):
print "Decimal 6", np.testing.assert_almost_equal(0.123456789, 0.123456780, decimal=7)
请注意,不会引发异常,如以下结果所示:
Decimal 6 None
-
使用较高精度调用函数(最多八位小数):
print "Decimal 7", np.testing.assert_almost_equal(0.123456789, 0.123456780, decimal=8)
结果是:
Decimal 7 Traceback (most recent call last): … raise AssertionError(msg) AssertionError: Arrays are not almost equal ACTUAL: 0.123456789 DESIRED: 0.12345678
在本节中,我们将介绍另一个断言函数。 如果两个数字不等于一定数量的有效数字,则assert_approx_equal
函数会引发异常。 函数结果是由以下条件触发的异常:
abs(actual - expected) >= 10**-(significant - 1)
让我们从上一教程中获得数字,然后让assert_approx_equal
函数对其起作用:
-
使用低重要性调用函数:
print "Significance 8", np.testing.assert_approx_ equal(0.123456789, 0.123456780, significant=8)
结果是:
Significance 8 None
-
使用高重要性调用函数:
print "Significance 9", np.testing.assert_approx_equal(0.123456789, 0.123456780, significant=9)
引发异常:
Significance 9 Traceback (most recent call last): ... raise AssertionError(msg) AssertionError: Items are not equal to 9 significant digits: ACTUAL: 0.123456789 DESIRED: 0.12345678
有时我们需要检查两个数组是否几乎相等。 如果两个数组的指定精度不相等,assert_array_almost_equal
函数将引发异常。 该函数检查两个数组的形状是否相同。 然后,将数组的值按元素进行如下比较:
|expected - actual| < 0.5 10-decimal
让我们通过向每个数组添加零来使用上一教程中的值形成数组:
-
以较低的精度调用该函数:
print "Decimal 8", np.testing.assert_array_almost_equal([0, 0.123456789], [0, 0.123456780], decimal=8)
结果是:
Decimal 8 None
-
以较高的精度调用该函数:
print "Decimal 9", np.testing.assert_array_almost_equal([0, 0.123456789], [0, 0.123456780], decimal=9)
引发异常:
Decimal 9 Traceback (most recent call last): … assert_array_compare raise AssertionError(msg) AssertionError: Arrays are not almost equal (mismatch 50.0%) x: array([ 0\. , 0.12345679]) y: array([ 0\. , 0.12345678])
正如我们大多数人在编程课上所学的那样,过早的优化是万恶之源。 但是,一旦进入了软件开发的最后阶段,很可能是代码的某些部分不必要地变慢了,或者使用了比严格需要的更多的内存。 我们可以通过分析过程找到这些问题。 分析涉及度量指标,例如一段代码(如函数或一条语句)的执行时间。
IPython 是交互式 Python 环境,还带有与标准 Python shell 相似的 shell。 在 IPython 中,我们可以使用timeit
来分析小的代码片段。 我们还可以分析更大的脚本。 我们将展示两种方法。
-
计时一段代码:
在
pylab
模式下启动 IPythonipython -pylab
-
创建一个包含 1,000 个介于 0 到 1,000 之间的整数值的数组。
In [1]: a = arange(1000)
现在是时候搜索数组中所有内容 42 的答案了。
In [2]: %timeit searchsorted(a, 42) 100000 loops, best of 3: 7.58 us per loop
-
分析脚本:
我们将分析这个小脚本,该脚本会反转包含随机值的大小可变的矩阵:
import numpy def invert(n): a = numpy.matrix(numpy.random.rand(n, n)) return a.I sizes = 2 ** numpy.arange(0, 12)
对于
n
个大小:invert(n)
我们可以按以下方式计时:
In [1]: %run -t invert_matrix.py IPython CPU timings (estimated): User : 6.08 s. System : 0.52 s. Wall time: 19.26 s.
然后,我们可以使用
p
选项来分析脚本。In [2]: %run -p invert_matrix.py 852 function calls in 6.597 CPU seconds Ordered by: internal time ncalls tottime percall cumtime percall filename:lineno(function) 12 3.228 0.269 3.228 0.269 {numpy.linalg.lapack_lite.dgesv} 24 2.967 0.124 2.967 0.124 {numpy.core.multiarray._fastCopyAndTranspose} 12 0.156 0.013 0.156 0.013 {method 'rand' of 'mtrand.RandomState' objects} 12 0.087 0.007 0.087 0.007 {method 'copy' of 'numpy.ndarray' objects} 12 0.069 0.006 0.069 0.006 {method 'astype' of 'numpy.ndarray' objects} 12 0.025 0.002 6.304 0.525 linalg.py:404(inv) 12 0.024 0.002 6.328 0.527 defmatrix.py:808(getI) 1 0.017 0.017 6.596 6.596 invert_matrix.py:1(<module>) 24 0.014 0.001 0.014 0.001 {numpy.core.multiarray.zeros} 12 0.009 0.001 6.580 0.548 invert_matrix.py:3(invert) 12 0.000 0.000 6.264 0.522 linalg.py:244(solve) 12 0.000 0.000 0.014 0.001 numeric.py:1875(identity) 1 0.000 0.000 6.597 6.597 {execfile} 36 0.000 0.000 0.000 0.000 defmatrix.py:279(__array_finalize__) 12 0.000 0.000 2.967 0.247 linalg.py:139(_fastCopyAndTranspose) 24 0.000 0.000 0.087 0.004 defmatrix.py:233(__new__) 12 0.000 0.000 0.000 0.000 linalg.py:99(_commonType) 24 0.000 0.000 0.000 0.000 {method '__array_prepare__' of 'numpy.ndarray' objects} 36 0.000 0.000 0.000 0.000 linalg.py:66(_makearray) 36 0.000 0.000 0.000 0.000 {numpy.core.multiarray.array} 12 0.000 0.000 0.000 0.000 {method 'view' of 'numpy.ndarray' objects} 12 0.000 0.000 0.000 0.000 linalg.py:127(_to_native_byte_order) 1 0.000 0.000 6.597 6.597 interactiveshell.py:2270(safe_execfile)
列标题的解释与标准 Python 分析器的解释相同:
标头 描述 ncalls
这是调用数量。 tottime
这是在给定函数上花费的总时间(不包括在调用子函数上花费的时间)。 percall
这是 tottime
除以ncalls
的商。cumtime
这是此函数和所有子函数(从调用到退出)花费的总时间。 即使对于递归函数,此数字也是准确的。 percall
(第二个)这是 cumtime
除以原始调用的商。
调试是的其中一项,我们通过适当的单元测试来避免这些调试。 调试可能需要很长时间,而且很可能您没有时间。 因此,重要的是要系统地了解您的工具。 找到问题并实现修复后,应该进行单元测试。 这样,至少您不必再次经历调试的折磨。
我们将调试一些错误的代码,这些代码试图越界访问数组元素:
import numpy
a = numpy.arange(7)
print a[8]
继续执行以下步骤:
-
在 IPython 中运行错误的脚本。
启动
ipython
Shell。 发出以下命令,在 IPython 中运行错误的脚本:In [1]: %run buggy.py --------------------------------------------------------------------------- IndexError Traceback (most recent call last) .../site-packages/IPython/utils/py3compat.pyc in execfile(fname, *where) 173 else: 174 filename = fname --> 175 __builtin__.execfile(filename, *where) .../buggy.py in <module>() 2 3 a = numpy.arange(7) ----> 4 print a[8] IndexError: index out of bounds
-
启动调试器。
现在我们的程序崩溃了,我们可以启动调试器了。 这将在发生错误的行上设置一个断点:
In [2]: %debug > .../buggy.py(4)<module>() 2 3 a = numpy.arange(7) ----> 4 print a[8]
-
列出代码。
我们可以使用
list
命令列出代码或使用缩写l
。ipdb> list 1 import numpy 2 3 a = numpy.arange(7) ----> 4 print a[8]
-
在当前行求值代码。
现在,我们可以在当前行求值任意代码。
ipdb> len(a) 7 ipdb> print a [0 1 2 3 4 5 6]
-
查看调用栈。
我们可以使用
bt
命令查看调用栈:ipdb> bt .../py3compat.py(175)execfile() 171 if isinstance(fname, unicode): 172 filename = fname.encode(sys.getfilesystemencoding()) 173 else: 174 filename = fname --> 175 __builtin__.execfile(filename, *where) > .../buggy.py(4)<module>() 0 print a[8]
向上移动调用栈:
ipdb> u > .../site-packages/IPython/utils/py3compat.py(175)execfile() 173 else: 174 filename = fname --> 175 __builtin__.execfile(filename, *where)
下移调用栈:
ipdb> d > .../buggy.py(4)<module>() 2 3 a = numpy.arange(7) ----> 4 print a[8]
单元测试是自动化测试,用于测试一小段代码,通常是一个函数或方法。 Python 具有用于单元测试的 PyUnit API 。 作为 NumPy 的用户,我们可以使用之前在操作中看到的断言函数。
我们将为一个简单的阶乘函数编写测试。 测试将检查所谓的“快乐路径”(正常情况,并且预计将始终通过)和异常情况:
-
我们首先编写阶乘函数:
def factorial(n): if n == 0: return 1 if n < 0: raise ValueError, "Unexpected negative value" return np.arange(1, n+1).cumprod()
该代码使用了我们已经看到的
arange
和cumprod
函数来创建数组和计算累积乘积,但是我们对边界条件进行了一些检查。 -
现在我们将编写单元测试。 让我们写一个包含单元测试的类。 它从
unittest
模块扩展了TestCase
类,这是标准 Python 的一部分。 我们测试使用以下命令调用阶乘函数:-
正数,正确的方式
-
边界条件为零
-
负数,应导致错误
class FactorialTest(unittest.TestCase): def test_factorial(self): #Test for the factorial of 3 that should pass. self.assertEqual(6, factorial(3)[-1]) np.testing.assert_equal(np.array([1, 2, 6]), factorial(3)) def test_zero(self): #Test for the factorial of 0 that should pass. self.assertEqual(1, factorial(0)) def test_negative(self): #Test for the factorial of negative numbers that should fail. # It should throw a ValueError, but we expect IndexError self.assertRaises(IndexError, factorial(-10))
正如您在以下输出中看到的那样,我们将其中一项测试失败:
$ python unit_test.py .E. ====================================================================== ERROR: test_negative (__main__.FactorialTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "unit_test.py", line 26, in test_negative self.assertRaises(IndexError, factorial(-10)) File "unit_test.py", line 9, in factorial raise ValueError, "Unexpected negative value" ValueError: Unexpected negative value ---------------------------------------------------------------------- Ran 3 tests in 0.003s FAILED (errors=1)
我们为
factorial
函数代码做了一些快乐的路径测试。 我们让边界条件测试故意失败(请参见unit_test.py
),如下所示:import numpy as np import unittest def factorial(n): if n == 0: return 1 if n < 0: raise ValueError, "Unexpected negative value" return np.arange(1, n+1).cumprod() class FactorialTest(unittest.TestCase): def test_factorial(self): #Test for the factorial of 3 that should pass. self.assertEqual(6, factorial(3)[-1]) np.testing.assert_equal(np.array([1, 2, 6]), factorial(3)) def test_zero(self): #Test for the factorial of 0 that should pass. self.assertEqual(1, factorial(0)) def test_negative(self): #Test for the factorial of negative numbers that should fail. # It should throw a ValueError, but we expect IndexError self.assertRaises(IndexError, factorial(-10)) if __name__ == '__main__': unittest.main()
-
nose
是一个 Python 框架,它使(单元)测试更加容易。 鼻子可以帮助您组织测试。 根据nose
文档:
任何与testMatch
正则表达式匹配的 python 源文件,目录或包(默认情况下:(?:^|[b_.-])[Tt]est)
)都将作为测试被收集。
鼻子广泛使用装饰器。 Python 装饰器是指示有关方法或函数的注解。 numpy.testing
模块具有许多装饰器:
装饰器 | 描述 |
---|---|
numpy.testing.decorators.deprecated |
这是运行测试时过滤器的过时警告 |
numpy.testing.decorators.knownfailureif |
这会根据条件引发KnownFailureTest 异常。 |
numpy.testing.decorators.setastest |
该将函数标记为测试或非测试。 |
numpy.testing.decorators.skipif |
这会根据条件引发SkipTest 异常。 |
numpy.testing.decorators.slow |
这将测试函数或方法标记为缓慢。 |
此外,我们可以调用decorate_methods
函数,以将修饰符应用于与正则表达式或字符串匹配的类的方法。
我们将直接将 setastest
装饰器应用于测试函数。 然后,我们将相同的装饰器应用于禁用它的方法。 另外,我们将跳过其中一项测试,并通过另一项测试。 首先,如果您还没有安装 nose
,请按以下步骤安装:
-
使用安装工具安装
nose
,如下所示:easy_install nose
或点子:
pip install nose
-
直接应用装饰器,如下所示:
我们将一个函数作为测试,将另一个函数作为非测试:
@setastest(False) def test_false(): pass @setastest(True) def test_true(): pass
-
跳过测试如下:
我们可以使用
skipif
装饰器跳过测试。 让我们使用一个总是导致测试被跳过的条件:@skipif(True) def test_skip(): pass
-
使用
knownfailureif
装饰器的失败测试如下:添加一个始终通过的测试函数。 然后使用
knownfailureif
装饰器对其进行装饰,以使测试始终失败:@knownfailureif(True) def test_alwaysfail(): pass
-
定义测试类,如下所示:
我们将使用通常应由鼻子执行的方法来定义一些测试类:
class TestClass(): def test_true2(self): pass class TestClass2(): def test_false2(self): pass
-
禁用测试方法如下:
让我们从上一步中禁用第二种测试方法:
decorate_methods(TestClass2, setastest(False), 'test_false2')
-
运行测试,如下所示:
我们可以使用以下命令运行测试:
nosetests -v decorator_setastest.py decorator_setastest.TestClass.test_true2 ... ok decorator_setastest.test_true ... ok decorator_test.test_skip ... SKIP: Skipping test: test_skipTest skipped due to test condition decorator_test.test_alwaysfail ... ERROR ====================================================================== ERROR: decorator_test.test_alwaysfail ---------------------------------------------------------------------- Traceback (most recent call last): File "…/nose/case.py", line 197, in runTest self.test(*self.arg) File …/numpy/testing/decorators.py", line 213, in knownfailer raise KnownFailureTest(msg) KnownFailureTest: Test skipped due to known failure ---------------------------------------------------------------------- Ran 4 tests in 0.001s FAILED (SKIP=1, errors=1)
-
我们将某些函数和方法修饰为未经测试,以便
nose
忽略它们。 我们跳过了一项测试,也没有通过另一项测试。 我们通过直接应用装饰器并使用以下decorate_methods
函数(请参见decorator_test.py
)来做到这一点:from numpy.testing.decorators import setastest from numpy.testing.decorators import skipif from numpy.testing.decorators import knownfailureif from numpy.testing import decorate_methods @setastest(False) def test_false(): pass @setastest(True) def test_true(): pass @skipif(True) def test_skip(): pass @knownfailureif(True) def test_alwaysfail(): pass class TestClass(): def test_true2(self): pass class TestClass2(): def test_false2(self): pass decorate_methods(TestClass2, setastest(False), 'test_false2')
在本章中,我们了解了测试和 NumPy 测试工具。 我们介绍了单元测试,断言函数,性能分析和调试。 单元测试是一种标准做法,因为它应该为您提供质量更好的代码,并且回归风险低。 NumPy 提供断言函数来帮助您进行单元测试。 在本章中,我们介绍了其中一些函数。 无论您的单元测试有多好,在某个时候,您都必须进行性能分析和调试,因此在这方面给出了指针。
下一章的主题是科学的 Python 生态系统以及 NumPy 如何融入其中。