【问题标题】:Comparing Python dicts with floating point values included将 Python dicts 与包含的浮点值进行比较
【发布时间】:2012-12-08 05:13:16
【问题描述】:

我想比较一对字典并使用“模糊”浮点比较或更好的是使用numpy.allclose() 来这样做。但是,在 Python 中为字典使用默认的 ==!= 不会这样做。

我想知道是否有办法更改浮点比较操作(可能使用上下文管理器进行安全清理)。

我相信这里有一个例子会有所帮助。我有一个包含各种值的深度嵌套的字典。其中一些值是浮点值。我知道“比较”浮点值等存在大量陷阱。

d1 = {'a': {'b': 1.123456}}
d2 = {'a': {'b': 1.1234578}}

我想使用!= 来比较这两个字典,如果唯一的区别是某个范围内的浮点数,则让它返回True。例如,如果 close(不确定我想要的精度),则不要计算不同的值。

我想我可以自己递归地遍历字典并手动将numpy.allclose() 用于浮点值并回退到所有其他类型的正常相等测试等。但是,这有点棘手且容易出错。我确实认为这将是一个可以接受的解决方案,我很想看到一个这样的解决方案。希望有更优雅的东西。

我脑海中的优雅解决方案如下所示。但是,我不知道这样的事情是否可行:

with hacked_float_compare:
    result = d1 != d2

因此,在这个上下文管理器中,我将用我自己的比较或numpy.allclose() 替换浮点比较(仅适用于标准float() 值。

同样,我不确定这是否可行,因为猴子补丁float() 无法真正完成,因为它是用C 编写的。我还想避免将字典中的每个浮点值更改为我自己的具有__eq__() 的浮点类。也许这是最好的方式?

【问题讨论】:

  • 一种选择是为浮点数创建一个包装器,并在那里覆盖__eq__
  • 但是你需要用fuzzyfloat(0.5)等来创建你所有的花车
  • 对。我知道这种方法有效,只是不想使用特殊的对象/类,如果可以避免的话。在这种情况下,我真的只需要比较“模糊”。这就是为什么我希望使用上下文管理器并在有限的时间内进入不同的“模式”。

标签: python


【解决方案1】:

避免子类化内置类型。当你发现你的对象由于某种未知原因改变了类型时,你会后悔的。改用委托。例如:

import operator as op


class FuzzyDict(object):
    def __init__(self, iterable=(), float_eq=op.eq):
        self._float_eq = float_eq
        self._dict = dict(iterable)

    def __getitem__(self, key):
        return self._dict[key]

    def __setitem__(self, key, val):
        self._dict[key] = val

    def __iter__(self):
        return iter(self._dict)

    def __len__(self):
        return len(self._dict)

    def __contains__(self, key):
        return key in self._dict

    def __eq__(self, other):
        def compare(a, b):
            if isinstance(a, float) and isinstance(b, float):
                return self._float_eq(a, b)
            else:
                return a == b
        try:
            if len(self) != len(other):
                return False
            for key in self:
                if not compare(self[key], other[key]):
                    return False
            return True
        except Exception:
            return False

    def __getattr__(self, attr):
        # free features borrowed from dict
        attr_val = getattr(self._dict, attr)
        if callable(attr_val):
            def wrapper(*args, **kwargs):
                result = attr_val(*args, **kwargs)
                if isinstance(result, dict):
                    return FuzzyDict(result, self._float_eq)
                return result
            return wrapper
        return attr_val

还有一个用法示例:

>>> def float_eq(a, b):
...     return abs(a - b) < 0.01
... 
>>> A = FuzzyDict(float_eq=float_eq)
>>> B = FuzzyDict(float_eq=float_eq)
>>> A['a'] = 2.345
>>> A['b'] = 'a string'
>>> B['a'] = 2.345
>>> B['b'] = 'a string'
>>> B['a'] = 2.3445
>>> A == B
True
>>> B['a'] = 234.55
>>> A == B
False
>>> B['a'] = 2.345
>>> B['b'] = 'a strin'
>>> A == B
False

即使嵌套也可以工作:

>>> A['nested'] = FuzzyDict(float_eq=float_eq)
>>> A['nested']['a'] = 17.32
>>> B['nested'] = FuzzyDict(float_eq=float_eq)
>>> B['nested']['a'] = 17.321
>>> B['b'] = 'a string'   # changed before
>>> A == B
True
>>> B['nested']['a'] = 17.34
>>> A == B
False

完全替代dict 需要更多代码,并且可能需要进行一些测试以了解它的稳健性,但即使是上述解决方案也提供了许多dict 功能(例如copysetdefaultgetupdate 等)


关于为什么你不应该继承一个内置的。

此解决方案看似简单正确,但通常并非如此。 首先,即使您可以对内置类型进行子类化,但这并不意味着它们被编写为用作子类,因此您可能会发现要使某些东西正常工作,您必须编写比您想象的更多的代码。

此外,您可能希望使用内置方法,但这些方法将返回内置类型的实例,而不是您的类的实例,这意味着您必须重新实现类型。此外,您有时必须实现内置函数中未实现的其他方法。

例如,子类化list 你可能会认为,因为list 只实现了__iadd____add__,你可以安全地重新实现这两个方法,但你错了!您还必须实现__radd__,否则表达式如下:

[1,2,3] + MyList([1,2,3])

将返回正常的 list 而不是 MyList

总之,子类化一个内建函数的后果比你一开始想的要多得多,而且由于类型或行为的改变,你可能没有预料到,它可能会引入一些不可预知的错误。调试也变得更加困难,因为您不能简单地在日志中打印对象的实例,表示是正确的!您确实必须检查周围所有对象的类才能发现这些细微的错误。

在您的特定情况下,如果您打算仅在单个方法中转换字典,那么您可能会避免子类化 dict 的大多数缺点,但此时您为什么不简单地编写一个函数并比较 @ 987654339@s 有吗? 这应该可以正常工作,除非您想将 dicts 传递给进行比较的库函数。

【讨论】:

  • 这看起来不错。但是,就我而言,我认为只是子类化dict 可能没问题。我只想在本地转换字典来进行比较。因此,这个新类只会在内部用于单个方法。这是否有意义并且看起来合理?
  • 但是,如果 other 字典的键不在第一个字典中,则此解决方案不会返回 False。所以这个解决方案不仅仅通过浮点比较改变了比较字典的语义,对吧?
  • @durden2.0 我确实从第一次发布它时稍微改变了它,我认为它还可以。在我第一次检查sorted(self) == sorted(other) 之前,它会了解这种差异,但我认为上面的内容也可以。因为如果键的数量不同,则通过比较长度来捕获,然后我检查self 中的每个键,如果它不在other 中,则会引发KeyError,这会被except Exception 正确返回 False,所以应该没问题。无论如何,如果保证只在一个方法中进行更改,那么子类化dict 是可以的。
  • @durden2.0 我检查了 python 的dict.__eq__ 实现,他们做了类似的事情:检查字典的长度和迭代表并检查其他字典中的条目,所以我会说上面的实现是正确的。
  • 好吧,这是有道理的。感谢您的检查。在相关说明中,为什么我必须实现__ne____eq__?我以为 CPython 只会调用 __eq__ 并反转它,但似乎并非如此。我什至查看了 CPython 源代码,但我遗漏了一些东西。使用 != 时,不会调用覆盖的 __eq__(至少在 Python 2.7.3 中)。你能指出这在 CPython 源代码中是如何工作的吗?
【解决方案2】:

仅供参考,我认为在我的情况下,子类化并不是最好的方法。我已经制定了一个我很可能会使用here 的解决方案。

这不是公认的答案,因为它是基于我从该线程中学到的协作方法。只是想要一个其他人可以从中受益的“解决方案”。

【讨论】:

    【解决方案3】:

    要覆盖比较运算符,您需要定义一个使用不同运算符的派生类。所以你不能按照你建议的方式去做。您可以做的是派生一个“模糊浮点”类(如@Null)建议,或从dict 派生和类,并指定它对浮点数使用模糊比较:

    class fuzzydict(dict):
        def __eq__(self, other):
            """Manually compare each element of `self` with `other`.
               Float values are compared up to reasonable precision."""
    

    您必须自己处理字典比较的逻辑,它可能不会像内置比较那样快,但您可以在代码中编写dict1 == dict2。对于所有可能包含浮点数的(嵌套)字典,请确保使用 fuzzydict 而不是 dict

    然而,我应该补充一点,您冒着不确定性的风险:您的字典将比较相等,但包含的数字略有不同,因此后续计算可能会为您提供比较相等的结果,具体取决于哪个你使用的字典。在我看来,更安全(和更理智)的方法是在将浮点数插入字典时对其进行舍入,以便它们比较严格相等。

    【讨论】:

    • 是的,这也可以。但是,我认为创建自己的 dict 类并将比较代码放在 dict 的 __eq__ 中并没有多大好处。这个完全相同的代码可能只是一个需要两个字典的方法。然后我不必在任何地方都使用这种新类型的字典或转换现有的字典等。同样,如果我在很多地方都这样做,这个解决方案会很好。但是,这种比较只在一个领域很重要。
    • 另外,使用这些浮点数进行计算很棘手,因为浮点表示等。此外,在我的场景中,我真的不介意这些数字在彼此的范围内.这不会导致任何奇怪的操作。这只是我想将其应用到的代码非常有限的区域。
    • Python 的 dict 比较是递归的。如果你派生一个类,python 将处理递归,你只需要实现平面逻辑:检查缺失或额外的键,并比较值。
    • 我很困惑。 dict __eq__ 的参数是 self(运算符左侧的 dict)和另一个 dict。如果字典是嵌套的怎么办?也许一个代码示例可以帮助我理解你的意思。我假设我仍然必须自己处理递归,因为我不能简单地循环遍历 other 并检查值。 other 的值可以是其他字典。一旦一切都归结为非序列类型,我只想比较这些值。
    • 其实我反应太快了。我想我明白你的意思。我将需要比较每个 dict 并且可以在值中查找 float 实例并进行比较,并在其他情况下默认为常规比较。代码 sn-p 仍然有助于确保我走在正确的轨道上。
    猜你喜欢
    • 2017-08-01
    • 2013-11-19
    • 1970-01-01
    • 1970-01-01
    • 2021-05-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多