【问题标题】:Compare (assert equality of) two complex data structures containing numpy arrays in unittest在 unittest 中比较(断言相等)两个包含 numpy 数组的复杂数据结构
【发布时间】:2012-12-24 04:40:21
【问题描述】:

我使用 Python 的 unittest 模块,想检查两个复杂的数据结构是否相等。对象可以是具有各种值的字典列表:数字、字符串、Python 容器(列表/元组/字典)和numpy 数组。后者是问这个问题的原因,因为我不能这样做

self.assertEqual(big_struct1, big_struct2)

因为它会产生一个

ValueError: The truth value of an array with more than one element is ambiguous.
Use a.any() or a.all()

我想我需要为此编写自己的相等测试。它应该适用于任意结构。我目前的想法是一个递归函数:

  • 尝试将arg1的当前“节点”与arg2的对应节点直接比较;
  • 如果没有引发异常,则继续(“终端”节点/叶子也在此处处理);
  • 如果ValueError 被捕获,则继续深入,直到找到numpy.array
  • 比较数组(例如like this)。

似乎有点问题的是跟踪两个结构的“对应”节点,但也许zip 就是我所需要的。

问题是:有没有比这种方法更好(更简单)的替代方法?也许numpy 提供了一些工具?如果没有建议替代方案,我将实施这个想法(除非我有更好的想法)并发布作为答案。

附:我有一种模糊的感觉,我可能已经看到了解决这个问题的问题,但我现在找不到。

附言另一种方法是遍历结构并将所有numpy.arrays 转换为列表的函数,但这更容易实现吗?在我看来是一样的。


编辑:子类化numpy.ndarray 听起来很有希望,但显然我没有将比较的双方都硬编码到测试中。不过,其中一个确实是硬编码的,所以我可以:

  • 使用numpy.array 的自定义子类填充它;
  • jterrace's answer 中将isinstance(other, SaneEqualityArray) 更改为isinstance(other, np.ndarray)
  • 在比较中始终将其用作 LHS。

我在这方面的问题是:

  1. 它会起作用吗(我的意思是,这听起来不错,但可能无法正确处理一些棘手的边缘情况)?在递归相等检查中,我的自定义对象是否总是以 LHS 结尾,正如我所料?
  2. 再说一次,有没有更好的方法(假设我得到了至少一个具有真实numpy 数组的结构)。

编辑 2:我试过了,(看似)有效的实现在 this answer 中显示。

【问题讨论】:

  • 我想编写一个适用于任意数据结构的相等测试会非常困难。这些真的没有固定的结构吗?
  • @goncalopp 其中有几个,相当复杂,理论上可能会发生变化。我不想依赖它,特别是因为我不知道有什么方法可以比较两个结构中的除了X 之外的所有内容,即使我知道X 在哪里。
  • 然后,就个人而言,我会采用递归函数方法。不过,我会先明确检查对象的type - 进行盲目比较,因为第一步可能是合理的,但如果您的数据结构很大,则将是浪费,因为如果@ 987654343@ 被提出。
  • @goncalopp 感谢您的意见。性能不是关键问题,这仅用于测试目的。我更关心的是尽量减少实施和维护解决方案所需的工作量。

标签: python unit-testing numpy


【解决方案1】:

本来想评论的,但是太长了……

有趣的是,您不能使用== 来测试数组是否相同,我建议您改用np.testing.assert_array_equal

  1. 检查数据类型、形状等,
  2. 对于(float('nan') == float('nan')) == False 的简洁小数学并没有失败(正常的python 序列== 有一种更有趣的方式来忽略这个有时,因为它使用PyObject_RichCompareBool一个(对于 NaN 不正确)is 快速检查(对于测试当然是完美的)...
  3. 还有assert_allclose,因为如果您进行实际计算,浮点相等会变得非常棘手,并且您通常希望几乎相同的值,因为这些值可能取决于硬件或可能是随机的,具体取决于什么你和他们一起做。

如果您想要这种疯狂嵌套的东西,我几乎建议您尝试使用 pickle 对其进行序列化,但这过于严格(并且第 3 点当然完全被破坏了),例如您的数组的内存布局无关紧要,但是对其序列化很重要。

【讨论】:

  • picklehas its own problems序列化...了解numpy.testing实用程序很好,但我仍然不确定如何在这里应用它们。
【解决方案2】:

assertEqual 函数将调用对象的__eq__ 方法,该方法应针对复杂数据类型进行递归。例外是 numpy,它没有健全的 __eq__ 方法。使用numpy subclass from this question,您可以恢复平等行为的理智:

import copy
import numpy
import unittest

class SaneEqualityArray(numpy.ndarray):
    def __eq__(self, other):
        return (isinstance(other, SaneEqualityArray) and
                self.shape == other.shape and
                numpy.ndarray.__eq__(self, other).all())

class TestAsserts(unittest.TestCase):

    def testAssert(self):
        tests = [
            [1, 2],
            {'foo': 2},
            [2, 'foo', {'d': 4}],
            SaneEqualityArray([1, 2]),
            {'foo': {'hey': SaneEqualityArray([2, 3])}},
            [{'foo': SaneEqualityArray([3, 4]), 'd': {'doo': 3}},
             SaneEqualityArray([5, 6]), 34]
        ]
        for t in tests:
            self.assertEqual(t, copy.deepcopy(t))

if __name__ == '__main__':
    unittest.main()

此测试通过。

【讨论】:

  • 我不会和__eq__ 混在一起,而是很多 宁愿和__nonzero__ 混在一起。尽管 numpy 这样做是有充分理由的,但这只会引发错误。
  • 非常感谢您的链接(现在看来我以前见过它)!但我正在测试一个函数,它返回带有numpy.array 的东西,而不是它的自定义子类。我将如何在断言之前进行转换? (否则我需要从__eq__ 中删除isinstance(other, SaneEqualityArray),这是个好主意吗?)
  • 还有@seberg,您能否详细说明您对覆盖__eq__ 的担忧? (也许为我说明关于__nonzero__ 的建议:我认为它需要在A-B 上调用?所以A-B 应该给出子类的一个实例?这是否意味着我还需要覆盖@987654335 @?)
  • @LevLevitsky 导致错误的原因不是__eq__,导致错误且定义不明确的是__nonzero__(即bool(np.ndarray)),更改__nonzero__ 可能不会改变工作程序,除非它依赖于抛出的错误。对我来说似乎是一个很大的优势......
  • @seberg 谢谢,但我不打算更改我测试的函数中的任何内容。我仍然希望他们将numpy.array 放在返回值中,而不是它的自定义子类。这是为了回应您对“更改工作程序”的担忧。
【解决方案3】:

所以jterrace 说明的想法似乎对我有用,只需稍作修改:

class SaneEqualityArray(np.ndarray):
    def __eq__(self, other):
        return (isinstance(other, np.ndarray) and self.shape == other.shape and 
            np.allclose(self, other))

就像我说的,包含这些对象的容器应该在相等检查的左侧。我从现有的 numpy.ndarrays 创建 SaneEqualityArray 对象,如下所示:

SaneEqualityArray(my_array.shape, my_array.dtype, my_array)

按照ndarray构造函数签名:

ndarray(shape, dtype=float, buffer=None, offset=0,
        strides=None, order=None)

此类在测试套件中定义,仅用于测试目的。相等检查的 RHS 是被测试函数返回的一个实际对象,包含真实的numpy.ndarray 对象。

附:感谢迄今为止发布的两个答案的作者,他们都非常有帮助。如果有人发现这种方法有任何问题,我会很感激您的反馈。

【讨论】:

    【解决方案4】:

    我将定义我自己的 assertNumpyArraysEqual() 方法,该方法明确地进行您想要使用的比较。这样,您的生产代码不会改变,但您仍然可以在单元测试中做出合理的断言。确保在包含__unittest = True 的模块中定义它,这样它就不会包含在堆栈跟踪中:

    import numpy
    __unittest = True
    
    def assertNumpyArraysEqual(self, other):
        if self.shape != other.shape:
            raise AssertionError("Shapes don't match")
        if not numpy.allclose(self, other)
            raise AssertionError("Elements don't match!")
    

    【讨论】:

    • 谢谢,这是个好主意,如果两个数组都是由正在测试的函数生成的,这将是最好的选择。
    【解决方案5】:

    检查numpy.testing.assert_almost_equal 哪个"raises an AssertionError if two items are not equal up to desired precision",例如:

     import numpy.testing as npt
     npt.assert_almost_equal(np.array([1.0,2.3333333333333]),
                             np.array([1.0,2.33333334]), decimal=9)
    

    【讨论】:

      【解决方案6】:

      我遇到了同样的问题,并开发了一个基于为对象创建固定哈希值来比较相等性的函数。这还有一个额外的优势,您可以通过将对象的哈希值与代码中支持的固定值进行比较来测试对象是否符合预期。

      代码(一个独立的 python 文件,is here)。有两个函数:fixed_hash_eq,解决您的问题,compute_fixed_hash,从结构中生成哈希。 Tests are here

      这是一个测试:

      obj1 = [1, 'd', {'a': 4, 'b': np.arange(10)}, (7, [1, 2, 3, 4, 5])]
      obj2 = [1, 'd', {'a': 4, 'b': np.arange(10)}, (7, [1, 2, 3, 4, 5])]
      obj3 = [1, 'd', {'a': 4, 'b': np.arange(10)}, (7, [1, 2, 3, 4, 5])]
      obj3[2]['b'][4] = 0
      assert fixed_hash_eq(obj1, obj2)
      assert not fixed_hash_eq(obj1, obj3)
      

      【讨论】:

        【解决方案7】:

        基于@dbw(感谢),插入测试用例子类的以下方法对我来说效果很好:

         def assertNumpyArraysEqual(self,this,that,msg=''):
            '''
            modified from http://stackoverflow.com/a/15399475/5459638
            '''
            if this.shape != that.shape:
                raise AssertionError("Shapes don't match")
            if not np.allclose(this,that):
                raise AssertionError("Elements don't match!")
        

        我在我的测试用例方法中将它称为self.assertNumpyArraysEqual(this,that),并且工作起来就像一个魅力。

        【讨论】:

          猜你喜欢
          • 2011-02-15
          • 2012-05-21
          • 1970-01-01
          • 1970-01-01
          • 2020-09-26
          • 2021-06-20
          • 2012-06-04
          相关资源
          最近更新 更多