为什么成功的assertEqual并不总是意味着成功的assertItemsEqual?

Mat*_*zol 5 python python-unittest

Python 2.7版文档指出,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()
Run Code Online (Sandbox Code Playgroud)

这是我机器上的输出:

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)
Run Code Online (Sandbox Code Playgroud)

Gen*_*ene 3

有趣的是,文档规范与实现分离,它从不进行任何排序。这是源代码。正如您所看到的,它首先尝试使用 进行散列计数collections.Counter。如果此操作因类型错误而失败(因为任一列表包含不可散列的项目),则会继续使用第二个算法,在其中使用 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
Run Code Online (Sandbox Code Playgroud)

所有测试均通过!

因此,这些文件似乎并不完全错误,但也不够清晰。他们应该提到计数是通过散列完成的,只有在失败时才尝试简单的相等匹配。仅当对象具有完整的散列语义或完全不可散列时,这才是有效的方法。你的处于中间立场。(我相信 Python 3 对于禁止或至少警告此类类型更加严格。)