khẳng địnhAlmostEqual trong kiểm tra đơn vị Python cho bộ sưu tập các phao


81

Phương thức khẳng địnhAlmostEqual (x, y) trong khung kiểm tra đơn vị của Python kiểm tra xem có xygần bằng nhau hay không giả sử chúng là phao.

Vấn đề assertAlmostEqual()là nó chỉ hoạt động trên phao. Tôi đang tìm một phương pháp như thế assertAlmostEqual()hoạt động trên danh sách phao, bộ phao, từ điển phao, bộ phao, danh sách bộ phao, bộ danh sách phao, v.v.

Ví dụ, chúng ta hãy x = 0.1234567890, y = 0.1234567891. xygần như bằng nhau vì chúng đồng ý với nhau về từng chữ số ngoại trừ chữ số cuối cùng. Do đó self.assertAlmostEqual(x, y)TrueassertAlmostEqual()hoạt động đối với phao.

Tôi đang tìm kiếm một giá trị chung hơn assertAlmostEquals()cũng đánh giá các lệnh gọi sau tới 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)]).

Có phương pháp nào như vậy không hay tôi phải tự thực hiện?

Giải thích:

  • assertAlmostEquals()có một tham số tùy chọn được đặt tên placesvà các số được so sánh bằng cách tính toán sự khác biệt được làm tròn đến số thập phân places. Theo mặc định places=7, do đó self.assertAlmostEqual(0.5, 0.4)là Sai trong khi self.assertAlmostEqual(0.12345678, 0.12345679)là Đúng. Đầu cơ của tôi assertAlmostEqual_generic()nên có cùng chức năng.

  • Hai danh sách được coi là gần như bằng nhau nếu chúng có số lượng gần như bằng nhau theo đúng thứ tự. về mặt hình thức for i in range(n): self.assertAlmostEqual(list1[i], list2[i]),.

  • Tương tự, hai tập hợp được coi là gần như bằng nhau nếu chúng có thể được chuyển đổi thành danh sách gần như bằng nhau (bằng cách gán một thứ tự cho mỗi tập hợp).

  • Tương tự, hai từ điển được coi là gần như bằng nhau nếu bộ khóa của mỗi từ điển gần như bằng với bộ khóa của từ điển kia và đối với mỗi cặp khóa gần như bằng nhau như vậy có một giá trị tương ứng gần như bằng nhau.

  • Nói chung: Tôi coi hai tập hợp gần như bằng nhau nếu chúng bằng nhau ngoại trừ một số phao tương ứng gần như bằng nhau. Nói cách khác, tôi muốn thực sự so sánh các đối tượng nhưng với độ chính xác thấp (tùy chỉnh) khi so sánh các phao trên đường đi.


Mục đích của việc sử dụng floatcác phím trong từ điển là gì? Vì bạn không thể chắc chắn có được chính xác cùng một phao, nên bạn sẽ không bao giờ tìm thấy các mục của mình bằng cách sử dụng tra cứu. Và nếu bạn không sử dụng tra cứu, tại sao không chỉ sử dụng danh sách các bộ giá trị thay vì từ điển? Đối số tương tự cũng áp dụng cho các tập hợp.
tối đa

Chỉ là một liên kết đến nguồn cho assertAlmostEqual.
djvg

Câu trả lời:


71

nếu bạn không phiền khi sử dụng NumPy (đi kèm với Python (x, y) của bạn), bạn có thể muốn xem np.testingmô-đun định nghĩa, trong số những người khác, một assert_almost_equalhàm.

Chữ ký là 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
Đó là gần, nhưng numpy.testingcác phương thức gần như bằng nhau chỉ hoạt động trên số, mảng, bộ giá trị và danh sách. Chúng không hoạt động trên từ điển, bộ và bộ sưu tập của bộ sưu tập.
snakile

Thật vậy, nhưng đó là một sự khởi đầu. Bên cạnh đó, bạn có quyền truy cập vào mã nguồn mà bạn có thể sửa đổi để cho phép so sánh các từ điển, bộ sưu tập, v.v. np.testing.assert_equalchẳng hạn như nhận dạng từ điển là đối số (ngay cả khi việc so sánh được thực hiện bởi một đối ==số sẽ không phù hợp với bạn).
Pierre GM

Tất nhiên, bạn vẫn sẽ gặp rắc rối khi so sánh các bộ, như @BrenBarn đã đề cập.
Pierre GM

Lưu ý rằng các tài liệu hiện tại của assert_array_almost_equalkhuyến cáo sử dụng assert_allclose, assert_array_almost_equal_nulphoặc assert_array_max_ulpthay thế.
phunehehe 10/11/19

10

Kể từ python 3.5, bạn có thể so sánh bằng cách sử dụng

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

Như được mô tả trong pep-0485 . Việc triển khai phải tương đương với

abs(a-b) <= max( rel_tol * max(abs(a), abs(b)), abs_tol )

7
Điều này giúp so sánh các thùng chứa với phao nổi như thế nào, câu hỏi được đặt ra là gì?
tối đa

9

Đây là cách tôi đã triển khai một is_almost_equal(first, second)hàm chung :

Đầu tiên, sao chép các đối tượng bạn cần so sánh ( firstsecond), nhưng không tạo một bản sao chính xác: cắt các chữ số thập phân không quan trọng của bất kỳ số float nào bạn gặp bên trong đối tượng.

Bây giờ bạn đã có các bản sao của firstsecondcác chữ số thập phân không quan trọng đã biến mất, chỉ cần so sánh firstsecondsử dụng ==toán tử.

Giả sử chúng ta có một cut_insignificant_digits_recursively(obj, places)hàm sao chép objnhưng chỉ để lại các placeschữ số thập phân quan trọng nhất của mỗi float trong bản gốc obj. Đây là một triển khai hoạt động của 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

Và đây là một triển khai hoạt động của 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

Mã và các bài kiểm tra đơn vị của nó có sẵn ở đây: https://github.com/snakile/approximate_comparator . Tôi hoan nghênh mọi cải tiến và sửa lỗi.


Thay vì so sánh phao, bạn đang so sánh các chuỗi? OK ... Nhưng sau đó, sẽ dễ dàng hơn để đặt một định dạng chung? Thích fmt="{{0:{0}f}}".format(decimals)và sử dụng fmtđịnh dạng này để "xâu chuỗi" các phao của bạn?
Pierre GM

1
Điều này trông đẹp, nhưng có một điểm nhỏ: placescung cấp số lượng vị trí thập phân, không phải là số lượng các số liệu quan trọng. Ví dụ: so sánh 1024.1231023.999với 3 quan trọng phải trả về bằng nhau, nhưng với 3 chữ số thập phân thì không.
Rodney Richardson

1
@pir, giấy phép thực sự là không xác định. Xem câu trả lời của snalile trong số báo này, trong đó anh ấy nói rằng anh ấy không có thời gian để chọn / thêm giấy phép, nhưng cấp quyền sử dụng / sửa đổi. Cảm ơn vì đã chia sẻ điều này, BTW.
Jérôme

1
@RodneyRichardson, vâng, đây là chữ số thập phân, giống như trong khẳng địnhAlmostEqual : "Lưu ý rằng các phương thức này làm tròn các giá trị đến số chữ số thập phân nhất định (tức là như hàm round ()) và không phải chữ số có nghĩa."
Jérôme

2
@ Jérôme, cảm ơn vì nhận xét. Tôi vừa thêm giấy phép MIT.
snakile

5

Nếu bạn không phiền khi sử dụng numpygói thì numpy.testingassert_array_almost_equalphương pháp.

Điều này hoạt động với array_likecác đối tượng, vì vậy nó tốt cho các mảng, danh sách và các bộ giá trị của float, nhưng nó không hoạt động với các bộ và từ điển.

Tài liệu ở đây .


4

Không có phương pháp nào như vậy, bạn phải tự làm.

Đối với danh sách và bộ giá trị, định nghĩa là hiển nhiên, nhưng lưu ý rằng các trường hợp khác mà bạn đề cập không rõ ràng, vì vậy không có gì ngạc nhiên khi một hàm như vậy không được cung cấp. Ví dụ, {1.00001: 1.00002}gần bằng {1.00002: 1.00001}? Việc xử lý các trường hợp như vậy đòi hỏi phải đưa ra lựa chọn về việc độ gần phụ thuộc vào khóa hoặc giá trị hoặc cả hai. Đối với các tập hợp, bạn khó có thể tìm thấy một định nghĩa có ý nghĩa, vì các tập hợp không có thứ tự, do đó không có khái niệm về các phần tử "tương ứng".


BrenBarn: Tôi đã làm rõ thêm cho câu hỏi. Câu trả lời cho câu hỏi của bạn là {1.00001: 1.00002}gần như bằng {1.00002: 1.00001}nếu và chỉ khi 1,00001 gần bằng 1,00002. Theo mặc định, chúng gần như không bằng nhau (vì độ chính xác mặc định là 7 chữ số thập phân) nhưng đối với một giá trị đủ nhỏ placesthì chúng gần như bằng nhau.
snakile

1
@BrenBarn: IMO, việc sử dụng các phím loại floattrong dict nên không được khuyến khích (và thậm chí có thể không được phép) vì những lý do rõ ràng. Sự bình đẳng gần đúng của dict chỉ nên dựa trên các giá trị; khung thử nghiệm không cần phải lo lắng về việc sử dụng không chính xác các floatkhóa. Đối với tập hợp, chúng có thể được sắp xếp trước khi so sánh và danh sách đã sắp xếp có thể được so sánh.
tối đa

2

Bạn có thể phải tự triển khai nó, trong khi sự thật là danh sách và các bộ có thể được lặp lại theo cùng một cách, từ điển là một câu chuyện khác, bạn lặp lại các khóa của chúng chứ không phải giá trị và ví dụ thứ ba có vẻ hơi mơ hồ đối với tôi, bạn có nghĩa là so sánh từng giá trị trong tập hợp hoặc từng giá trị từ từng tập hợp.

là một đoạn mã đơn giản.

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

Cảm ơn, giải pháp đúng cho danh sách và bộ giá trị nhưng không đúng cho các loại bộ sưu tập khác (hoặc bộ sưu tập lồng nhau). Xem phần làm rõ mà tôi đã thêm vào câu hỏi. Tôi hy vọng ý định của tôi là rõ ràng bây giờ. Hai tập hợp gần như bằng nhau nếu chúng được coi là ngang nhau trong một thế giới mà các con số không được đo lường chính xác.
snakile

0

Không có câu trả lời nào trong số này phù hợp với tôi. Đoạn mã sau sẽ hoạt động cho các bộ sưu tập, lớp, kính dữ liệu và các nhóm có tên của python. Tôi có thể đã quên một cái gì đó, nhưng cho đến nay điều này hiệu quả với tôi.

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

Tôi vẫn sẽ sử dụng self.assertEqual()nó để giữ thông tin nhiều nhất khi shit chạm vào người hâm mộ. Bạn có thể làm điều đó bằng cách làm tròn, ví dụ.

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

nơi 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]

Theo tài liệu python (xem https://stackoverflow.com/a/41407651/1031191 ), bạn có thể thoát khỏi các vấn đề làm tròn như 13,94999999, vì 13.94999999 == 13.95là vậy True.


-1

Một cách tiếp cận thay thế là chuyển đổi dữ liệu của bạn thành một dạng có thể so sánh được bằng cách biến mỗi float thành một chuỗi với độ chính xác cố định.

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

Sau đó bạn có thể:

self.assertEquals(comparable(value1), comparable(value2))
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.