Hypothesis 递归数据生成全解:用 st.recursive 打造树、JSON 与任意嵌套结构

发布时间:2026/9/25 2:14:40
Hypothesis 递归数据生成全解:用 st.recursive 打造树、JSON 与任意嵌套结构 测试开发工具【免费下载链接】hypothesisThe property-based testing library for Python项目地址https://gitcode.com/gh_mirrors/hy/hypothesis点击查看免费下载本文聚焦 Hypothesis 属性测试库中最具威力的策略之一 ——st.recursive。当你需要生成树形结构、任意嵌套的 JSON、递归定义的表达式等数据内部嵌套同类型数据的场景时st.recursive是官方推荐的结构化递归方案。阅读本文后你将掌握st.recursive的完整用法、max_leaves/min_leaves两个关键参数的精确语义、底层实现原理为什么它不会触发RecursionError以及如何用它写出可复用的自定义递归策略。什么是递归数据为什么需要专门策略递归数据是指为了画出一个值可能需要从同一个策略中再画一些值的数据类型。典型例子包括树形结构树的节点要么是单个布尔值叶子要么是由两棵子树组成的二元组任意 JSONJSON 值本身可以是对象、数组而对象和数组的值又可以嵌套 JSON数学表达式expr 数值 | expr expr | (expr)任意嵌套的列表[True]、[False, []]、[[[[True]]]]。这些结构有一个共同点类型定义中存在对自身的引用。Hypothesis 在hypothesis.strategies模块中提供了recursive函数来优雅地处理这类递归并且它以受控的方式生成数据避免无限膨胀。本篇即围绕这一策略展开原始文章见 recursive-data.md。第一步尝试用 st.composite 手写递归为什么行不通先看一个直观的树定义树要么是单个布尔值叶子要么是两棵子树的二元组。直觉上你可以用st.composite直接写递归import hypothesis.strategies as st st.composite def composite_tree(draw): return draw( st.one_of( st.booleans(), st.tuples(composite_tree(), composite_tree()), ) )如果你尝试从这个策略画例子大概率只会看到三种结果得到单个布尔值得到一棵非常大的树直接抛RecursionError栈溢出。几乎看不到非平凡的中等大小的例子。原因无约束递归的规模爆炸这种递归的本质是如果把它当作朴素的随机生成过程树的期望规模是无穷大。Hypothesis 虽然内置了一些限制器来防止真的生成无穷大的数据但在非平凡的情况下它依然倾向于画出非常大的树更重要的是它无法解决 Python 层面的递归问题 ——st.composite的递归是在 Python 调用栈上真实发生的深度一大就会栈溢出。因此与其使用这种无结构的递归Hypothesis 提供了一种更结构化的递归方式让引擎能更有效地控制生成数据的规模。这正是st.recursive。st.recursive 的基本用法base extend使用recursive策略需要两个组成部分base 策略负责生成简单的、非递归的基础实例如叶子节点extend 函数接收一个子策略children该子策略生成你想要的那种类型的数据返回一个生成更大实例的新策略。回到布尔树例子叶子用booleans()扩展逻辑就是两棵子树组成一个二元组import hypothesis.strategies as st recursive_tree st.recursive( st.booleans(), lambda children: st.tuples(children, children) )生成效果立刻脱胎换骨 —— 相比 composite 版本它画出的中、小规模树质量高得多而且永远不会触发 RecursionError recursive_tree.example() ((False, True), ((True, True), False) recursive_tree.example() ((((False, False), True), False), False) recursive_tree.example() (False, True) recursive_tree.example() True这一 API 从 2016 年发布至今保持稳定recursive在 hypothesis/src/hypothesis/strategies/init.py 中被公开导出其签名与文档字符串位于 core.pydef recursive( base: SearchStrategy[Ex], extend: Callable[[SearchStrategy[Any]], SearchStrategy[T]], *, min_leaves: int | None None, max_leaves: int 100, ) - SearchStrategy[T | Ex]:官方 docstring 用一句话概括其语义返回的策略S满足S extend(base | S)即每个值要么直接来自base要么来自反复套用extend和|后可达的任意策略。例如recursive(booleans(), lists)会生成任意嵌套、任意混合的布尔列表False、[True]、[False, []]、[[[[True]]]]都是合法取值。递归策略的工作原理逐层叠出的策略序列原文章给出了一个理解recursive的心智模型它相当于反复构建如下的策略序列s1 base s2 one_of(s1, extend(s1)) s3 one_of(s2, extend(s2)) ...最终从这条无限序列中挑一个策略再画值原文也诚实指出实际并非完全如此但非常接近。从源码看RecursiveStrategy的实现确实是在构造一个受max_leaves限制的有限策略列表见 recursive.pystrategies [self.limited_base, self.extend(self.limited_base)] while 2 ** (len(strategies) - 1) max_leaves: strategies.append(extend(OneOfStrategy(tuple(strategies)))) # If min_leaves 1, we can never draw from base directly if min_leaves is not None and min_leaves 1: strategies strategies[1:] self.strategy OneOfStrategy(strategies)要点解读第一层是受限的基础策略limited_base叶子层之后每一层都是extend作用在前 N 层策略的one_of并集上即在之前所有可能性之上再扩展一层层数受max_leaves约束由于2 ** (len - 1) max_leaves对默认max_leaves100最多构建约 8 层。递归深度因此被数学上地封顶这正是它永不触发RecursionError的根本原因 —— 与 composite 方案在 Python 栈上真实递归形成鲜明对比最终的self.strategy是所有这些层的OneOfStrategy画值时在有限层集合中做出选择。叶子计数机制LimitedStrategy 与 cappedRecursiveStrategy还配套了一个内部类LimitedStrategy其职责是统计从 base 里画了多少个值通过capped(max_templates)上下文管理器设置一个计数器marker每次从 base 画一个值marker减 1当marker归零后仍试图画值就抛出内部异常LimitReached。对应的画值逻辑do_drawrecursive.py在捕获LimitReached后会把该次尝试记为事件exceeded max_leaves ... had to be retried并重新尝试从而保证最终结果满足叶子数上限。测试 test_recursive.py 验证了这一点max_leaves10时把任意嵌套结构压平后叶子总数不超过 10。用 max_leaves 控制规模默认值与调整策略recursive的第三个关键字参数max_leaves控制从 base 策略画出的值的数量上限。 st.recursive(st.booleans(), lambda children: st.tuples(children, children), max_leaves2).example() True st.recursive(st.booleans(), lambda children: st.tuples(children, children), max_leaves2).example() (True, False)当前版本源码中max_leaves的默认值是100见 core.py倾向于产出中等规模的值原文章写作时2016 年的默认值为 50。不同版本以你实际安装版本的 API 文档为准如 reference/strategies.rst 的autofunction页面。max_leaves的作用是防止规模失控某些extend函数会让数据规模飞速增长如果不设上限很容易画出庞大到难以缩小、也难以复用的例子。一个典型的规模爆炸例子 —— 用列表作为扩展 st.recursive(st.booleans(), lambda children: st.lists(children, min_size3)).example() [[False, True, False, False, False, True, True, True, False, False, False, True, True, False], False, [False, True, False, True, False], [True, False, True, False, False, False]]在这个例子中因为extend每个子节点展开成一个至少 3 元素的列表让策略规模增长极快生成结果会顶到max_leaves上限。如果你想要更大的例子就需要把max_leaves调高。反之测试 test_can_exclude_branching_with_max_leaves 展示了一个极端情况max_leaves1时只能画到叶子完全无法产生分支 —— 这是叶子配额最直观的体现。min_leaves源码新增的下限参数原文章写作时尚不存在的min_leaves参数在现版本的 API 中已经可用core.pymin_leaves单次运行中至少要从 base 画出的元素数量默认None表示不设下限当min_leaves 1时策略列表会跳过第一层直接画 base 不可能满足下限即源码中的strategies strategies[1:]若一次画值没能达到min_leavesdo_draw会记录事件并重试最多重试 5 次仍不满足则将该次尝试标记为无效mark_invalid见 recursive.py。测试 test_respects_min_leaves 通过随机抽取min_leaves与max_leaves断言所有生成的树都满足min_leaves 叶子数 max_leavestest_can_set_exact_leaf_count 则验证min_leaves5, max_leaves5时精确生成 5 个叶子 —— 想强制生成恰好 N 个叶子的样例这是现成的做法。参数校验无效输入会被当场拒绝RecursiveStrategy.do_validaterecursive.py会在策略使用前做一系列校验常见陷阱包括base必须是策略check_strategy(self.base, base)extend的返回也必须是策略否则抛InvalidArgumentmax_leaves与min_leaves必须是正整数且min_leaves max_leavesmin_leaves max_leaves时抛InvalidArgument见 recursive.pyextend若是恒等函数lambda x: x会发出HypothesisWarningno-op你多半应该直接用 base 策略recursive.pyextend完全不引用其参数时无法真正递归在min_leaves缺省时以弃用警告提示、在显式传入min_leaves时直接抛InvalidArgumentrecursive.py。上述全部规则都有测试背书test_invalid_args 覆盖了max_leaves-1/0/1.0、min_leaves-1/0/1.0、min_leaves10, max_leaves5等非法组合test_identity_extend_warns 验证恒等extend的告警test_recursive_call_validates_expand_returns_strategies 验证extend返回非策略如lambda x: 1会被拒绝。实战用 recursive 封装一个 JSON 生成器st.recursive最经典的工程化用法是封装任意 JSON策略。官方教程 custom-strategies.rst 给出了可直接复用的实现def json(*, finite_onlyTrue): Helper function to describe JSON objects, with optional inf and nan. numbers st.floats(allow_infinitynot finite_only, allow_nannot finite_only) return st.recursive( st.none() | st.booleans() | st.integers() | numbers | st.text(), extendlambda xs: st.lists(xs) | st.dictionaries(st.text(), xs), )base所有标量叶子的并集 ——none、布尔、整数、浮点、文本extend把任意子策略xs扩展为元素类型为 xs 的列表或键为文本、值为 xs 的字典从而支持任意深度嵌套的数组与对象得益于recursive的层叠结构与max_leaves限额这个策略能稳定产出从小到大的真实 JSON而不会在嵌套深度的暴走上失控。recursive还被用于其他官方文档示例中例如 how-to/external-fuzzers.rst 里面向外部模糊测试工具的种子生成。若需要缩到最小的反例recursive的缩小行为同样友好docstring 明确指出例子会先尝试减少递归量再按 base 与 extend 各自策略的缩小行为进行收缩core.py测试 test_can_find_nested 证实在recursive(st.booleans(), lambda x: st.tuples(x, x))上寻找首个元素仍是二元组的最简反例会稳定得到((False, False), False)。小结什么时候用 recursive用st.recursive(base, extend)当数据是以自身为子节点的递归结构树、JSON、嵌套列表、表达式需要可控规模且绝不栈溢出时 —— 这是首选别用st.composite直接自递归无结构递归会导致规模爆炸与RecursionError且几乎画不出有价值的非平凡小样例规模调控靠max_leaves上限默认 100与min_leaves下限默认不限extend增长越猛越容易顶到上限需要更大样例就调高max_leaves要求恰好/至少若干叶子时组合两者即可记得校验参数min_leaves max_leaves、二者皆为正整数、extend必须真正使用其参数 —— 源码与测试已把这些约束固化写错会在使用时报InvalidArgument或告警便于及早发现。如需进一步验证行为或深入实现可继续阅读源码 recursive.py、参数定义 core.py 以及覆盖测试 test_recursive.py。赞分享测试开发工具【免费下载链接】hypothesisThe property-based testing library for Python项目地址https://gitcode.com/gh_mirrors/hy/hypothesis点击查看免费下载相关推荐PHP 8.5.7 运行时 RCE 漏洞实战StreamBucket 类型混淆到 SOAP 数字 Cookie 完整 PoC 解析PHP 8.5.7 运行时 RCE 漏洞实战StreamBucket 类型混淆到 SOAP 数字 Cookie 完整 PoC 解析 一句话概要 本文解析 E网络安全渗透测试示例工程AtlasOS让你的Windows 11性能飙升的开源优化神器AtlasOS让你的Windows 11性能飙升的开源优化神器 AtlasOS是一款专为Windows 11设计的开源系统优化工具通过精简不必要的后台服务、操作系统隐私合规Bend 如何用 fold 与 bend 对递归数据结构求和并生成树Bend 如何用 fold 与 bend 对递归数据结构求和并生成树 Bend 是一种面向大规模并行的高层编程语言。它的变量不可变也没有 for 循环所以编程语言编译器语言运行时高性能计算上一篇justified-layout高级用法突破传统网格的创意布局设计下一篇cAdvisor生产环境部署终极指南高可用和扩展性配置完全解析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考