简体   繁体   中英

Test for actual equality in Python

I am writing a Python2 module that emulates a certain library. The results may be float , int , long , unicode , str , tuple , list , and custom objects. Lists may not contain lists, but they may contain tuples. Tuples may not contain lists or tuples. Otherwise, lists and tuples may contain any of the other types listed above.

(Actually, the module should not return long or str , but if it does, they should be caught and reported as different when compared to int and unicode , respectively.)

I am writing a testing program that checks the results against known answers by the library my module tries to emulate. The obvious answer would be to test the values and the types, but one problem I'm facing is that in corner cases, possible results to test for are -0.0 (which should be distinguished from 0.0 ) and NaN (Not a Number - a value a float can take).

However:

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

The is operator doesn't help a bit:

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

repr helps:

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

But only to a point, since it doesn't help with objects:

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

This works though:

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

But it fails with tuples and lists:

>>> 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__'

It seems I'm close, so I need to know:

  1. Is there a simpler way to check for actual equality that doesn't have the burden of converting to string?
  2. If not, how would I go about comparing lists or tuples containing objects?

Edit: To be clear, what I'm after is a full comparison function. My test function looks roughly like this:

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

My question concerns what should reallyequal() look like.

Edit 2: I've found the Python standard module unittest but unfortunately none of the checks covers this use case, so it seems that if I intend to use it, I should use something like self.assertTrue(reallyequal(actual, expected)) .

I'm actually surprised that it's so hard to make unit tests including expected NaNs and minus zeros nested within the results. I'm still using the repr solution which is a half-solution, but I'm open to other ideas.

Here is one implementation:

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

A few of your edge cases from "classic" equality:

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

Classes should implement their own __eq__ "magic method" to determine whether or not two instances are equal - they will fall through to # 4 and be compared there:

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

From the answers and comments it seems clear that the answer to my first question (is there a simpler way than using repr() ?) is no, there is no simpler way. So I've researched more on how to accomplish this as simply as possible and I've come up with this solution which answers my second question.

repr() works for the most part, but fails on objects of custom classes. Since the default repr() of a custom object is not useful as-is anyway for any meaningful purpose, what I've done is to override the __repr__ method of each base class like this:

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

Now I can use repr() on any of the values and get an expression that actually represents these values uniquely, that my test program can catch.

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

(which I will actually embed in the test function due to its simplicity).

Here it is in action:

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

Edit: Edited to incorporate commenter's suggestions re repr results with __dict__ .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM