You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何Pytest会尝试用字典减浮点数?嵌套approx类异常排查

问题:嵌套pytest approx字典比较的键顺序异常

我实现了一个嵌套的pytest.approx扩展类,用于比较包含浮点数的复杂字典结构,支持近似比较。但遇到一个问题:当字典键顺序不同且浮点数不匹配时,会抛出TypeError: unsupported operand type(s) for -: 'dict' and 'float'异常,而键顺序一致时则能正常给出错误提示。

实现代码

import pytest
from _pytest.python_api import ApproxMapping
from collections.abc import Mapping

def nested_approx(expected, rel=None, abs=None, nan_ok=False):
    if isinstance(expected, Mapping):
        return ApproxNestedMapping(expected, rel, abs, nan_ok)
    else:
        return pytest.approx(expected, rel=rel, abs=abs, nan_ok=nan_ok)

class ApproxNestedMapping(ApproxMapping):
    def _yield_comparisons(self, actual):
        if not isinstance(actual, Mapping):
            raise TypeError(f"Expected a mapping but got {type(actual)}")

        # 确保键完全匹配
        expected_keys = set(self.expected.keys())
        actual_keys = set(actual.keys())

        if expected_keys != actual_keys:
            missing = expected_keys - actual_keys
            extra = actual_keys - expected_keys
            raise AssertionError(f"Key mismatch:\n  Missing: {missing}\n  Extra: {extra}")

        # 按排序后的键比较,避免顺序影响结果
        for k in sorted(expected_keys):
            yield actual[k], nested_approx(
                self.expected[k],
                rel=self.rel,
                abs=self.abs,
                nan_ok=self.nan_ok
            )

    def _check_type(self):
        if not isinstance(self.expected, Mapping):
            raise TypeError(f"Expected a mapping but got {type(self.expected)}")

测试案例与问题复现

失败测试(键顺序不同+浮点数不匹配)

def test_redux_this_fails():
    actual = {
        'trail_angle': 10.593548226756798,
        'joint': { },
    }

    expected = {
        "joint": { },
        "trail_angle": 10.463173570164972,
    }
    assert actual == nested_approx(expected)

错误输出

_______________________________ test_redux_this_fails ________________________________

    def test_redux_this_fails():
        actual = {
            'trail_angle': 10.593548226756798,
            'joint': { },
        }
    
        expected = {
            "joint": { },
            "trail_angle": 10.463173570164972,
        }
>       assert actual == nested_approx(expected)
E       AssertionError: assert {'trail_angle': 10.593548226756798, 'joint': {}} == approx({'joint': {}, 'trail_angle': 10.463173570164972 ± 1.0e-05})
E         
E         (pytest_assertion plugin: representation of details failed: /home/mmachenry/src/keystone/ScheduleServer/services/rollingmodel/.tox/py39/lib/python3.9/site-packages/_pytest/python_api.py:267: TypeError: unsupported operand type(s) for -: 'dict' and 'float'.
E          Probably an object has a faulty __repr__.)

正常通过测试(浮点数匹配,键顺序不同)

def test_redux_this_passes():
    actual = { 
        'trail_angle': 10.593548226756798,
        'joint': { },
    }   

    expected = { 
        "joint": { },
        'trail_angle': 10.593548226756798,
    }   
    assert actual == nested_approx(expected)

正常失败测试(键顺序一致+浮点数不匹配)

def test_redux_this_fails_with_a_reasonable_error():
    actual = { 
        'joint': { },
        "trail_angle": 10.463173570164972,
    }   

    expected = { 
        "joint": { },
        'trail_angle': 10.593548226756798,
    }   
    assert actual == nested_approx(expected)

问题原因分析

异常出现在pytest生成错误提示的阶段,而非比较逻辑本身:

  • 我们在_yield_comparisons中按排序后的键进行比较,但ApproxMapping父类生成错误差异时,使用的是原始字典的插入顺序来配对实际值和期望值。
  • 当键顺序不同时,pytest会错误地将实际字典的第一个值(浮点数)与期望值的第一个值(空字典)配对做差值计算,导致dict - float的类型错误。

解决方案

重写ApproxNestedMapping的__repr__方法,确保错误提示中的期望值按排序后的键展示,与我们的比较顺序一致:

class ApproxNestedMapping(ApproxMapping):
    def _yield_comparisons(self, actual):
        if not isinstance(actual, Mapping):
            raise TypeError(f"Expected a mapping but got {type(actual)}")

        expected_keys = set(self.expected.keys())
        actual_keys = set(actual.keys())

        if expected_keys != actual_keys:
            missing = expected_keys - actual_keys
            extra = actual_keys - expected_keys
            raise AssertionError(f"Key mismatch:\n  Missing: {missing}\n  Extra: {extra}")

        sorted_keys = sorted(expected_keys)
        for k in sorted_keys:
            yield actual[k], nested_approx(
                self.expected[k],
                rel=self.rel,
                abs=self.abs,
                nan_ok=self.nan_ok
            )

    def _check_type(self):
        if not isinstance(self.expected, Mapping):
            raise TypeError(f"Expected a mapping but got {type(self.expected)}")

    def __repr__(self):
        # 按排序后的键生成repr,避免顺序问题
        sorted_items = sorted(self.expected.items())
        approx_items = []
        for k, v in sorted_items:
            if isinstance(v, Mapping):
                approx_items.append(f"{repr(k)}: {nested_approx(v, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok)!r}")
            else:
                approx_items.append(f"{repr(k)}: {pytest.approx(v, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok)!r}")
        return f"approx({{{', '.join(approx_items)}}})"

修改后,错误提示中的期望值会与比较顺序一致,pytest计算差异时就不会出现类型不匹配的问题了。


内容的提问来源于stack exchange,提问作者mmachenry

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 06:30:00