assertAlmostEqual у модулі-тесті Python для колекцій плаваючих елементів


81

(Х, Y) assertAlmostEqual метод модульного тестування каркасного Пітона тести , дійсно чи 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]).

  • Подібним чином два набори вважаються майже рівними, якщо їх можна перетворити на майже рівні списки (шляхом призначення порядку для кожного набору).

  • Так само два словники вважаються майже рівними, якщо набір ключів кожного словника майже дорівнює набору ключів іншого словника, і для кожної такої майже рівної пари ключів існує відповідне майже рівне значення.

  • Загалом: я вважаю дві колекції майже рівними, якщо вони рівні, за винятком деяких відповідних плаваючих знаків, які майже майже рівні між собою. Іншими словами, я хотів би по-справжньому порівнювати об'єкти, але з низькою (налаштованою) точністю, коли порівнювати поплавці на цьому шляху.


Який сенс використовувати floatключі у словнику? Оскільки ви не можете бути впевнені, що отримаєте точно такий самий флоат, ви ніколи не знайдете своїх предметів за допомогою пошуку. І якщо ви не використовуєте пошук, чому б просто не використовувати список кортежів замість словника? Той самий аргумент стосується і множин.
максимум

Просто посилання на джерело для assertAlmostEqual.
djvg

Відповіді:


71

якщо ви не проти використовувати 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)

4
Це близько, але numpy.testingмайже однакові методи працюють лише над числами, масивами, кортежами та списками. Вони не працюють над словниками, наборами та колекціями колекцій.
snakile

Дійсно, але це початок. Крім того, у вас є доступ до вихідного коду, який ви можете змінити, щоб дозволити порівняння словників, збірників тощо. np.testing.assert_equalнаприклад, розпізнає словники як аргументи (навіть якщо порівняння виконується a, ==яке не буде працювати для вас).
Pierre GM

Звичайно, ви все одно зіткнетеся з проблемами при порівнянні наборів, як згадував @BrenBarn.
Pierre GM

Зверніть увагу , що поточна документація assert_array_almost_equalрекомендує використовувати assert_allclose, assert_array_almost_equal_nulpабо assert_array_max_ulpзамість цього.
phunehehe

10

Станом на 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 )

7
Як це допомагає порівняти контейнери з поплавками, про які задавалось питання?
максимум

9

Ось як я реалізував загальну 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 . Я вітаю будь-які покращення та виправлення помилок.


Замість того, щоб порівнювати плаваючі, ви порівнюєте рядки? Добре ... Але тоді, чи не простіше було б встановити загальний формат? Подобається fmt="{{0:{0}f}}".format(decimals), і використовувати цей fmtформат, щоб "натягнути" ваші поплавки?
Pierre GM

1
Це виглядає приємно, але невеликий пункт: placesдає кількість знаків після коми, а не кількість значущих цифр. Наприклад, порівняння 1024.123та 1023.999до 3 значущих повинно повернути рівне, а до 3 знаків після коми це не так.
Родні Річардсон

1
@pir, ліцензія дійсно не визначена. Дивіться відповідь Snalile у цьому випуску, в якому він каже, що не має часу вибрати / додати ліцензію, але надає дозволи на використання / модифікацію. Дякуємо, що поділилися цим, до речі.
Jérôme

1
@RodneyRichardson, так, це десяткові знаки , як у assertAlmostEqual : "Зверніть увагу, що ці методи округлюють значення до заданої кількості знаків після коми (тобто як функція round ()), а не значущих цифр."
Jérôme

2
@ Джером, дякую за коментар. Я щойно додав ліцензію MIT.
snakile

5

Якщо ви не проти використовувати numpyпакет, тоді numpy.testingє assert_array_almost_equalметод.

Це працює для array_likeоб'єктів, тому добре для масивів, списків і кортежів з плаваючою точкою, але не працює для наборів та словників.

Документація тут .


4

Такого методу не існує, вам доведеться це зробити самостійно.

Для списків та кортежів визначення є очевидним, але зауважте, що інші випадки, про які ви згадали, не очевидні, тому не дивно, що така функція не передбачена. Наприклад, {1.00001: 1.00002}майже дорівнює {1.00002: 1.00001}? Для розгляду таких випадків потрібно зробити вибір щодо того, чи залежить близькість від ключів чи значень, або від того, і від іншого. Для наборів ви навряд чи знайдете значуще визначення, оскільки набори є невпорядкованими, тому поняття "відповідні" елементи немає.


BrenBarn: Я додав роз’яснення до питання. Відповідь на ваше запитання полягає в тому, що {1.00001: 1.00002}майже дорівнює {1.00002: 1.00001}тоді і тільки тоді, коли 1.00001 майже дорівнює 1.00002. За замовчуванням вони майже не рівні (оскільки точність за замовчуванням дорівнює 7 знакам після коми), але для досить малого значення placesвони майже рівні.
snakile

1
@BrenBarn: IMO, використання ключів типу floatв dict слід відмовляти (а може навіть і забороняти) зі зрозумілих причин. Приблизна рівність дикту повинна базуватися лише на цінностях; тестовий фреймворк не повинен турбуватися про неправильне використання floatключів. Для наборів їх можна відсортувати перед порівнянням та відсортувати списки.
максимум

2

Можливо, вам доведеться реалізувати це самостійно, хоча справді, що список і набори можна повторювати однаково, словники - це інша історія, ви повторюєте їх ключі, а не значення, і третій приклад здається мені трохи неоднозначним, ви маєте на увазі порівняти кожне значення в межах набору або кожне значення з кожного набору.

ось простий фрагмент коду.

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

Дякуємо, рішення є правильним для списків та кортежів, але не для інших типів колекцій (або вкладених колекцій). Див. Роз’яснення, які я додав до запитання. Сподіваюся, мій намір зараз зрозумілий. Два набори майже рівні, якби їх вважали рівними у світі, де цифри вимірюються не дуже точно.
snakile

1

Жодна з цих відповідей не працює для мене. Наступний код повинен працювати для колекцій python, класів, класів даних та namedtuples. Можливо, я щось забув, але поки це для мене працює.

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

0

Я б все-таки використовував, 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]

Згідно з документами python (див. Https://stackoverflow.com/a/41407651/1031191 ), ви можете уникнути проблем округлення, таких як 13.94999999, тому що 13.94999999 == 13.95це так True.


-1

Альтернативним підходом є перетворення даних у порівнянну форму, наприклад, перетворюючи кожен поплавок у рядок із фіксованою точністю.

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))
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.