为何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
相关产品推荐
相关产品推荐

