在Python中测试实际相等性

2 投票
2 回答
1704 浏览
提问于 2025-04-18 10:06

我正在写一个Python2模块,目的是模拟某个库的功能。这个模块的结果可能是 float(浮点数)、int(整数)、long(长整数)、unicode(Unicode字符串)、str(普通字符串)、tuple(元组)、list(列表)以及自定义对象。列表里不能包含列表,但可以包含元组。元组里不能包含列表或元组。除此之外,列表和元组可以包含上面提到的其他类型。

(实际上,这个模块不应该返回 longstr,但如果返回了,它们在与 intunicode 比较时应该被视为不同的类型。)

我正在写一个测试程序,用来检查模块的结果是否与我想要模拟的库的已知答案一致。显而易见的方法是测试值和类型,但我遇到的一个问题是,在一些特殊情况下,可能的测试结果包括 -0.0(需要和 0.0 区分开)和 NaN(不是一个数字 - 浮点数可以取的一个值)。

但是:

>>> a = float('nan')
>>> b = float('nan')
>>> a == b
False
>>> c = float('-0.0')
>>> c
-0.0
>>> d = 1.0 - 1.0
>>> c == d
True

is 操作符一点用都没有:

>>> a is b
False
>>> d is 0.0
False

repr 有点帮助:

>>> repr(a) == repr(b)
True
>>> repr(c) == repr(d)
False
>>> repr(d) == repr(0.0)
True

但仅限于某种程度,因为它对对象没有帮助:

>>> class e:
...   pass
... 
>>> f = e()
>>> g = e()
>>> f.x = float('nan')
>>> g.x = float('nan')
>>> f == g
False
>>> repr(f) == repr(g)
False

不过这个方法有效:

>>> repr(f.__dict__) == repr(g.__dict__)
True

但在处理元组和列表时就失败了:

>>> h = [float('nan'), f]
>>> i = [float('nan'), g]
>>> h == i
False
>>> repr(h) == repr(i)
False
>>> repr(h.__dict__) == repr(i.__dict__)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'list' object has no attribute '__dict__'

看起来我已经接近了,所以我需要知道:

  1. 有没有更简单的方法来检查实际的相等性,而不需要转换成字符串?
  2. 如果没有,我该如何比较包含对象的列表或元组?

编辑:为了更清楚,我想要的是一个完整的比较函数。我的测试函数大致如下:

>>> def test(expression, expected):
...   actual = eval(expression)
...   if not reallyequal(actual, expected):
...     report_error(expression, actual, expected)

我的问题是,reallyequal() 应该是什么样子的。

编辑 2:我找到了Python的标准模块unittest,但不幸的是,没有一个检查能覆盖这个用例,所以如果我打算使用它,我应该用类似 self.assertTrue(reallyequal(actual, expected)) 的方式。

我其实很惊讶,写单元测试时包括预期的NaN和负零嵌套在结果中竟然这么困难。我现在还在用 repr 的解决方案,这只是个半解决方案,但我愿意听听其他的想法。

2 个回答

0

从大家的回答和评论来看,我的第一个问题(有没有比使用 repr() 更简单的方法?)的答案是没有,确实没有更简单的方法。所以我进一步研究了如何尽可能简单地实现这个功能,最后找到了一个解决方案,回答了我的第二个问题。

repr() 在大多数情况下都能正常工作,但对于自定义类的对象就不太行了。因为自定义对象的默认 repr() 输出其实没什么用,无法满足实际需求。所以我做的就是重写每个基类的 __repr__ 方法,像这样:

class MyClass:
    def __repr__(self):
        return self.__class__.__name__ + "(" \
            + repr(sorted(self.__dict__.items(), key=lambda t: t[0])) + ")"

现在我可以在任何值上使用 repr(),得到一个真正能唯一表示这些值的表达式,这样我的测试程序就能捕捉到这些值。

def reallyequal(actual, expected):
    return repr(actual) == repr(expected)

(实际上我会把它嵌入到测试函数中,因为这样更简单)。

下面是它的实际效果:

>>> reallyequal(-0.0, 0.0)
False
>>> reallyequal(float('nan'),float('nan'))
True
>>> f = MyClass()
>>> f.x = float('nan')
>>> g = MyClass()
>>> g.x = float('nan')
>>> reallyequal(f, g)
True
>>> h = [f,3]
>>> i = [g,4]
>>> reallyequal(h, i)
False
>>> i[1] = 3
>>> reallyequal(h, i)
True
>>> g.x = 1
>>> reallyequal(h, i)
False
>>> f.x = 1L
>>> reallyequal(h, i)
False
>>> f.x = 1
>>> reallyequal(h, i)
True

编辑: 根据评论者的建议,编辑了内容,加入了关于 __dict__repr 结果。

1

这里有一个实现的例子:

def really_equal(actual, expected, tolerance=0.0001):
    """Compare actual and expected for 'actual' equality."""

    # 1. Both same type?
    if not isinstance(actual, type(expected)):
        return False

    # 2. Deal with floats (edge cases, tolerance)
    if isinstance(actual, float):
        if actual == 0.0:
            return str(actual) == str(expected)
        elif math.isnan(actual):
            return math.isnan(expected)
        return abs(actual - expected) < tolerance

    # 3. Deal with tuples and lists (item-by-item, recursively)
    if isinstance(actual, (tuple, list)):
        return all(really_equal(i1, i2) for i1, i2 in zip(actual, expected))

    # 4. Fall back to 'classic' equality
    return actual == expected

还有一些你在“经典”相等性中遇到的特殊情况:

>>> float('nan') == float('nan')
False
>>> really_equal(float('nan'), float('nan'))
True

>>> 0.0 == -0.0
True
>>> really_equal(0.0, -0.0)
False

>>> "foo" == u"foo"
True
>>> really_equal("foo", u"foo")
False

>>> 1L == 1
True
>>> really_equal(1L, 1)
False

类应该实现自己的 __eq__ “魔法方法”,用来判断两个实例是否相等 - 如果没有实现,就会走到 # 4,在那里进行比较:

>>> class Test(object):

    def __init__(self, val):
        self.val = val

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


>>> a = Test(1)
>>> b = Test(1)
>>> really_equal(a, b)
True

撰写回答