Development Tip

float 컬렉션에 대한 Python 단위 테스트의 assertAlmostEqual

yourdevel 2020. 12. 12. 12:32
반응형

float 컬렉션에 대한 Python 단위 테스트의 assertAlmostEqual


assertAlmostEqual (X, Y) 에있어서 파이썬 유닛 테스트 워크 시험 여부 xy정도가 플로트 가정하에 동일하다.

문제 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]).

  • 마찬가지로, 두 세트가 거의 동일한 목록으로 변환 될 수 있다면 (각 세트에 순서를 할당하여) 거의 동일한 것으로 간주됩니다.

  • 유사하게, 각 사전의 키 세트가 다른 사전의 키 세트와 거의 같고 거의 동일한 키 쌍마다 해당하는 거의 동일한 값이있는 경우 두 사전은 거의 동일한 것으로 간주됩니다.

  • 일반적으로 : 서로 거의 동일한 일부 부동 소수점을 제외하면 두 컬렉션이 같으면 거의 같다고 생각합니다. 즉, 나는 정말로 객체를 비교하고 싶지만 도중에 부동 소수점을 비교할 때 낮은 (사용자 정의) 정밀도로 비교하고 싶습니다.


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)

일반 is_almost_equal(first, second)함수를 구현 한 방법은 다음과 같습니다 .

먼저 비교해야하는 객체 ( firstsecond)를 복제하되 정확한 사본을 만들지는 마십시오. 객체 내부에서 만나는 부동 소수점의 중요하지 않은 소수 자릿수를 잘라냅니다.

이제 당신의 사본을 가지고 firstsecond하찮은 진수가 사라있는 위해를 바로 비교 firstsecond사용하여 ==연산자를.

cut_insignificant_digits_recursively(obj, places)중복 obj되지만 places원래의 각 float의 최상위 십진수 남는 함수 가 있다고 가정 해 봅시다 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에서 확인할 수 있습니다 . 개선 및 버그 수정을 환영합니다.


파이썬 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 )

numpy패키지를 사용해도 괜찮다 numpy.testingassert_array_almost_equal방법이 있습니다.

이것은 array_like객체에 대해 작동 하므로 float의 배열, 목록 및 튜플에는 적합하지만 세트 및 사전에는 작동하지 않습니다.

문서는 여기에 있습니다 .


그러한 방법은 없습니다. 직접해야합니다.

목록과 튜플의 경우 정의는 분명하지만 언급 한 다른 경우는 분명하지 않으므로 그러한 함수가 제공되지 않는 것은 당연합니다. 예를 들어, {1.00001: 1.00002}거의 같 {1.00002: 1.00001}습니까? 이러한 경우를 처리하려면 친밀도가 키나 값 또는 둘 다에 따라 달라지는 지 여부를 선택해야합니다. 집합의 경우 집합이 순서가 지정되지 않았으므로 의미있는 정의를 찾을 가능성이 낮으므로 "해당하는"요소에 대한 개념이 없습니다.


직접 구현해야 할 수도 있지만 목록과 세트는 동일한 방식으로 반복 될 수 있지만 사전은 다른 이야기이고 값이 아닌 키를 반복하며 세 번째 예는 저에게 약간 모호해 보입니다. 세트 내의 각 값 또는 각 세트의 각 값을 비교합니다.

여기에 간단한 코드 스 니펫이 있습니다.

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

다른 방법은 예를 들어 각 부동 소수점을 고정 정밀도의 문자열로 변환하여 데이터를 유사한 형식으로 변환하는 것입니다.

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

이 답변 중 어느 것도 나를 위해 작동하지 않습니다. 다음 코드는 파이썬 컬렉션, 클래스, 데이터 클래스 및 명명 된 튜플에 대해 작동합니다. 나는 뭔가를 잊었을 수도 있지만 지금까지 이것은 나를 위해 작동합니다.

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

나는 여전히 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]

파이썬 문서에 따르면 (참조 https://stackoverflow.com/a/41407651/1031191을 때문에, 13.94999999 같은 문제를 반올림 멀리 얻을 수 있습니다) 13.94999999 == 13.95입니다 True.

참고 URL : https://stackoverflow.com/questions/12136762/assertalmostequal-in-python-unit-test-for-collections-of-floats

반응형