ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

用 Hypothesis 属性测试破解浮点均值:从溢出到次正规数的完整实战指南

用 Hypothesis 属性测试破解浮点均值:从溢出到次正规数的完整实战指南 测试开发工具【免费下载链接】hypothesisThe property-based testing library for Python项目地址https://gitcode.com/gh_mirrors/hy/hypothesis点击查看免费下载给定一个由普通浮点数无 NaN、无 Infinity仅简单有限浮点数组成的列表计算其均值平均。这看起来是一个教科书级别的入门问题——但事实证明这是一道难题想做到大致正确都很困难。本篇指南以 Hypothesis 官方案例为基础逐步演示如何用属性测试property-based testing暴露朴素均值实现中的浮点缺陷剖析底层原因溢出、舍入误差、次正规数并引入min(ls) mean(ls) max(ls)这类宽松约束作为普适的测试技巧。读完你将掌握given、floats(allow_nanFalse, allow_infinityFalse)、lists(..., min_size1)的组合用法浮点极端值最大有限数、次正规数为何能让sum(ls)/len(ls)、numpy均值甚至 Python 3.4statistics模块全部翻车以及先保证不崩溃、再叠加结果约束的分层测试策略。问题计算一个浮点数列表的均值假设你有一个浮点数列表元素都是普通浮点数——没有 NaN也没有 Infinity只是常见的、有限的浮点数。任务是计算均值平均值。关键问题在于你做得对吗这个问题远比看起来困难。为了验证正确性先写出一个基于 Hypothesis 的属性测试from hypothesis import given from hypothesis.strategies import floats, lists given(lists(floats(allow_nanFalse, allow_infinityFalse), min_size1)) def test_mean_is_within_reasonable_bounds(ls): assert min(ls) mean(ls) max(ls)这个测试对正确性的要求其实极其宽松它只要求均值落在列表的最小值和最大值之间完全没有验证均值与各元素的精确关系。事实上min、max、中位数等一大堆函数都能通过这个测试——它们根本不是均值。然而几乎所有人的均值实现都过不了这个测试。这就是属性测试的价值即使是如此宽松的约束也能轻松揪出浮点计算的深层错误。先理解测试用到的两个策略它们分别来自 floats 定义 与 lists 定义floats(allow_nanFalse, allow_infinityFalse)生成有限的浮点数。在源码 numbers.py 中可以看到allow_nan默认在min_value与max_value均为None时为True这里显式关闭 NaN 与 Infinity正是为了模拟没有 nasty tricks的普通浮点数场景。lists(..., min_size1)生成长度至少为 1 的浮点数列表避免对空列表求均值。源码中的校验逻辑保证0 min_size max_size见 collections.py。第一个实现sum(ls) / len(ls)与溢出按照均值定义直接写一个实现def mean(ls): return sum(ls) / len(ls)这看起来足够合理——它就是均值的定义——但它是错的。Hypothesis 立刻给出反例assert inf 8.98846567431158e307 where inf mean([8.988465674311579e307, 8.98846567431158e307]) and 8.98846567431158e307 max([8.988465674311579e307, 8.98846567431158e307]) Failing test case: test_mean_is_within_reasonable_bounds( ls[8.988465674311579e307, 8.98846567431158e307] )问题根源在于有限的浮点数可能大到它们的和溢出为无穷大。两个约8.988e307的数相加这是接近 IEEE-754 double 最大有限值的量级其结果超出 double 的表示范围溢出为inf。接下来用有限数除inf仍然得到inf——而inf显然超出了[min(ls), max(ls)]的范围。这是经典的先求和后求平均陷阱分子在求和阶段就已溢出无论分母列表长度多么普通都于事无补。第二个实现先除后加与精度丢失为了避免求和溢出尝试先把每个数除以长度再求和def mean(ls): return sum(l / len(ls) for l in ls)这次的反例完全不同assert min(ls) mean(ls) max(ls) assert 1.390671161567e-309 1.390671161566996e-309 where 1.390671161567e-309 min([1.390671161567e-309, 1.390671161567e-309, 1.390671161567e-309]) and 1.390671161566996e-309 mean([1.390671161567e-309, 1.390671161567e-309, 1.390671161567e-309]) Failing test case: test_mean_is_within_reasonable_bounds( ls[1.390671161567e-309, 1.390671161567e-309, 1.390671161567e-309] )注意反例中的量级1.390671161567e-309。这是次正规数subnormal number——接近 double 能表示的最小正数。这次的问题不是溢出而是浮点数的精度限制浮点数只能精确表示2 的幂乘以整数形式的数值除以 3 会引入舍入误差导致(x / 3) * 3 ! x在一般情况下成立列表中有三个1.390671161567e-309每个元素先除以 3 再求和三次舍入误差累积后均值竟比最小值还小一点点1.390671161566996e-309 1.390671161567e-309。从源码结构看Hypothesis 的floats策略在无界时默认allow_subnormalTrue见 numbers.py 的推断逻辑因此它会系统性地生成次正规数来考验你的实现这正是先除后加方案翻车的原因。仓库中 test_subnormal_floats.py 还验证了allow_subnormalTrue与边界参数组合时的参数校验行为说明次正规数是该策略的一等公民。现有实现的战绩numpy 与 Python 3.4 statistics用同样的测试考验现有实现。numpy 版本import numpy as np def mean(ls): return np.array(ls).mean()它撞上了第一个实现同样的问题——求和溢出assert min(ls) mean(ls) max(ls) assert inf 8.98846567431158e307 where inf mean([8.988465674311579e307, 8.98846567431158e307]) and 8.98846567431158e307 max([8.988465674311579e307, 8.98846567431158e307]) Failing test case: test_mean_is_within_reasonable_bounds( ls[8.988465674311579e307, 8.98846567431158e307] )Python 3.4 新增的statistics模块同样失败该问题在 3.5.2 中修复OverflowError: integer division result too large for a float Failing test case: test_mean_is_within_reasonable_bounds( ls[8.988465674311579e307, 8.98846567431158e307] )与溢出为无穷不同这里直接抛出OverflowError。原因在于statistics模块内部把一切转换为Fraction类型——一种任意精度有理数类型——而从 Fraction 转换回 float 的时机与位置的细节问题导致产生了无法轻易转回 float 的有理数。结论很清晰连语言标准库和主流数值库的朴素均值实现都无法满足一个如此宽松的属性测试。作弊方案钳制clamp能过测试但不是均值要让测试通过其实很容易——只要作弊不去真正计算均值即可def clamp(lo, v, hi): return min(hi, max(lo, v)) def mean(ls): return clamp(min(ls), sum(ls) / len(ls), max(ls))即直接把结果限制在期望区间[min(ls), max(ls)]内。这个函数能满足上述测试但它掩盖了真实的计算错误并不值得推荐——它提醒我们通过属性测试不代表实现正确属性测试的价值在于证伪而非证明。真正正确的均值实现能够通过该测试相当困难业界甚至有针对计算两个数的均值这一子问题的专门研究论文。这一测试揭示的通用技巧与浮点测试的反思这个例子是通用测试策略的一个绝佳示范也与 Getting started with Hypothesis 中提出的思路一脉相承先让代码不崩溃先用随机数据调用函数处理掉你不关心的异常Hypothesis 提供reject()丢弃无效样本。再叠加对结果值的约束一旦不崩溃测试跑通就可以开始对返回值施加额外约束。即使约束非常宽松——如本例的min(ls) mean(ls) max(ls)——也常常能捕获有趣的 bug。该测试还揭示了一个更深层的问题浮点数学非常难因此它不太适合用 Hypothesis 测试。这不是因为 Hypothesis 不擅长测试浮点代码恰恰相反正是因为它太擅长揭示编程实际有多难——而浮点代码比人们愿意承认的还要难得多。Hypothesis 会发现的这类 bug多数开发者往往不打算修复态度通常是这些数字太怪了我们不太关心大概够用了。很少有人愿意为浮点正确性投入数值敏感性分析numerical sensitivity analysis所需的工作量。结语直面困难而非回避作者坦承因为告诉人们他们不想修的 bug 既换不来 bug 修复也换不来朋友已经不再常用这个例子来演示 Hypothesis。但了解这个问题的存在仍然值得编程确实很难忽略问题并不会让它变简单。你可以暂时无视正确性问题直到它们真正咬到你但最好在它们发生时不要感到意外。通用技术仍然值得记住这个技巧不只对浮点数有用——大多数代码都能从中受益而且大多数时候测试告诉你的 bug 远没有这么令人不适。Hypothesis 的官方入门教程introduction.rst同样展示了先lists(integers())排序、再加floats(allow_nanFalse)的渐进式约束思路与本例异曲同工。下次面对简单的数值问题时不妨先问一句我的实现真的正确吗让 Hypothesis 来回答。赞分享测试开发工具【免费下载链接】hypothesisThe property-based testing library for Python项目地址https://gitcode.com/gh_mirrors/hy/hypothesis点击查看免费下载相关推荐Qlib当量化投资遇上AI普通人的智能投资新体验Qlib当量化投资遇上AI普通人的智能投资新体验 你知道吗曾经需要博士学历和多年编程经验才能玩转的量化投资现在有了全新的打开方式。Qlib——这个由微软测试开发工具终极指南如何快速掌握Hypothesis属性测试从新手到专家终极指南如何快速掌握Hypothesis属性测试从新手到专家 Hypothesis是一个强大、灵活且易于使用的Python属性测试库它能帮助开发者编写更健壮测试开发工具OpenCore Legacy Patcher完全实操8步免费让旧Mac流畅运行最新macOSOpenCore Legacy Patcher完全实操8步免费让旧Mac流畅运行最新macOS 软件更新永远停在了旧版本、新应用纷纷提示系统版本过低、换机CLI数据分析上一篇洛雪音乐助手免费跨平台音乐播放器的完整使用指南下一篇在 Create React App 项目中集成 Socket.IO从实时服务器到 React 客户端完整实战创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表