AssertAlmostEqual в модульном тесте Python для коллекций поплавков
Метод assertAlmostEqual (x, y) в инфраструктуре модульного тестирования Python проверяет, x
и y
приблизительно равны при условии, что они являются поплавками.
Проблема с assertAlmostEqual()
заключается в том, что она работает только с поплавками. Я ищу метод типа assertAlmostEqual()
, который работает с списками поплавков, множеств поплавков, словарей поплавков, кортежей поплавков, списков кортежей поплавков, наборов списков поплавков и т.д.
Например, пусть x = 0.1234567890
, y = 0.1234567891
. x
и y
почти равны, поскольку они согласуются с каждой цифрой, за исключением последней. Поэтому self.assertAlmostEqual(x, y)
является True
, потому что assertAlmostEqual()
работает для поплавков.
Я ищу более общий assertAlmostEquals()
, который также оценивает следующие вызовы True
:
-
self.assertAlmostEqual_generic([x, x, x], [y, y, y])
.
-
self.assertAlmostEqual_generic({1: x, 2: x, 3: x}, {1: y, 2: y, 3: y})
.
-
self.assertAlmostEqual_generic([(x,x)], [(y,y)])
.
Есть ли такой метод или я должен сам его реализовать?
Разъяснения:
-
assertAlmostEquals()
имеет необязательный параметр с именем places
, а числа сравниваются путем вычисления разности, округленной до числа десятичных places
. По умолчанию places=7
, поэтому self.assertAlmostEqual(0.5, 0.4)
является False, а self.assertAlmostEqual(0.12345678, 0.12345679)
- True. Мое умозрительное assertAlmostEqual_generic()
должно иметь одинаковую функциональность.
-
Два списка считаются почти равными, если они имеют почти равные числа в точно таком же порядке. формально, for i in range(n): self.assertAlmostEqual(list1[i], list2[i])
.
-
Аналогично, два набора считаются почти равными, если их можно преобразовать в почти равные списки (путем назначения порядка каждому набору).
-
Аналогично, два словаря считаются почти равными, если набор ключей каждого словаря почти равен набору ключей другого словаря, и для каждой такой почти равной пары ключей существует соответствующее почти равное значение.
-
В целом: я считаю, что две коллекции почти равны, если они равны, за исключением некоторых соответствующих поплавков, которые почти равны друг другу. Другими словами, я хотел бы действительно сравнивать объекты, но с низкой (настраиваемой) точностью при сравнении поплавков на этом пути.
Ответы
Ответ 1
Вот как я реализовал универсальную is_almost_equal(first, second)
:
Во-первых, продублируйте объекты, которые нужно сравнить (first
и second
), но не делайте точной копии: вырежьте незначительные десятичные цифры любого поплавка, с которым вы столкнетесь внутри объекта.
Теперь, когда у вас есть копии first
и second
для которых пропущены незначительные десятичные цифры, просто сравните first
и second
с помощью оператора ==
.
Пусть Предположим, у нас есть cut_insignificant_digits_recursively(obj, places)
функция, которая дублирует obj
, но оставляет только places
наиболее значащие десятичные цифры каждого поплавка в оригинальном obj
. Вот рабочая реализация is_almost_equals(first, second, places)
:
from insignificant_digit_cutter import cut_insignificant_digits_recursively
def is_almost_equal(first, second, places):
'''returns True if first and second equal.
returns true if first and second aren't equal but have exactly the same
structure and values except for a bunch of floats which are just almost
equal (floats are almost equal if they're equal when we consider only the
[places] most significant digits of each).'''
if first == second: return True
cut_first = cut_insignificant_digits_recursively(first, places)
cut_second = cut_insignificant_digits_recursively(second, places)
return cut_first == cut_second
А вот рабочая реализация cut_insignificant_digits_recursively(obj, places)
:
def cut_insignificant_digits(number, places):
'''cut the least significant decimal digits of a number,
leave only [places] decimal digits'''
if type(number) != float: return number
number_as_str = str(number)
end_of_number = number_as_str.find('.')+places+1
if end_of_number > len(number_as_str): return number
return float(number_as_str[:end_of_number])
def cut_insignificant_digits_lazy(iterable, places):
for obj in iterable:
yield cut_insignificant_digits_recursively(obj, places)
def cut_insignificant_digits_recursively(obj, places):
'''return a copy of obj except that every float loses its least significant
decimal digits remaining only [places] decimal digits'''
t = type(obj)
if t == float: return cut_insignificant_digits(obj, places)
if t in (list, tuple, set):
return t(cut_insignificant_digits_lazy(obj, places))
if t == dict:
return {cut_insignificant_digits_recursively(key, places):
cut_insignificant_digits_recursively(val, places)
for key,val in obj.items()}
return obj
Код и его модульные тесты доступны здесь: https://github.com/snakile/approximate_comparator. Я приветствую любые улучшения и исправления ошибок.
Ответ 2
если вы не против использования NumPy (который поставляется вместе с вашим Python (x, y)), вы можете захотеть посмотреть модуль np.testing
, который определяет, среди прочего, функцию assert_almost_equal
.
Подпись np.testing.assert_almost_equal(actual, desired, decimal=7, err_msg='', verbose=True)
>>> x = 1.000001
>>> y = 1.000002
>>> np.testing.assert_almost_equal(x, y)
AssertionError:
Arrays are not almost equal to 7 decimals
ACTUAL: 1.000001
DESIRED: 1.000002
>>> np.testing.assert_almost_equal(x, y, 5)
>>> np.testing.assert_almost_equal([x, x, x], [y, y, y], 5)
>>> np.testing.assert_almost_equal((x, x, x), (y, y, y), 5)
Ответ 3
Начиная с Python 3.5 вы можете сравнить, используя
math.isclose(a, b, rel_tol=1e-9, abs_tol=0.0)
Как описано в pep-0485. Реализация должна быть эквивалентна
abs(a-b) <= max( rel_tol * max(abs(a), abs(b)), abs_tol )
Ответ 4
Если вы не возражаете против использования пакета numpy
, тогда в numpy.testing
есть метод assert_array_almost_equal
.
Это работает для объектов типа array_like
, поэтому оно подходит для массивов, списков и кортежей с плавающей точкой, но не работает для наборов и словарей.
Документация здесь.
Ответ 5
Нет такого метода, вам придется сделать это самостоятельно.
Для списков и кортежей определение очевидно, но обратите внимание, что другие случаи, о которых вы говорите, не очевидны, поэтому неудивительно, что такая функция не предоставляется. Например, {1.00001: 1.00002}
почти равно {1.00002: 1.00001}
? Для обработки таких случаев требуется выбор того, зависит ли близость от ключей или значений или того и другого. Для наборов вы вряд ли найдете значимое определение, так как множества неупорядочены, поэтому нет понятия "соответствующих" элементов.
Ответ 6
Возможно, вам придется реализовать его самостоятельно, в то время как его истина, что список и наборы могут быть повторены одинаково, словари - это другая история, вы перебираете свои ключи не значениями, а третий пример для меня немного двусмыслен, do вы хотите сравнить каждое значение внутри набора или каждое значение из каждого набора.
содержит простой фрагмент кода.
def almost_equal(value_1, value_2, accuracy = 10**-8):
return abs(value_1 - value_2) < accuracy
x = [1,2,3,4]
y = [1,2,4,5]
assert all(almost_equal(*values) for values in zip(x, y))
Ответ 7
Альтернативный подход заключается в преобразовании ваших данных в сопоставимую форму, например, путем преобразования каждого числа с плавающей точкой в строку с фиксированной точностью.
def comparable(data):
"""Converts 'data' to a comparable structure by converting any floats to a string with fixed precision."""
if isinstance(data, (int, str)):
return data
if isinstance(data, float):
return '{:.4f}'.format(data)
if isinstance(data, list):
return [comparable(el) for el in data]
if isinstance(data, tuple):
return tuple([comparable(el) for el in data])
if isinstance(data, dict):
return {k: comparable(v) for k, v in data.items()}
Тогда ты можешь:
self.assertEquals(comparable(value1), comparable(value2))
Ответ 8
Ни один из этих ответов не работает для меня. Следующий код должен работать для коллекций Python, классов, классов данных и именованных кортежей. Я мог что-то забыть, но пока это работает для меня.
import unittest
from collections import namedtuple, OrderedDict
from dataclasses import dataclass
from typing import Any
def are_almost_equal(o1: Any, o2: Any, max_abs_ratio_diff: float, max_abs_diff: float) -> bool:
"""
Compares two objects by recursively walking them trough. Equality is as usual except for floats.
Floats are compared according to the two measures defined below.
:param o1: The first object.
:param o2: The second object.
:param max_abs_ratio_diff: The maximum allowed absolute value of the difference.
'abs(1 - (o1 / o2)' and vice-versa if o2 == 0.0. Ignored if < 0.
:param max_abs_diff: The maximum allowed absolute difference 'abs(o1 - o2)'. Ignored if < 0.
:return: Whether the two objects are almost equal.
"""
if type(o1) != type(o2):
return False
composite_type_passed = False
if hasattr(o1, '__slots__'):
if len(o1.__slots__) != len(o2.__slots__):
return False
if any(not are_almost_equal(getattr(o1, s1), getattr(o2, s2),
max_abs_ratio_diff, max_abs_diff)
for s1, s2 in zip(sorted(o1.__slots__), sorted(o2.__slots__))):
return False
else:
composite_type_passed = True
if hasattr(o1, '__dict__'):
if len(o1.__dict__) != len(o2.__dict__):
return False
if any(not are_almost_equal(k1, k2, max_abs_ratio_diff, max_abs_diff)
or not are_almost_equal(v1, v2, max_abs_ratio_diff, max_abs_diff)
for ((k1, v1), (k2, v2))
in zip(sorted(o1.__dict__.items()), sorted(o2.__dict__.items()))
if not k1.startswith('__')): # avoid infinite loops
return False
else:
composite_type_passed = True
if isinstance(o1, dict):
if len(o1) != len(o2):
return False
if any(not are_almost_equal(k1, k2, max_abs_ratio_diff, max_abs_diff)
or not are_almost_equal(v1, v2, max_abs_ratio_diff, max_abs_diff)
for ((k1, v1), (k2, v2)) in zip(sorted(o1.items()), sorted(o2.items()))):
return False
elif any(issubclass(o1.__class__, c) for c in (list, tuple, set)):
if len(o1) != len(o2):
return False
if any(not are_almost_equal(v1, v2, max_abs_ratio_diff, max_abs_diff)
for v1, v2 in zip(o1, o2)):
return False
elif isinstance(o1, float):
if o1 == o2:
return True
else:
if max_abs_ratio_diff > 0: # if max_abs_ratio_diff < 0, max_abs_ratio_diff is ignored
if o2 != 0:
if abs(1.0 - (o1 / o2)) > max_abs_ratio_diff:
return False
else: # if both == 0, we already returned True
if abs(1.0 - (o2 / o1)) > max_abs_ratio_diff:
return False
if 0 < max_abs_diff < abs(o1 - o2): # if max_abs_diff < 0, max_abs_diff is ignored
return False
return True
else:
if not composite_type_passed:
return o1 == o2
return True
class EqualityTest(unittest.TestCase):
def test_floats(self) -> None:
o1 = ('hi', 3, 3.4)
o2 = ('hi', 3, 3.400001)
self.assertTrue(are_almost_equal(o1, o2, 0.0001, 0.0001))
self.assertFalse(are_almost_equal(o1, o2, 0.00000001, 0.00000001))
def test_ratio_only(self):
o1 = ['hey', 10000, 123.12]
o2 = ['hey', 10000, 123.80]
self.assertTrue(are_almost_equal(o1, o2, 0.01, -1))
self.assertFalse(are_almost_equal(o1, o2, 0.001, -1))
def test_diff_only(self):
o1 = ['hey', 10000, 1234567890.12]
o2 = ['hey', 10000, 1234567890.80]
self.assertTrue(are_almost_equal(o1, o2, -1, 1))
self.assertFalse(are_almost_equal(o1, o2, -1, 0.1))
def test_both_ignored(self):
o1 = ['hey', 10000, 1234567890.12]
o2 = ['hey', 10000, 0.80]
o3 = ['hi', 10000, 0.80]
self.assertTrue(are_almost_equal(o1, o2, -1, -1))
self.assertFalse(are_almost_equal(o1, o3, -1, -1))
def test_different_lengths(self):
o1 = ['hey', 1234567890.12, 10000]
o2 = ['hey', 1234567890.80]
self.assertFalse(are_almost_equal(o1, o2, 1, 1))
def test_classes(self):
class A:
d = 12.3
def __init__(self, a, b, c):
self.a = a
self.b = b
self.c = c
o1 = A(2.34, 'str', {1: 'hey', 345.23: [123, 'hi', 890.12]})
o2 = A(2.34, 'str', {1: 'hey', 345.231: [123, 'hi', 890.121]})
self.assertTrue(are_almost_equal(o1, o2, 0.1, 0.1))
self.assertFalse(are_almost_equal(o1, o2, 0.0001, 0.0001))
o2.hello = 'hello'
self.assertFalse(are_almost_equal(o1, o2, -1, -1))
def test_namedtuples(self):
B = namedtuple('B', ['x', 'y'])
o1 = B(3.3, 4.4)
o2 = B(3.4, 4.5)
self.assertTrue(are_almost_equal(o1, o2, 0.2, 0.2))
self.assertFalse(are_almost_equal(o1, o2, 0.001, 0.001))
def test_classes_with_slots(self):
class C(object):
__slots__ = ['a', 'b']
def __init__(self, a, b):
self.a = a
self.b = b
o1 = C(3.3, 4.4)
o2 = C(3.4, 4.5)
self.assertTrue(are_almost_equal(o1, o2, 0.3, 0.3))
self.assertFalse(are_almost_equal(o1, o2, -1, 0.01))
def test_dataclasses(self):
@dataclass
class D:
s: str
i: int
f: float
@dataclass
class E:
f2: float
f4: str
d: D
o1 = E(12.3, 'hi', D('hello', 34, 20.01))
o2 = E(12.1, 'hi', D('hello', 34, 20.0))
self.assertTrue(are_almost_equal(o1, o2, -1, 0.4))
self.assertFalse(are_almost_equal(o1, o2, -1, 0.001))
o3 = E(12.1, 'hi', D('ciao', 34, 20.0))
self.assertFalse(are_almost_equal(o2, o3, -1, -1))
def test_ordereddict(self):
o1 = OrderedDict({1: 'hey', 345.23: [123, 'hi', 890.12]})
o2 = OrderedDict({1: 'hey', 345.23: [123, 'hi', 890.0]})
self.assertTrue(are_almost_equal(o1, o2, 0.01, -1))
self.assertFalse(are_almost_equal(o1, o2, 0.0001, -1))
Ответ 9
Я бы все еще использовал self.assertEqual()
поскольку он остается наиболее информативным, когда дерьмо попадает в фанат. Вы можете сделать это путем округления, например.
self.assertEqual(round_tuple((13.949999999999999, 1.121212), 2), (13.95, 1.12))
где round_tuple
def round_tuple(t: tuple, ndigits: int) -> tuple:
return tuple(round(e, ndigits=ndigits) for e in t)
def round_list(l: list, ndigits: int) -> list:
return [round(e, ndigits=ndigits) for e in l]
Согласно питону документации (см fooobar.com/info/11446/...) вы можете уйти с округлением вопросов, как 13.94999999, потому что 13.94999999 == 13.95
является True
.