Skip to content

Latest commit

 

History

History
637 lines (449 loc) · 20.5 KB

File metadata and controls

637 lines (449 loc) · 20.5 KB

六、性能分析,调试和测试

分析,调试和测试是开发过程的组成部分。 您可能熟悉单元测试的概念。 单元测试是程序员编写的用于测试其代码的自动测试。 例如,这些测试可以单独测试函数或函数的一部分。 每次测试仅测试一小部分代码。 这样做的好处是提高了对代码质量的信心,可重复进行的测试,以及副作用,使代码更清晰,更正确。 单元测试还促进了协作编辑,因为通常没有人会自己理解复杂项目中的所有代码,因此,单元测试可防止贡献者破坏现有代码。 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函数

由于浮点点号的性质及其在计算机中的表示方式,我们不能像整数一样总是声明相等性。 让我们使用assert_almost_equal函数检查它们是否相等:

  1. 使用较低精度调用函数(最多七位小数):

    print "Decimal 6", np.testing.assert_almost_equal(0.123456789, 0.123456780, decimal=7)

    注意

    请注意,不会引发异常,如以下结果所示:

    Decimal 6 None
  2. 使用较高精度调用函数(最多八位小数):

    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函数对其起作用:

  1. 使用低重要性调用函数:

    print "Significance 8", np.testing.assert_approx_
    equal(0.123456789, 0.123456780,
    significant=8)

    结果是:

    Significance 8 None
  2. 使用高重要性调用函数:

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

有时我们需要检查两个数组是否几乎相等。 如果两个数组的指定精度不相等,assert_array_almost_equal函数将引发异常。 该函数检查两个数组的形状是否相同。 然后,将数组的值按元素进行如下比较:

|expected - actual| < 0.5 10-decimal

让我们通过向每个数组添加零来使用上一教程中的值形成数组:

  1. 以较低的精度调用该函数:

    print "Decimal 8", np.testing.assert_array_almost_equal([0, 0.123456789], [0, 0.123456780], decimal=8)

    结果是:

    Decimal 8
    None
  2. 以较高的精度调用该函数:

    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 分析程序

正如我们大多数人在编程课上所学的那样,过早的优化是万恶之源。 但是,一旦进入了软件开发的最后阶段,很可能是代码的某些部分不必要地变慢了,或者使用了比严格需要的更多的内存。 我们可以通过分析过程找到这些问题。 分析涉及度量指标,例如一段代码(如函数或一条语句)的执行时间。

IPython 是交互式 Python 环境,还带有与标准 Python shell 相似的 shell。 在 IPython 中,我们可以使用timeit来分析小的代码片段。 我们还可以分析更大的脚本。 我们将展示两种方法。

  1. 计时一段代码:

    pylab模式下启动 IPython

    ipython -pylab
  2. 创建一个包含 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
  3. 分析脚本:

    我们将分析这个小脚本,该脚本会反转包含随机值的大小可变的矩阵:

    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除以原始调用的商。

使用 IPython 进行调试

调试是的其中一项,我们通过适当的单元测试来避免这些调试。 调试可能需要很长时间,而且很可能您没有时间。 因此,重要的是要系统地了解您的工具。 找到问题并实现修复后,应该进行单元测试。 这样,至少您不必再次经历调试的折磨。

我们将调试一些错误的代码,这些代码试图越界访问数组元素:

import numpy

a = numpy.arange(7)
print a[8]

继续执行以下步骤:

  1. 在 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
  2. 启动调试器。

    现在我们的程序崩溃了,我们可以启动调试器了。 这将在发生错误的行上设置一个断点:

    In [2]: %debug
    > .../buggy.py(4)<module>()
     2 
     3 a = numpy.arange(7)
    ----> 4 print a[8]
  3. 列出代码。

    我们可以使用list命令列出代码或使用缩写l

    ipdb> list
     1 import numpy
     2 
     3 a = numpy.arange(7)
    ----> 4 print a[8]
  4. 在当前行求值代码。

    现在,我们可以在当前行求值任意代码。

    ipdb> len(a)
    7
    
    ipdb> print a
    [0 1 2 3 4 5 6]
  5. 查看调用栈。

    我们可以使用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 的用户,我们可以使用之前在操作中看到的断言函数。

我们将为一个简单的阶乘函数编写测试。 测试将检查所谓的“快乐路径”(正常情况,并且预计将始终通过)和异常情况:

  1. 我们首先编写阶乘函数:

    def factorial(n):
       if n == 0:
          return 1
    
       if n < 0:
          raise ValueError, "Unexpected negative value"
    
       return np.arange(1, n+1).cumprod()

    该代码使用了我们已经看到的arangecumprod函数来创建数组和计算累积乘积,但是我们对边界条件进行了一些检查。

  2. 现在我们将编写单元测试。 让我们写一个包含单元测试的类。 它从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测试装饰器

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,请按以下步骤安装:

  1. 使用安装工具安装nose,如下所示:

    easy_install nose

    或点子:

    pip install nose
  2. 直接应用装饰器,如下所示:

    我们将一个函数作为测试,将另一个函数作为非测试:

    @setastest(False)
    def test_false():
       pass
    
    @setastest(True)
    def test_true():
       pass
  3. 跳过测试如下:

    我们可以使用skipif装饰器跳过测试。 让我们使用一个总是导致测试被跳过的条件:

    @skipif(True)
    def test_skip():
       pass
  4. 使用knownfailureif装饰器的失败测试如下:

    添加一个始终通过的测试函数。 然后使用knownfailureif装饰器对其进行装饰,以使测试始终失败:

    @knownfailureif(True)
    def test_alwaysfail():
         pass
  5. 定义测试类,如下所示:

    我们将使用通常应由鼻子执行的方法来定义一些测试类:

    class TestClass():
       def test_true2(self):
          pass
    
    class TestClass2():
       def test_false2(self):
          pass
  6. 禁用测试方法如下:

    让我们从上一步中禁用第二种测试方法:

    decorate_methods(TestClass2, setastest(False), 'test_false2')
  7. 运行测试,如下所示:

    我们可以使用以下命令运行测试:

    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)
  8. 我们将某些函数和方法修饰为未经测试,以便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 如何融入其中。