- 测试
- 开发工具
【免费下载链接】hypothesis
The property-based testing library for Python
本指南以 Hypothesis 官方文档 type-strategies.rst 为核心,系统讲解如何为基于 Hypothesis 的属性测试(property-based testing)编写类型提示:从SearchStrategy[T]泛型的基础用法、st.composite装饰器下正确标注返回值类型,到SearchStrategy的协变(covariance)语义及其对类型检查器推断的影响。读完本文,你将掌握为自定义策略函数和组合策略编写准确、可被 mypy / Pyright / Pyre 等类型检查工具验证的类型签名,让策略代码享受完整的静态类型保障。所有结论均有当前仓库源码佐证。
为什么策略需要类型提示
Hypothesis 为所有策略以及所有返回策略的函数提供了类型提示(type hints)。这意味着从hypothesis.strategies(常简写为st)导入的每个策略构造器——如st.integers()、st.lists()——其返回值的类型都能被静态类型检查器精确识别。
from hypothesis import strategies as st reveal_type(st.integers()) # SearchStrategy[int] reveal_type(st.lists(st.integers())) # SearchStrategy[list[int]]reveal_type是 mypy、Pyright、Pyre 等类型检查器提供的诊断函数:它会直接在错误输出中打印表达式的推断类型。上述两行揭示出:
st.integers()返回SearchStrategy[int],即生成整数值的策略;st.lists(st.integers())返回SearchStrategy[list[int]],即生成整数列表的策略。
这些类型提示的实现位于仓库的 strategies/init.py(SearchStrategy等公共符号在此重新导出,源码注释明确写道“The implementation of all of these lives in_strategies.py,but we re-export them via this module to avoid exposing implementation details”),真正带类型参数的定义在 strategies/_internal/strategies.py 与 strategies/_internal/core.py 中。
SearchStrategy:策略的类型
SearchStrategy是策略的类型,定义于 strategies/_internal/strategies.py#L255-L261。它是泛型(generic)的:类型参数即为该策略所生成值的类型。该类在源码 docstring 中明确说明:
"A
SearchStrategytells Hypothesis how to generate that kind of input. This class is only part of the public API for use in type annotations, so that you can write e.g.-> SearchStrategy[Foo]for your function which returnsbuilds(Foo, ...). Do not inherit from or directly instantiate this class."
即:SearchStrategy只用于类型标注,不要继承或直接实例化它。
一个典型用法是给“返回策略的函数”写返回类型注解:
from hypothesis import strategies as st from hypothesis.strategies import SearchStrategy # returns a strategy for "normal" numbers def numbers() -> SearchStrategy[int | float]: return st.integers() | st.floats(allow_nan=False, allow_infinity=False)这里的int | float是 Python 3.10+ 的联合类型(union type)写法。st.integers() | st.floats(...)使用的是策略的|运算符(等价于st.one_of(...)),它把两个策略组合成一个能同时生成整数与有限浮点数的策略,因此返回类型标注为SearchStrategy[int | float]完全匹配。
策略(strategy)与返回策略的函数(function)的区别
官方文档特意强调了一个容易混淆的点:区分“策略”和“返回策略的函数”。
st.integers是一个函数,调用后返回一个策略;因此st.integers的类型是Callable[..., SearchStrategy[int]];- 而
s = st.integers()中的s是值,其类型是SearchStrategy[int]。
这一区别在源码签名上体现得淋漓尽致。看 numbers.py#L123-L154 中integers的真实签名:
def integers( min_value: int | None = None, max_value: int | None = None, ) -> SearchStrategy[int]:它接收可选的min_value/max_value边界参数,返回SearchStrategy[int]。再看floats的签名(numbers.py#L281-L291):
def floats( min_value: Real | None = None, max_value: Real | None = None, *, allow_nan: bool | None = None, allow_infinity: bool | None = None, allow_subnormal: bool | None = None, width: Literal[16, 32, 64] = 64, exclude_min: bool = False, exclude_max: bool = False, ) -> SearchStrategy[float]:可见每个策略构造器的返回类型都被精确标注为SearchStrategy[具体值类型],这就是类型检查器能够精准推断st.integers()结果为SearchStrategy[int]的根本原因。
其他常见策略的返回类型同样精确,例如:
st.lists(element)→SearchStrategy[list[T]](见 core.py#L305)st.builds(target, ...)→SearchStrategy[target类型](见 core.py#L1184)st.from_type(thing)→SearchStrategy[T],其中T与传入的类型绑定(见 core.py#L1273)
st.composite 下的类型提示写法
当使用@st.composite装饰器定义自定义策略时,类型提示的写法有一个关键规则:标注返回值的类型,而不是SearchStrategy。
@st.composite def ordered_pairs(draw) -> tuple[int, int]: n1 = draw(st.integers()) n2 = draw(st.integers(min_value=n1)) return (n1, n2)这里函数签名写的是-> tuple[int, int],即组合策略最终产出的值的类型。@st.composite装饰器会自动把它包装成一个返回SearchStrategy[tuple[int, int]]的函数——这一包装逻辑正是源码中composite的真实实现(core.py#L2174-L2189):
if typing.TYPE_CHECKING or ParamSpec is not None: P = ParamSpec("P") def composite( f: Callable[Concatenate[DrawFn, P], Ex], ) -> Callable[P, SearchStrategy[Ex]]: return _composite(f)注意composite的类型签名:它接收一个Callable[Concatenate[DrawFn, P], Ex]——即第一个参数是DrawFn类型(draw),其余参数为P,返回值类型为Ex(也就是你标注的返回类型)——并返回Callable[P, SearchStrategy[Ex]]。所以当你在函数上写-> tuple[int, int]时,Ex = tuple[int, int],装饰器返回的新函数类型就是SearchStrategy[tuple[int, int]]的构造器。
DrawFn协议(Protocol)同样定义在源码中(core.py#L2053-L2079),其 docstring 明确写道:
"This type only exists so that you can write type hints for functions decorated with
@composite."
它的签名是:
def __call__(self, strategy: SearchStrategy[Ex], label: object = None) -> Ex:这意味着在@st.composite函数内部,draw(st.integers())会被推断为返回int,draw(st.text())被推断为返回str——draw的返回类型自动等于所传策略的值类型。源码 docstring 中给出了示例:
@composite def list_and_index(draw: DrawFn) -> tuple[int, str]: i = draw(integers()) # type of `i` inferred as 'int' s = draw(text()) # type of `s` inferred as 'str' return i, s底层包装:CompositeStrategy
@st.composite的底层实现会创建一个CompositeStrategy(见 core.py#L2036-L2050):
class CompositeStrategy(SearchStrategy): def __init__(self, definition, args, kwargs): super().__init__() self.definition = definition self.args = args self.kwargs = kwargs def do_draw(self, data): return self.definition(data.draw, *self.args, **self.kwargs)它在执行do_draw时把 ConjectureData 的draw方法作为第一个参数传给原函数,这正是你在@st.composite函数中接收的draw参数的来源。这一实现印证了:类型层面DrawFn协议对应运行时真正的data.draw调用接口,两者一一对应。
补充:给 draw 参数也加上类型
虽然官方示例中draw参数未加注解,但为了更好的类型检查体验,可以显式标注:
from hypothesis.strategies import DrawFn @st.composite def ordered_pairs(draw: DrawFn) -> tuple[int, int]: n1 = draw(st.integers()) n2 = draw(st.integers(min_value=n1)) return (n1, n2)DrawFn可以从hypothesis.strategies导入(已在 strategies/init.py 的__all__中导出,见该文件第 68 行)。这样draw(...)的返回值类型就能被静态推断,n1、n2都是int,进而min_value=n1的传参也能通过类型检查。
SearchStrategy 的协变性(Covariance)
含义
SearchStrategy是协变(covariant)的,即:如果B < A(B 是 A 的子类型),那么SearchStrategy[B] < SearchStrategy[A](SearchStrategy[B]是SearchStrategy[A]的子类型)。
用官方文档的例子:策略st.from_type(Dog)是策略st.from_type(Animal)的子类型(其中Dog继承自Animal)。这符合直觉——凡是能生成Animal的地方,都能接受一个只生成Dog的策略。
源码证据
协变语义在 strategies/_internal/strategies.py#L64-L67 中通过TypeVar的covariant=True参数实现:
if TYPE_CHECKING: Ex = TypeVar("Ex", covariant=True, default=Any) else: Ex = TypeVar("Ex", covariant=True) class SearchStrategy(Generic[Ex]): # L255Ex声明为covariant=True的TypeVar,SearchStrategy继承自Generic[Ex]。当类型检查器看到SearchStrategy[Dog]与SearchStrategy[Animal]时,就能依据Dog < Animal推导出前者是后者的子类型。
协变在实践中的价值
协变让策略可以在函数参数与返回位置灵活替换。例如,下面这个接受“任意动物策略”的函数:
def run_experiment(animals: SearchStrategy[Animal]) -> None: ... run_experiment(st.from_type(Dog)) # OK:协变,SearchStrategy[Dog] 可传给 SearchStrategy[Animal] run_experiment(st.from_type(Animal)) # OK如果把SearchStrategy设计成不变(invariant)的,则run_experiment(st.from_type(Dog))会直接报类型错误,即使从语义上完全合理。正是协变设计让这种直观的用法得以通过类型检查。
需要说明的是:SearchStrategy的协变是在类型层面由TypeVar(covariant=True)声明的性质;类型检查器(mypy、Pyright、Pyre 等)在静态分析时依据该声明进行子类型推断,运行时并不存在子类型关系的强制检查。
类型提示的验证与测试保障
Hypothesis 仓库自身就用测试保障了这些类型提示的准确性。例如 tests/cover/test_annotations.py 中有如下断言(第 99 行附近):
assert sig_comp.return_annotation == st.SearchStrategy[int]它验证组合策略函数签名的返回注解确实是st.SearchStrategy[int]。此外,仓库还维护了whole_repo_tests/types/目录下的类型测试套件(test_mypy.py、test_pyright.py、test_hypothesis.py),用真实类型检查器对整仓库代码进行验证,确保策略 API 的类型提示不会退化。
实践小结
| 场景 | 正确的类型标注 | 说明 |
|---|---|---|
| 函数返回一个策略 | -> SearchStrategy[int] | 返回的是“策略”本身 |
使用@st.composite定义策略 | -> tuple[int, int](标注值的类型) | 装饰器会包装为SearchStrategy[tuple[int, int]]构造器 |
@st.composite函数内部 | draw: DrawFn或省略 | draw(strategy)的返回值类型自动等于策略值类型 |
| 多个策略组合 | SearchStrategy[int \| float] | 用\|或st.one_of组合后类型取联合 |
核心要点回顾:
SearchStrategy[T]是策略的泛型类型,T是策略生成值的类型;它是公共 API 中仅用于类型标注的类,不要继承或实例化;st.integers(函数)的类型是Callable[..., SearchStrategy[int]],而s = st.integers()(值)的类型是SearchStrategy[int],两者务必区分;@st.composite下标注返回值的类型,而不是SearchStrategy[...],装饰器负责自动包装;SearchStrategy是协变的:B < A蕴含SearchStrategy[B] < SearchStrategy[A],这让策略在参数与返回值位置可以灵活替换,其语义由源码中TypeVar("Ex", covariant=True)声明保证。
按照上述规则为策略代码添加类型提示,即可让 mypy、Pyright、Pyre 等工具在编写阶段发现策略类型不匹配的问题,让属性测试代码同样享受现代静态类型检查带来的安全性与可维护性。
- 测试
- 开发工具
【免费下载链接】hypothesis
The property-based testing library for Python
相关推荐
如何快速捕获网页视频?猫抓浏览器扩展的终极指南
如何快速捕获网页视频?猫抓浏览器扩展的终极指南 猫抓(cat catch)浏览器扩展是一款专业的网页资源嗅探工具,它能自动检测并捕获网页中的视频资源,从普通MP
音视频Loguru API 参考指南:从 logger 方法到类型提示(Type Hints)的完整解读
Loguru API 参考指南:从 logger 方法到类型提示(Type Hints)的完整解读 导读 本文以 loguru 官方 API 参考文档( doc
开发工具Hello-Python类型提示:Type Hints代码规范
Hello Python类型提示:Type Hints代码规范 为什么需要Type Hints(类型提示)? 你是否曾在维护Python代码时遇到过"这个变量到
示例工程教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考