断言两个字典几乎相等
我想要检查两个字典(就是一种数据结构,像个装东西的箱子)是否几乎相等,但我似乎找不到办法。
这里有个例子:
>>> import nose.tools as nt
>>> nt.assert_dict_equal({'a' : 12.4}, {'a' : 5.6 + 6.8})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python2.7/unittest/case.py", line 838, in assertDictEqual
self.fail(self._formatMessage(msg, standardMsg))
File "/usr/lib/python2.7/unittest/case.py", line 413, in fail
raise self.failureException(msg)
AssertionError: {'a': 12.4} != {'a': 12.399999999999999}
- {'a': 12.4}
+ {'a': 12.399999999999999}
我希望这个检查能通过,像这样:
>>> nt.assert_almost_equal(12.4, 5.6 + 6.8)
我在想,可能我遗漏了一些简单的东西,比如 nt.assert_almost_dict_equal
,或者也许有个参数可以传给 nt.assert_dict_equal
,用来指定浮点数(就是带小数的数字)应该有多接近,但我找不到相关的信息。
当然,我可以遍历字典,逐个比较值,用 nt.assert_almost_equal
来检查;不过在我的情况下,字典比较复杂,所以我希望能避免这样做。
那么,检查两个字典是否几乎相等的最佳方法是什么呢?
5 个回答
0
我需要一个递归版本的 round
,这样可以让输出看起来更好:
def recursive_round(data, ndigits=5):
if isinstance(data, dict):
return {k: recursive_round(v, ndigits) for k, v in data.items()}
if isinstance(data, list):
return [recursive_round(v, ndigits) for v in data]
if isinstance(data, tuple):
return tuple(recursive_round(v, ndigits) for v in data)
if isinstance(data, set):
return {recursive_round(v, ndigits) for v in data}
if isinstance(data, float):
if data.is_integer():
return int(data)
return round(data, ndigits)
return data
举个例子:
from pprint import pprint
import math
DATA = {
'test': [1.23456, 'whatever', (2.34567, math.pi)],
'another_key': [1.0, 2.0, 0.0, {math.e, math.inf}],
'last_key': [0.123456789, 9.87654321]
}
pprint(recursive_round(DATA, 3))
# {'another_key': [1, 2, 0, {2.718, inf}],
# 'last_key': [0.123, 9.877],
# 'test': [1.235, 'whatever', (2.346, 3.142)]}
这个也可以用来做单元测试,配合 assertEqual
使用:
import unittest
import math
def recursive_round(data, ndigits=5):
if isinstance(data, dict):
return {k: recursive_round(v, ndigits) for k, v in data.items()}
if isinstance(data, list):
return [recursive_round(v, ndigits) for v in data]
if isinstance(data, tuple):
return tuple(recursive_round(v, ndigits) for v in data)
if isinstance(data, set):
return {recursive_round(v, ndigits) for v in data}
if isinstance(data, float):
if data.is_integer():
return int(data)
return round(data, ndigits)
return data
DATA = {
'test': [1.23456, 'whatever', (2.34567, math.pi)],
'another_key': [1.0, 2.0, 0.0, {math.e, math.inf}],
'last_key': [0.123456789, 9.87654321]
}
class TestClass(unittest.TestCase):
def test_method(self):
expected = {'another_key': [1, 2, 0, {2.718, math.inf}],
'last_key': [0.123, 9.877],
'test': [1.235, 'whatever', (2.346, 3.142)]}
self.assertEqual(expected, recursive_round(DATA, 3))
if __name__ == '__main__':
unittest.main()
1
我没能让Akavall的函数运行,所以我自己写了一个。虽然有点简单,但对我来说足够用了。下面是用pytest测试这个函数是否正常工作的代码。
from numbers import Number
from math import isclose
def dictsAlmostEqual(dict1, dict2, rel_tol=1e-8):
"""
If dictionary value is a number, then check that the numbers are almost equal, otherwise check if values are exactly equal
Note: does not currently try converting strings to digits and comparing them. Does not care about ordering of keys in dictionaries
Just returns true or false
"""
if len(dict1) != len(dict2):
return False
# Loop through each item in the first dict and compare it to the second dict
for key, item in dict1.items():
# If it is a nested dictionary, need to call the function again
if isinstance(item, dict):
# If the nested dictionaries are not almost equal, return False
if not dictsAlmostEqual(dict1[key], dict2[key], rel_tol=rel_tol):
return False
# If it's not a dictionary, then continue comparing
# Put in else statement or else the nested dictionary will get compared twice and
# On the second time will check for exactly equal and will fail
else:
# If the value is a number, check if they are approximately equal
if isinstance(item, Number):
# if not abs(dict1[key] - dict2[key]) <= rel_tol:
# https://stackoverflow.com/questions/5595425/what-is-the-best-way-to-compare-floats-for-almost-equality-in-python
if not isclose(dict1[key], dict2[key], rel_tol=rel_tol):
return False
else:
if not (dict1[key] == dict2[key]):
return False
return True
使用pytest来验证函数的输出。
import pytest
import dictsAlmostEqual
def test_dictsAlmostEqual():
a = {}
b = {}
assert dictsAlmostEqual(a, b)
a = {"1": "a"}
b = {}
assert not dictsAlmostEqual(a, b)
a = {"1": "a"}
b = {"1": "a"}
assert dictsAlmostEqual(a, b)
a = {"1": "a"}
b = {"1": "b"}
assert not dictsAlmostEqual(a, b)
a = {"1": "1.23"}
b = {"1": "1.23"}
assert dictsAlmostEqual(a, b)
a = {"1": "1.234"}
b = {"1": "1.23"}
assert not dictsAlmostEqual(a, b)
a = {"1": 1.000000000000001, "2": "a"}
b = {"1": 1.000000000000002, "2": "a"}
assert not dictsAlmostEqual(a, b, rel_tol=1e-20)
assert dictsAlmostEqual(a, b, rel_tol=1e-8)
assert dictsAlmostEqual(a, b)
# Nested dicts
a = {"1": {"2": 1.000000000000001}}
b = {"1": {"2": 1.000000000000002}}
assert not dictsAlmostEqual(a, b, rel_tol=1e-20)
assert dictsAlmostEqual(a, b, rel_tol=1e-8)
assert dictsAlmostEqual(a, b)
a = {"1": {"2": 1.000000000000001, "3": "a"}, "2": "1.23"}
b = {"1": {"2": 1.000000000000002, "3": "a"}, "2": "1.23"}
assert not dictsAlmostEqual(a, b, rel_tol=1e-20)
assert dictsAlmostEqual(a, b, rel_tol=1e-8)
assert dictsAlmostEqual(a, b)
3
>>> import pandas as pd
>>> from pandas.testing import assert_series_equal
>>> a = pd.Series({'a' : 12.4})
>>> b = pd.Series({'a': 12.399999999999999})
>>> assert_series_equal(a, b)
我知道你不会单纯为了这个去导入pandas库,但如果你正好在使用pandas的话,你可以把字典转换成序列,然后使用
pandas.testing
里的assert_series_equal
函数。这个函数默认情况下是check_exact=False
。
4
Pytest的"approx"功能可以解决这个问题
在[10]: {'a': 2.000001} == pytest.approx({'a': 2})
输出结果是: True
22
@dano的评论回答了我的问题:
我从dano提供的链接复制了一个函数。
import unittest
import numpy
def assertDeepAlmostEqual(test_case, expected, actual, *args, **kwargs):
"""
Assert that two complex structures have almost equal contents.
Compares lists, dicts and tuples recursively. Checks numeric values
using test_case's :py:meth:`unittest.TestCase.assertAlmostEqual` and
checks all other values with :py:meth:`unittest.TestCase.assertEqual`.
Accepts additional positional and keyword arguments and pass those
intact to assertAlmostEqual() (that's how you specify comparison
precision).
:param test_case: TestCase object on which we can call all of the basic
'assert' methods.
:type test_case: :py:class:`unittest.TestCase` object
"""
is_root = not '__trace' in kwargs
trace = kwargs.pop('__trace', 'ROOT')
try:
if isinstance(expected, (int, float, long, complex)):
test_case.assertAlmostEqual(expected, actual, *args, **kwargs)
elif isinstance(expected, (list, tuple, numpy.ndarray)):
test_case.assertEqual(len(expected), len(actual))
for index in xrange(len(expected)):
v1, v2 = expected[index], actual[index]
assertDeepAlmostEqual(test_case, v1, v2,
__trace=repr(index), *args, **kwargs)
elif isinstance(expected, dict):
test_case.assertEqual(set(expected), set(actual))
for key in expected:
assertDeepAlmostEqual(test_case, expected[key], actual[key],
__trace=repr(key), *args, **kwargs)
else:
test_case.assertEqual(expected, actual)
except AssertionError as exc:
exc.__dict__.setdefault('traces', []).append(trace)
if is_root:
trace = ' -> '.join(reversed(exc.traces))
exc = AssertionError("%s\nTRACE: %s" % (exc.message, trace))
raise exc
# My part, using the function
class TestMyClass(unittest.TestCase):
def test_dicts(self):
assertDeepAlmostEqual(self, {'a' : 12.4}, {'a' : 5.6 + 6.8})
def test_dicts_2(self):
dict_1 = {'a' : {'b' : [12.4, 0.3]}}
dict_2 = {'a' : {'b' : [5.6 + 6.8, 0.1 + 0.2]}}
assertDeepAlmostEqual(self, dict_1, dict_2)
def main():
unittest.main()
if __name__ == "__main__":
main()
结果:
Ran 2 tests in 0.000s
OK