assertAlmostEqual im Python-Unit-Test für Floatsammlungen

81

Die assertAlmostEqual (x, y) -Methode in Pythons Unit-Testing-Framework testet, ob xund yungefähr gleich, vorausgesetzt, es handelt sich um Floats.

Das Problem dabei assertAlmostEqual()ist, dass es nur auf Floats funktioniert. Ich suche nach einer Methode, assertAlmostEqual()die auf Float-Listen, Float-Sätzen, Float-Wörterbüchern, Float-Tupeln, Float-Tupel-Listen, Float-Listen, Float-Listen usw. funktioniert.

Zum Beispiel lassen Sie x = 0.1234567890, y = 0.1234567891. xund ysind fast gleich, weil sie mit Ausnahme der letzten auf jeder einzelnen Ziffer übereinstimmen. Deshalb self.assertAlmostEqual(x, y)liegt es Truedaran, assertAlmostEqual()für Schwimmer zu arbeiten.

Ich suche nach einem allgemeineren, assertAlmostEquals()der auch die folgenden Aufrufe an bewertet 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)]).

Gibt es eine solche Methode oder muss ich sie selbst implementieren?

Erläuterungen:

  • assertAlmostEquals()hat einen optionalen Parameter mit dem Namen placesund die Zahlen werden verglichen, indem die auf die Dezimalzahl gerundete Differenz berechnet wird places. Standardmäßig places=7ist daher self.assertAlmostEqual(0.5, 0.4)False und self.assertAlmostEqual(0.12345678, 0.12345679)True. Mein Spekulativ assertAlmostEqual_generic()sollte die gleiche Funktionalität haben.

  • Zwei Listen gelten als nahezu gleich, wenn sie in genau derselben Reihenfolge fast gleiche Nummern haben. formal , for i in range(n): self.assertAlmostEqual(list1[i], list2[i]).

  • In ähnlicher Weise werden zwei Sätze als nahezu gleich angesehen, wenn sie in nahezu gleiche Listen konvertiert werden können (indem jedem Satz eine Reihenfolge zugewiesen wird).

  • In ähnlicher Weise werden zwei Wörterbücher als nahezu gleich angesehen, wenn der Schlüsselsatz jedes Wörterbuchs fast gleich dem Schlüsselsatz des anderen Wörterbuchs ist, und für jedes dieser nahezu gleichen Schlüsselpaare gibt es einen entsprechenden nahezu gleichen Wert.

  • Im Allgemeinen: Ich halte zwei Sammlungen für fast gleich, wenn sie gleich sind, mit Ausnahme einiger entsprechender Floats, die fast gleich sind. Mit anderen Worten, ich möchte Objekte wirklich vergleichen, aber mit einer geringen (benutzerdefinierten) Genauigkeit, wenn ich Floats auf dem Weg vergleiche.

Snakile
quelle
Was bringt es, floatSchlüssel im Wörterbuch zu verwenden? Da Sie nicht sicher sein können, genau den gleichen Float zu erhalten, werden Sie Ihre Artikel niemals mithilfe der Suche finden. Und wenn Sie keine Suche verwenden, warum nicht einfach eine Liste von Tupeln anstelle eines Wörterbuchs verwenden? Das gleiche Argument gilt für Mengen.
Max
Nur ein Link zur Quelle für assertAlmostEqual.
DJVG

Antworten:

71

Wenn es Ihnen nichts ausmacht, NumPy (das mit Ihrem Python (x, y) geliefert wird) zu verwenden, sollten Sie sich das np.testingModul ansehen , das unter anderem eine assert_almost_equalFunktion definiert.

Die Unterschrift ist 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)
Pierre GM
quelle
4
Das ist nah, aber numpy.testingfast gleiche Methoden funktionieren nur für Zahlen, Arrays, Tupel und Listen. Sie arbeiten nicht mit Wörterbüchern, Sets und Sammlungen von Sammlungen.
Snakile
In der Tat, aber das ist ein Anfang. Außerdem haben Sie Zugriff auf den Quellcode, den Sie ändern können, um Wörterbücher, Sammlungen usw. vergleichen zu können. np.testing.assert_equalerkennt beispielsweise Wörterbücher als Argumente an (auch wenn der Vergleich von a durchgeführt wird, ==was für Sie nicht funktioniert).
Pierre GM
Natürlich werden Sie beim Vergleichen von Sets immer noch auf Probleme stoßen, wie @BrenBarn erwähnt hat.
Pierre GM
Beachten Sie, dass in der aktuellen Dokumentation von assert_array_almost_equalempfohlen wird assert_allclose, assert_array_almost_equal_nulpoder assert_array_max_ulpstattdessen zu verwenden.
Phunehehe
10

Ab Python 3.5 können Sie mit vergleichen

math.isclose(a, b, rel_tol=1e-9, abs_tol=0.0)

Wie in pep-0485 beschrieben . Die Implementierung sollte äquivalent zu sein

abs(a-b) <= max( rel_tol * max(abs(a), abs(b)), abs_tol )
Maximiliano Ramirez Mellado
quelle
7
Wie hilft dies, Container mit Floats zu vergleichen, nach denen die Frage gestellt wurde?
Max
9

So habe ich eine generische is_almost_equal(first, second)Funktion implementiert :

Duplizieren Sie zunächst die Objekte, die Sie vergleichen möchten ( firstund second), aber erstellen Sie keine exakte Kopie: Schneiden Sie die unbedeutenden Dezimalstellen aller Floats aus, auf die Sie im Objekt stoßen.

Nachdem Sie Kopien von firstund haben, secondfür die die unbedeutenden Dezimalstellen weg sind, vergleichen Sie einfach den Operator firstund secondverwenden Sie ihn ==.

Nehmen wir an, wir haben eine cut_insignificant_digits_recursively(obj, places)Funktion, die dupliziert, objaber nur die placeshöchstwertigen Dezimalstellen jedes Floats im Original belässt obj. Hier ist eine funktionierende Implementierung von 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

Und hier ist eine funktionierende Implementierung von 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

Der Code und seine Komponententests sind hier verfügbar: https://github.com/snakile/approximate_comparator . Ich freue mich über jede Verbesserung und Fehlerbehebung.

Snakile
quelle
Anstatt Floats zu vergleichen, vergleichen Sie Strings? OK ... Aber wäre es dann nicht einfacher, ein gemeinsames Format festzulegen? Wie fmt="{{0:{0}f}}".format(decimals), und verwenden Sie dieses fmtFormat, um Ihre Floats zu "stringifizieren"?
Pierre GM
1
Das sieht gut aus, aber ein kleiner Punkt: placesGibt die Anzahl der Dezimalstellen an, nicht die Anzahl der signifikanten Zahlen. Zum Beispiel sollte ein Vergleich von 1024.123und 1023.999mit 3 signifikanten Werten gleich sein, aber mit 3 Dezimalstellen sind sie nicht gleich.
Rodney Richardson
1
@pir, die Lizenz ist in der Tat undefiniert. Siehe die Antwort von snalile in dieser Ausgabe, in der er sagt, dass er keine Zeit hat, eine Lizenz auszuwählen / hinzuzufügen, aber Verwendungs- / Änderungsberechtigungen erteilt. Danke, dass du das geteilt hast, übrigens.
Jérôme
1
@RodneyRichardson, ja, dies sind Dezimalstellen, wie in assertAlmostEqual : "Beachten Sie, dass diese Methoden die Werte auf die angegebene Anzahl von Dezimalstellen (dh wie die Funktion round ()) und nicht auf signifikante Stellen runden."
Jérôme
2
@ Jérôme, danke für den Kommentar. Ich habe gerade eine MIT-Lizenz hinzugefügt.
Snakile
5

Wenn es Ihnen nichts ausmacht, das numpyPaket zu verwenden, dann numpy.testinghaben Sie die assert_array_almost_equalMethode.

Dies funktioniert für array_likeObjekte, ist also für Arrays, Listen und Tupel von Floats in Ordnung, funktioniert jedoch nicht für Mengen und Wörterbücher.

Die Dokumentation finden Sie hier .

DJCowley
quelle
4

Es gibt keine solche Methode, Sie müssten es selbst tun.

Bei Listen und Tupeln ist die Definition offensichtlich. Beachten Sie jedoch, dass die anderen von Ihnen genannten Fälle nicht offensichtlich sind. Kein Wunder, dass eine solche Funktion nicht bereitgestellt wird. Ist zum Beispiel {1.00001: 1.00002}fast gleich {1.00002: 1.00001}? Um solche Fälle zu behandeln, muss entschieden werden, ob die Nähe von Schlüsseln oder Werten oder von beiden abhängt. Für Mengen ist es unwahrscheinlich, dass Sie eine aussagekräftige Definition finden, da die Mengen ungeordnet sind und daher keine Vorstellung von "entsprechenden" Elementen besteht.

BrenBarn
quelle
BrenBarn: Ich habe der Frage Klarstellungen hinzugefügt. Die Antwort auf Ihre Frage lautet, dass {1.00001: 1.00002}fast {1.00002: 1.00001}genau dann fast gleich ist, wenn 1,00001 fast gleich 1,00002 ist. Standardmäßig sind sie nicht fast gleich (da die Standardgenauigkeit 7 Dezimalstellen beträgt), aber für einen ausreichend kleinen Wert sind placessie fast gleich.
Snakile
1
@BrenBarn: IMO, die Verwendung von Schlüsseln vom Typ floatin Diktat sollte aus offensichtlichen Gründen nicht empfohlen (und möglicherweise sogar verboten) werden. Die ungefähre Gleichheit des Diktats sollte nur auf Werten basieren. Das Testframework muss sich nicht um die falsche Verwendung von floatfor-Schlüsseln kümmern . Für Sets können sie vor dem Vergleich sortiert und sortierte Listen verglichen werden.
Max
2

Möglicherweise müssen Sie es selbst implementieren, obwohl es stimmt, dass Liste und Mengen auf dieselbe Weise iteriert werden können, Wörterbücher eine andere Geschichte sind, Sie ihre Schlüssel und keine Werte iterieren und das dritte Beispiel mir etwas mehrdeutig erscheint, meinen Sie das? Vergleichen Sie jeden Wert innerhalb des Satzes oder jeden Wert aus jedem Satz.

Hier ist ein einfaches Code-Snippet.

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))
Samy Vilar
quelle
Dank ist die Lösung für Listen und Tupel korrekt, jedoch nicht für andere Arten von Sammlungen (oder verschachtelte Sammlungen). Siehe die Erläuterungen, die ich der Frage hinzugefügt habe. Ich hoffe, meine Absicht ist jetzt klar. Zwei Sätze sind fast gleich, wenn sie in einer Welt, in der Zahlen nicht sehr genau gemessen werden, als gleich angesehen worden wären.
Snakile
0

Keine dieser Antworten funktioniert für mich. Der folgende Code sollte für Python-Sammlungen, Klassen, Datenklassen und Namedtuples funktionieren. Ich hätte vielleicht etwas vergessen, aber bisher funktioniert das für mich.

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))
redsk
quelle
0

Ich würde immer noch dafür verwenden, self.assertEqual()dass es am informativsten bleibt, wenn Scheiße den Fan trifft. Sie können dies durch Runden tun, z.

self.assertEqual(round_tuple((13.949999999999999, 1.121212), 2), (13.95, 1.12))

wo round_tupleist

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]

Nach Ansicht der Python - Dokumentation (siehe https://stackoverflow.com/a/41407651/1031191 ) Sie mit Rundungs Themen wie 13,94999999 weg erhalten können, da 13.94999999 == 13.95ist True.

Barney Szabolcs
quelle
-1

Ein alternativer Ansatz besteht darin, Ihre Daten in eine vergleichbare Form zu konvertieren, indem Sie beispielsweise jeden Float mit fester Genauigkeit in einen String verwandeln.

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

Dann kannst du:

self.assertEquals(comparable(value1), comparable(value2))
Karl Rosaen
quelle