【问题标题】:Why does a successful assertEqual not always imply a successful assertItemsEqual?为什么成功的 assertEqual 并不总是意味着成功的 assertItemsEqual?
【发布时间】:2015-04-17 13:10:44
【问题描述】:

Python 2.7 docs 声明assertItemsEqual“等同于assertEqual(sorted(expected), sorted(actual))”。在下面的示例中,除 test4 之外的所有测试都通过了。为什么assertItemsEqual 在这种情况下会失败?

根据最小惊讶原则,给定两个迭代,我希望成功的assertEqual 意味着成功的assertItemsEqual

import unittest

class foo(object):
    def __init__(self, a):
        self.a = a

    def __eq__(self, other):
        return self.a == other.a

class test(unittest.TestCase):
    def setUp(self):
        self.list1 = [foo(1), foo(2)]
        self.list2 = [foo(1), foo(2)]

    def test1(self):
        self.assertTrue(self.list1 == self.list2)

    def test2(self):
        self.assertEqual(self.list1, self.list2)

    def test3(self):
        self.assertEqual(sorted(self.list1), sorted(self.list2))

    def test4(self):
        self.assertItemsEqual(self.list1, self.list2)

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

这是我机器上的输出:

FAIL: test4 (__main__.test)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "assert_test.py", line 25, in test4
    self.assertItemsEqual(self.list1, self.list2)
AssertionError: Element counts were not equal:
First has 1, Second has 0:  <__main__.foo object at 0x7f67b3ce2590>
First has 1, Second has 0:  <__main__.foo object at 0x7f67b3ce25d0>
First has 0, Second has 1:  <__main__.foo object at 0x7f67b3ce2610>
First has 0, Second has 1:  <__main__.foo object at 0x7f67b3ce2650>

----------------------------------------------------------------------
Ran 4 tests in 0.001s

FAILED (failures=1)

【问题讨论】:

  • 因为您还没有为foo 对象定义排序?
  • 谢谢。就您而言,如果我在 foo 上定义 hash 方法,test4 通过。但是,文档声明 assertItemsEqual 适用于不可散列的对象。我误解了文档吗?
  • 我不太了解 Python,但错误消息清楚地告诉您,它通过计算两者中的唯一对象来比较列表,然后逐个键比较计数。比较是按对象地址进行的。由于每个列表中有不同的对象实例,因此列表比较不相等。如果你说a = foo(1); b = foo(2); self.list1 = [a, b] self.list2 = [b, a],我敢打赌最后一次测试会通过。
  • 它适用于基于对象地址的不可散列对象(地址总是允许散列)!
  • 谢谢,吉恩。是的,测试将通过您修改后的示例。我的问题的重点是文档似乎不清楚(至少对我而言)——它们似乎暗示 assertEqual(sorted(actual), sorted(expected)) 等同于 assertItemsEqual(actual, expected),即使对于不可散列对象。

标签: python python-unittest


【解决方案1】:

有趣的是,文档规范与实现分离,它从不进行任何排序。 Here is the source code。如您所见,它首先尝试使用collections.Counter 进行散列计数。如果此操作因类型错误而失败(因为任一列表包含不可散列的项目),它将转到a second algorithm,并使用python == 和O(n^2) 循环进行比较。

因此,如果您的 foo 类不可散列,则第二种算法将发出匹配信号。但它是完全可散列的。来自文档:

默认情况下,作为用户定义类实例的对象是可散列的;它们都比较不相等(除了它们自己),它们的哈希值来自它们的 id()。

我通过调用collections.Counter([foo(1)]) 验证了这一点。没有类型错误异常。

所以这就是您的代码出轨的地方。来自__hash__ 上的文档:

如果它定义了 cmp() 或 eq() 但未定义 hash(),则其实例将无法在散列集合中使用。

不幸的是,“不可用”显然不等于“不可散列”。

接着说:

从父类继承 hash() 方法但改变 cmp() 或 eq() 含义的类,使得返回的哈希值不再合适(例如,通过切换到基于值的相等概念而不是默认的基于身份的相等)可以通过在类中设置 hash = None 来明确地将自己标记为不可哈希定义。

如果我们重新定义:

class foo(object):
    __hash__ = None
    def __init__(self, a):
        self.a = a
    def __eq__(self, other):
        return isinstance(other, foo) and self.a == other.a

所有测试都通过了!

所以看起来这些文件并没有完全错误,但它们也不是很清楚。他们应该提到,计数是通过散列完成的,只有在失败时才尝试简单的相等匹配。仅当对象具有完整的散列语义或完全不可散列时,这才是有效的方法。你的处于中间位置。 (我相信 Python 3 更严格地禁止或至少警告这种类型的类。)

【讨论】:

  • 谢谢你,吉恩。您的回复写得很清楚并解释了根本原因——我特别感谢您将您的答案建立在 Python 源代码中。
  • 不客气。这是一个写得很好的问题。下一个有趣的事情是我尝试将__hash__ = None 设置为用户类设置__eq__ 而不是__hash__。果然,第二个算法运行,因为这使得foo 不可散列。但显然它有一个错误!我得到一个例外,字段a 不存在。有一个伪造的对象被比较。我需要睡一会儿。玩得开心。
  • @MatthewNizol 我知道发生了什么。第二种算法通过在已经比较的对象上写入对 NULL 对象的引用来工作。您的 __eq__ 在这些引用上失败,因为 NULL 没有 a 字段。我在上面的代码中修复了它。晚安!
【解决方案2】:

文档的相关部分在这里:

https://docs.python.org/2/reference/expressions.html?highlight=ordering#not-in

大多数其他内置类型的对象比较不相等,除非它们是同一个对象;一个对象被认为比另一个对象更小还是更大的选择是任意的,但在程序的一次执行中始终如一。

因此,如果您创建x, y = foo(1), foo(1),那么排序最终是x &gt; y 还是x &lt; y 并没有明确的定义。在 python3 中,您根本不会被允许,sorted 调用应该引发异常。

由于 unittest 会为每个测试方法调用 setUp,因此每次都会创建不同的 foo 实例。


assertItemsEqual 是用collections.Counter(dict 的子类)实现的,所以我认为test4 的失败可能是这个事实的症状:

>>> x, y = foo(1), foo(1)
>>> x == y
True
>>> {x: None} == {y: None}
False

如果两个项目比较相等,那么它们的哈希值应该相同,否则你可能会破坏这样的映射。

【讨论】:

  • 谢谢,维姆。我明白您的观点,即 sorted() 调用可能会产生未定义的行为。但是,我在将 cmp 方法添加到 foo 后测试了该行为(如果 self.a other.a),而 test4 仍然失败。如果我添加 hash 方法(返回 self.a),Test4 确实会成功;所以看起来 assertItemEqual 是根据散列值进行分组的。但是,文档似乎没有说明该要求。
  • 我认为这是因为assertItemsEqual 是用collections.Counter 实现的。有趣的。让我进一步调查..
  • 非常感谢您的研究。您得出了与 Gene 相同的结论——根本原因是在实现中使用了 collections.Counter。但是,因为我只能接受 1 个答案,所以我接受了 Gene 的意见,因为他还观察到所有用户定义的对象默认情况下都可以通过它们的 id() 进行哈希处理,这说明了为什么 collections.Counter 将 foo 实例分组为这样。再次感谢您!
  • 这种可疑行为似乎已在 python3 中修复。相关:stackoverflow.com/q/1608842/674039
  • 具体来说,请参见 martin 的评论stackoverflow.com/questions/1608842/…
猜你喜欢
  • 2011-11-12
  • 2018-02-23
  • 1970-01-01
  • 1970-01-01
  • 2012-07-06
  • 2021-01-27
  • 1970-01-01
  • 1970-01-01
  • 2017-12-18
相关资源
最近更新 更多