用 Hypothesis @given 与 lists 测试去重函数并自动缩小反例

举报
yd_217513249 发表于 2026/10/25 01:05:41 2026/10/25
【摘要】 介绍如何用 Hypothesis 的 @given 与 lists 策略为去重函数编写属性测试,断言输出无重复且元素均来自输入,并集成 pytest;当属性失败时,Hypothesis 会自动缩小到最小反例,帮助定位边界问题。

问题场景

去重函数看起来简单,但很容易在边界上出错:空列表、全是重复值、混入未在输入中出现的元素、返回结果内部仍有重复等。与其只写几个手写用例,不如把去重函数的核心契约写成属性:输出中没有重复元素,输出中的每个元素都来自输入,且输出长度不应超过输入长度。Hypothesis 的 @given 与 lists 策略可以自动生成大量列表输入,并在断言失败时把输入缩小到更简单的反例,方便定位问题。

可运行的最小示例

先准备环境:

pip install hypothesis pytest

实现一个保持首次出现顺序的去重函数:

# dedupe.py
def dedupe_preserve_order(seq):
    seen = set()
    result = []
    for item in seq:
        if item not in seen:
            seen.add(item)
            result.append(item)
    return result

对应的属性测试文件如下。这里使用 st.lists(st.integers()) 生成可哈希的整数列表,因此可以直接用 set 判断重复与来源:

# test_dedupe.py
from hypothesis import given, strategies as st
from dedupe import dedupe_preserve_order

@given(st.lists(st.integers()))
def test_dedupe_properties(seq):
    result = dedupe_preserve_order(seq)
    assert len(result) == len(set(result)), '输出存在重复元素'
    assert set(result).issubset(set(seq)), '输出包含输入中不存在的元素'
    assert len(result) <= len(seq), '输出长度不应超过输入'

运行方式:

pytest -q test_dedupe.py

pytest 会自动发现 test_*.py 文件。Hypothesis 与 pytest 集成良好,通常不需要额外插件。上面的测试如果实现正确,会正常通过;如果实现错误,Hypothesis 会报告失败用例。

失败时自动缩小反例

为了观察缩小行为,可以故意写一个错误实现:空列表也插入 0,从而破坏“输出元素均来自输入”的属性。

# test_dedupe_buggy.py
from hypothesis import given, strategies as st

def dedupe_buggy(seq):
    return [0] + list(set(seq))

@given(st.lists(st.integers()))
def test_dedupe_buggy(seq):
    result = dedupe_buggy(seq)
    assert set(result).issubset(set(seq)), '输出包含输入中不存在的元素'

运行:

pytest -q test_dedupe_buggy.py

当 seq 为空列表时,dedupe_buggy([]) 返回 [0],而 0 并不来自输入,因此断言失败。Hypothesis 在发现失败后不会只停留在随机的大列表上,而会尝试更简单的输入,最终通常会把反例缩小到空列表或只包含少量元素的列表。若错误实现是把输入原样返回,则最小反例往往会缩小到类似 [0, 0] 这样的重复元素列表。这个过程就是 shrinking,它让失败原因更直观。

还可以用 @example 固定补充边界用例,与自动生成策略配合使用:

from hypothesis import example, given, strategies as st
from dedupe import dedupe_preserve_order

@given(st.lists(st.integers()))
@example([])
@example([1, 1, 2, 2])
def test_dedupe_properties_with_examples(seq):
    result = dedupe_preserve_order(seq)
    assert len(result) == len(set(result)), '输出存在重复元素'
    assert set(result).issubset(set(seq)), '输出包含输入中不存在的元素'

边界与限制

第一,属性测试不是形式证明。Hypothesis 默认只尝试有限数量的例子,可以通过 @settings(max_examples=...) 增加尝试次数,但会增加运行时间。

第二,st.lists(st.integers()) 适合可哈希元素。如果元素不可哈希,例如列表或字典,set(result) 和 set(seq) 会失败。此时需要调整策略,例如使用 st.lists(st.tuples(st.integers(), st.integers())) 生成可哈希元组,或把来源断言改为 all(item in seq for item in result),但要注意后者是线性查找,列表很大时会变慢。

第三,浮点特殊值需要谨慎。NaN 在集合中的行为容易让人困惑,可能影响重复判断。如果去重函数面向浮点数,建议明确语义,必要时避开 NaN 或单独处理。

第四,列表长度默认有上限。需要覆盖超长列表时,可以使用 st.lists(st.integers(), max_size=...) 控制生成范围,同时注意内存与执行时间。

第五,本示例只断言“无重复”和“元素来自输入”,没有断言保持原顺序。如果顺序也是契约的一部分,可以额外断言首次出现顺序,例如 result == list(dict.fromkeys(seq)),但同样要求元素可哈希且语义明确。

把这些属性测试与 pytest 集成后,去重函数的边界问题会更容易暴露,失败时也能借助 Hypothesis 的缩小机制快速找到最小反例。

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。