NumPy 秘籍中文第二版:八、质量保证

发布时间 2023-04-12 22:07:49作者: ApacheCN

原文:NumPy Cookbook - Second Edition

协议:CC BY-NC-SA 4.0

译者:飞龙

“如果您对计算机撒谎,它将帮助您。”

-- Perry Farrar,ACM 通讯,第 28 卷

在本章中,我们将介绍以下秘籍:

  • 安装 Pyflakes
  • 使用 Pyflakes 执行静态分析
  • 用 Pylint 分析代码
  • 使用 Pychecker 执行静态分析
  • 使用docstrings测试代码
  • 编写单元测试
  • 使用模拟测试代码
  • 以 BDD 方式来测试

简介

与普遍的看法相反,质量保证与其说是发现错误,不如说是发现它们。 我们将讨论两种提高代码质量,从而防止出现问题的方法。 首先,我们将对已经存在的代码进行静态分析。 然后,我们将讨论单元测试; 这包括模拟和行为驱动开发BDD)。

安装 Pyflakes

Pyflakes 是 Python 代码分析包。 它可以分析代码并发现潜在的问题,例如:

  • 未使用的导入
  • 未使用的变量

准备

如有必要,请安装pipeasy_install

操作步骤

选择以下之一来安装pyflakes

  • 使用pip命令安装 pyflakes:

    $ sudo pip install pyflakes
    
    
  • 使用easy_install命令安装 Pyflakes:

    $ sudo easy_install pyflakes
    
    
  • 这是在 Linux 上安装此包的两种方法:

    Linux 包的名称也为pyflakes。 例如,在 RedHat 上执行以下操作:

    $ sudo yum install pyflakes
    
    

    在 Debian/Ubuntu 上,命令如下:

    $ sudo apt-get install pyflakes
    
    

另见

使用 Pyflakes 执行静态分析

我们将对 NumPy 代码库的一部分执行静态分析。 为此,我们将使用 Git 签出代码。 然后,我们将使用pyflakes对部分代码进行静态分析。

操作步骤

要检查 NumPy 代码中,我们需要 Git。 安装 Git 超出了本书的范围

  1. 用 Git 命令检索代码如下:

    $ git clone git://github.com/numpy/numpy.git numpy
    
    

    或者,从这里下载源档案。

  2. 上一步使用完整的 NumPy 代码创建一个numpy目录。 转到此目录,并在其中运行以下命令:

    $ pyflakes *.py
    pavement.py:71: redefinition of unused 'md5' from line 69
    pavement.py:88: redefinition of unused 'GIT_REVISION' from line 86
    pavement.py:314: 'virtualenv' imported but unused
    pavement.py:315: local variable 'e' is assigned to but never used
    pavement.py:380: local variable 'sdir' is assigned to but never used
    pavement.py:381: local variable 'bdir' is assigned to but never used
    pavement.py:536: local variable 'st' is assigned to but never used
    setup.py:21: 're' imported but unused
    setup.py:27: redefinition of unused 'builtins' from line 25
    setup.py:124: redefinition of unused 'GIT_REVISION' from line 118
    setupegg.py:17: 'setup' imported but unused
    setupscons.py:61: 'numpy' imported but unused
    setupscons.py:64: 'numscons' imported but unused
    setupsconsegg.py:6: 'setup' imported but unused
    
    

    这将对代码样式进行分析,并检查当前目录中所有 Python 脚本中的 PEP-8 违规情况。 如果愿意,还可以分析单个文件。

工作原理

正如您所见,分析代码样式并使用 Pyflakes 查找违反 PEP-8 的行为非常简单。 另一个优点是速度。 但是,Pyflakes 报告的错误类型的数量是有限的。

使用 Pylint 分析代码

Pylint 是另一个由 Logilab 创建的开源静态分析器 。 Pylint 比 Pyflakes 更复杂; 它允许更多的自定义和代码检查。 但是,它比 Pyflakes 慢。 有关更多信息,请参见手册

在本秘籍中,我们再次从 Git 存储库下载 NumPy 代码-为简便起见,省略了此步骤。

准备

您可以从源代码发行版中安装 Pylint。 但是,有很多依赖项,因此最好使用easy_installpip进行安装。 安装命令如下:

$ easy_install pylint
$ sudo pip install pylint

操作步骤

我们将再次从 NumPy 代码库的顶部目录进行分析。 注意,我们得到了更多的输出。 实际上,Pylint 打印了太多文本,因此在这里大部分都必须省略:

$ pylint *.py
No config file found, using default configuration
************* Module pavement
C: 60: Line too long (81/80)
C:139: Line too long (81/80)
...
W: 50: TODO
W:168: XXX: find out which env variable is necessary to avoid the pb with python
W: 71: Reimport 'md5' (imported line 143)
F: 73: Unable to import 'paver'
F: 74: Unable to import 'paver.easy'
C: 79: Invalid name "setup_py" (should match (([A-Z_][A-Z0-9_]*)|(__.*__))$)
F: 86: Unable to import 'numpy.version'
E: 86: No name 'version' in module 'numpy'
C:149: Operator not followed by a space
if sys.platform =="darwin":
 ^^
C:202:prepare_nsis_script: Missing docstring
W:228:bdist_superpack: Redefining name 'options' from outer scope (line 74)
C:231:bdist_superpack.copy_bdist: Missing docstring
W:275:bdist_wininst_nosse: Redefining name 'options' from outer scope (line 74)

工作原理

Pylint 默认输出原始文本; 但是我们可以根据需要请求 HTML 输出。 消息具有以下格式:

MESSAGE_TYPE: LINE_NUM:[OBJECT:] MESSAGE

消息类型可以是以下之一:

  • [R]:这意味着建议进行重构
  • [C]:这意味着存在违反代码风格的情况
  • [W]:用于警告小问题
  • [E]:用于错误或潜在的错误
  • [F]:这表明发生致命错误,阻止了进一步的分析

另见

  • 使用 Pyflakes 执行静态分析

使用 Pychecker 执行静态分析

Pychecker 是一个古老的静态分析工具。 它不是十分活跃的开发工具,但它在此提到的速度又足够好。 在编写本书时,最新版本是 0.8.19,最近一次更新是在 2011 年。Pychecker 尝试导入每个模块并对其进行处理。 然后,它搜索诸如传递不正确数量的参数,使用不存在的方法传递不正确的格式字符串以及其他问题之类的问题。 在本秘籍中,我们将再次分析代码,但是这次使用 Pychecker。

操作步骤

  1. Sourceforge 下载tar.gz。 解压缩源归档文件并运行以下命令:

    $ python setup.py install
    
    

    或者,使用pip安装 Pychecker:

    $ sudo pip install http://sourceforge.net/projects/pychecker/files/pychecker/0.8.19/pychecker-0.8.19.tar.gz/download
    
    
  2. 分析代码,就像先前的秘籍一样。 我们需要的命令如下:

    $ pychecker *.py
    ...
    Warnings...
    
    ...
    
    setup.py:21: Imported module (re) not used
    setup.py:27: Module (builtins) re-imported
    
    ...
    

使用文档字符串测试代码

Doctests 是注释字符串,它们嵌入在类似交互式会话的 Python 代码中。 这些字符串可用于测试某些假设或仅提供示例。 我们需要使用doctest模块来运行这些测试。

让我们写一个简单的示例,该示例应该计算阶乘,但不涵盖所有可能的边界条件。 换句话说,某些测试将失败。

操作步骤

  1. 用将通过的测试和将失败的另一个测试编写docstringdocstring文本应类似于在 Python shell 中通常看到的文本:

    """
    Test for the factorial of 3 that should pass.
    >>> factorial(3)
    6
    
    Test for the factorial of 0 that should fail.
    >>> factorial(0)
    1
    """
    
    
  2. 编写以下 NumPy 代码:

    return np.arange(1, n+1).cumprod()[-1]
    

    我们希望这段代码有时会故意失败。 它将创建一个序列号数组,计算该数组的累积乘积,并返回最后一个元素。

  3. 使用doctest模块运行测试:

    doctest.testmod()
    

    以下是本书代码包中docstringtest.py文件的完整测试示例代码:

    import numpy as np
    import doctest
    
    def factorial(n):
       """
       Test for the factorial of 3 that should pass.
       >>> factorial(3)
       6
    
       Test for the factorial of 0 that should fail.
       >>> factorial(0)
       1
       """
       return np.arange(1, n+1).cumprod()[-1]
    
    doctest.testmod()
    

    我们可以使用-v选项获得详细的输出,如下所示:

    $ python docstringtest.py -v
    Trying:
     factorial(3)
    Expecting:
     6
    ok
    Trying:
     factorial(0)
    Expecting:
     1
    **********************************************************************
    File "docstringtest.py", line 11, in __main__.factorial
    Failed example:
     factorial(0)
    Exception raised:
     Traceback (most recent call last):
     File ".../doctest.py", line 1253, in __run
     compileflags, 1) in test.globs
     File "<doctest __main__.factorial[1]>", line 1, in <module>
     factorial(0)
     File "docstringtest.py", line 14, in factorial
     return numpy.arange(1, n+1).cumprod()[-1]
     IndexError: index out of bounds
    1 items had no tests:
     __main__
    **********************************************************************
    1 items had failures:
     1 of   2 in __main__.factorial
    2 tests in 2 items.
    1 passed and 1 failed.
    ***Test Failed*** 1 failures.
    
    

工作原理

如您所见,我们没有考虑零和负数。 实际上,由于数组为空,我们出现了index out of bounds错误。 当然,这很容易解决,我们将在下一个教程中进行。

另见

编写单元测试

测试驱动开发TDD)是本世纪软件开发诞生的最好的事情。 TDD 的最重要方面之一是,几乎把重点放在单元测试上。

注意

TDD 方法使用所谓的测试优先方法,在此方法中,我们首先编写一个失败的测试,然后编写相应的代码以通过测试。 测试应记录开发人员的意图,但要比功能设计的水平低。 一组测试通过降低回归概率来增加置信度,并促进重构。

单元测试是自动测试,通常测试一小段代码,通常是一个函数或方法。 Python 具有用于单元测试的 PyUnit API。 作为 NumPy 的用户,我们也可以使用numpy.testing模块中的便捷函数。 顾名思义,该模块专用于测试。

操作步骤

让我们编写一些代码进行测试:

  1. 首先编写以下factorial()函数:

    def factorial(n):
      if n == 0:
        return 1
    
      if n < 0:
        raise ValueError, "Don't be so negative"
    
      return np.arange(1, n+1).cumprod()
    

    该代码与前面的秘籍中的代码相同,但是我们添加了一些边界条件检查。

  2. 让我们写一个类; 此类将包含单元测试。 它从unittest模块扩展了TestCase类,是 Python 标准测试的一部分。 我们通过调用factorial()函数并运行以下代码来运行测试:

    • 一个正数-幸福的道路!

    • 边界条件等于0

    • 负数,这将导致错误:

      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))
      

      factorial()函数和整个单元测试的代码如下:

      import numpy as np
      import unittest
      
      def factorial(n):
         if n == 0:
            return 1
      
         if n < 0:
            raise ValueError, "Don't be so negative"
      
         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()
      

      负数测试失败,如以下输出所示:

      .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, "Don't be so negative"
      ValueError: Don't be so negative
      
      ----------------------------------------------------------------------
      Ran 3 tests in 0.001s
      
      FAILED (errors=1)
      
      

工作原理

我们看到了如何使用标准unittest Python 模块实现简单的单元测试。 我们编写了一个测试类 ,该类从unittest模块扩展了TestCase类。 以下函数用于执行各种测试:

函数 描述
numpy.testing.assert_equal() 测试两个 NumPy 数组是否相等
unittest.assertEqual() 测试两个值是否相等
unittest.assertRaises() 测试是否引发异常

testing NumPy 包具有许多我们应该了解的测试函数,如下所示:

函数 描述
assert_almost_equal() 如果两个数字不等于指定的精度,则此函数引发异常
assert_approx_equal() 如果两个数字在一定意义上不相等,则此函数引发异常
assert_array_almost_equal() 如果两个数组不等于指定的精度,此函数会引发异常
assert_array_equal() 如果两个数组不相等,则此函数引发异常
assert_array_less() 如果两个数组的形状不同,并且此函数引发异常,则第一个数组的元素严格小于第二个数组的元素
assert_raises() 如果使用定义的参数调用的可调用对象未引发指定的异常,则此函数将失败
assert_warns() 如果未抛出指定的警告,则此函数失败
assert_string_equal() 此函数断言两个字符串相等

使用模拟测试代码

模拟是用来代替真实对象的对象,目的是测试真实对象的部分行为。 如果您看过电影《身体抢夺者》,您可能已经对基本概念有所了解。 一般来说, 仅在被测试的真实对象的创建成本很高(例如数据库连接)或测试可能产生不良副作用时才有用。 例如,我们可能不想写入文件系统或数据库。

在此秘籍中,我们将测试一个核反应堆,当然不是真正的反应堆! 此类核反应堆执行阶乘计算,从理论上讲,它可能导致连锁反应,进而导致核灾难。 我们将使用mock包通过模拟来模拟阶乘计算。

操作步骤

首先,我们将安装mock包; 之后,我们将创建一个模拟并测试一段代码:

  1. 要安装mock包,请执行以下命令:

    $ sudo easy_install mock
    
    
  2. 核反应堆类有一个do_work()方法,该方法调用了我们要模拟的危险的factorial()方法。 创建一个模拟,如下所示:

    reactor.factorial = MagicMock(return_value=6)
    

    这样可以确保模拟返回值6

  3. 我们可以通过多种方式检查模拟的行为,然后从中检查真实对象的行为。 例如,断言使用正确的参数调用了潜在爆炸性的factorial()方法,如下所示:

    reactor.factorial.assert_called_with(3, "mocked")
    

    带有模拟的完整测试代码如下:

    from __future__ import print_function
    from mock import MagicMock
    import numpy as np
    import unittest
    
    class NuclearReactor():
       def __init__(self, n):
          self.n = n
    
       def do_work(self, msg):
          print("Working")
    
          return self.factorial(self.n, msg)
    
       def factorial(self, n, msg):
          print(msg)
    
          if n == 0:
             return 1
    
          if n < 0:
             raise ValueError, "Core meltdown"
    
          return np.arange(1, n+1).cumprod()
    
    class NuclearReactorTest(unittest.TestCase):
       def test_called(self):
          reactor = NuclearReactor(3)
          reactor.factorial = MagicMock(return_value=6)
          result = reactor.do_work("mocked")
          self.assertEqual(6, result)
          reactor.factorial.assert_called_with(3, "mocked")
    
       def test_unmocked(self):
          reactor = NuclearReactor(3)
          reactor.factorial(3, "unmocked")
          np.testing.assert_raises(ValueError)
    
    if __name__ == '__main__':
        unittest.main()
    

我们将一个字符串传递给factorial()方法,以显示带有模拟的代码不会执行实际的代码。 该单元测试的工作方式与上一秘籍中的单元测试相同。 这里的第二项测试不测试任何内容。 第二个测试的目的只是演示,如果我们在没有模拟的情况下执行真实代码,会发生什么。

测试的输出如下:

Working
.unmocked
.
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

工作原理

模拟没有任何行为。 他们就像外星人的克隆人,假装是真实的人。 只能比外星人傻—外星人克隆人无法告诉您被替换的真实人物的生日。 我们需要设置它们以适当的方式进行响应。 例如,在此示例中,模拟返回6 。 我们可以记录模拟发生了什么,被调用了多少次以及使用了哪些参数。

另见

以 BDD 方式来测试

BDD行为驱动开发)是您可能遇到的另一个热门缩写。 在 BDD 中,我们首先根据某些约定和规则定义(英语)被测系统的预期行为。 在本秘籍中,我们将看到这些约定的示例。

这种方法背后的想法是,我们可以让可能无法编程或编写测试大部分内容的人员参加。 这些人编写的功能采用句子的形式,包括多个步骤。 每个步骤或多或少都是我们可以编写的单元测试,例如,使用 NumPy。 有许多 Python BDD 框架。 在本秘籍中,我们使用 Lettuce 来测试阶乘函数。

操作步骤

在本节中,您将学习如何安装 Lettuce,设置测试以及编写测试规范:

  1. 要安装 Lettuce,请运行以下命令之一:

    $ pip install lettuce
    $ sudo easy_install lettuce
    
    
  2. Lettuce 需要特殊的目录结构进行测试。 在tests目录中,我们将有一个名为features的目录,其中包含factorial.feature文件,以及steps.py文件中的功能说明和测试代码:

    ./tests:
    features
    
    ./tests/features:
    factorial.feature	steps.py
    
    
  3. 提出业务需求是一项艰巨的工作。 以易于测试的方式将其全部写下来更加困难。 幸运的是,这些秘籍的要求非常简单-我们只需写下不同的输入值和预期的输出。 我们在GivenWhenThen部分中有不同的方案,它们对应于不同的测试步骤。 为阶乘函数定义以下三种方案:

    Feature: Compute factorial
    
        Scenario: Factorial of 0
          Given I have the number 0 
          When I compute its factorial 
          Then I see the number 1
    
        Scenario: Factorial of 1
          Given I have the number 1 
          When I compute its factorial 
          Then I see the number 1
    
        Scenario: Factorial of 3
          Given I have the number 3 
          When I compute its factorial 
          Then I see the number 1, 2, 6
    
  4. 我们将定义与场景步骤相对应的方法。 要特别注意用于注释方法的文本。 它与业务场景文件中的文本匹配,并且我们使用正则表达式获取输入参数。 在前两个方案中,我们匹配数字,在最后一个方案中,我们匹配任何文本。 fromstring() NumPy 函数用于从 NumPy 数组创建字符串,字符串中使用整数数据类型和逗号分隔符。 以下代码测试了我们的方案:

    from lettuce import *
    import numpy as np
    
    @step('I have the number (\d+)')
    def have_the_number(step, number):
        world.number = int(number)
    
    @step('I compute its factorial')
    def compute_its_factorial(step):
        world.number = factorial(world.number)
    
    @step('I see the number (.*)')
    def check_number(step, expected):
        expected = np.fromstring(expected, dtype=int, sep=',')
        np.testing.assert_equal(world.number, expected, \
            "Got %s" % world.number)
    
    def factorial(n):
       if n == 0:
          return 1
    
       if n < 0:
          raise ValueError, "Core meltdown"
    
       return np.arange(1, n+1).cumprod()
    
  5. 要运行测试,请转到tests目录,然后键入以下命令:

    $ lettuce
    
    Feature: Compute factorial        # features/factorial.feature:1
    
     Scenario: Factorial of 0        # features/factorial.feature:3
     Given I have the number 0     # features/steps.py:5
     When I compute its factorial  # features/steps.py:9
     Then I see the number 1       # features/steps.py:13
    
     Scenario: Factorial of 1        # features/factorial.feature:8
     Given I have the number 1     # features/steps.py:5
     When I compute its factorial  # features/steps.py:9
     Then I see the number 1       # features/steps.py:13
    
     Scenario: Factorial of 3        # features/factorial.feature:13
     Given I have the number 3     # features/steps.py:5
     When I compute its factorial  # features/steps.py:9
     Then I see the number 1, 2, 6 # features/steps.py:13
    
    1 feature (1 passed)
    3 scenarios (3 passed)
    9 steps (9 passed)
    
    

工作原理

我们定义了具有三个方案和相应步骤的函数。 我们使用 NumPy 的测试函数来测试不同步骤,并使用fromstring()函数从规格文本创建 NumPy 数组。

另见