You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何配置hypothesis.strategies为可选字段始终生成非None值

如何配置hypothesis.strategies为可选字段始终生成非None值

我完全懂你的需求——用Hypothesis生成随机实例,但烦可选字段时不时冒出的None,而且还不想因为类结构变动去维护硬编码的实例化逻辑对吧?

先理下你的场景:你在用别人维护的dataclass结构,比如:

from dataclasses import dataclass
@dataclass
class Child:
    f1: int
    f2: int | None
@dataclass
class Parent:
    child: Child

实际的类结构比这复杂得多,层级更深、字段更多。你自己写了个生成函数用Hypothesis造实例,但可选字段偶尔会出None,你希望这些可选字段永远是有效类型值(比如Child.f2必须是int)。

你的当前实现是这样的:

from typing import TypeVar
import hypothesis
import hypothesis.strategies
T = TypeVar("T")
def generate(cls: type[T], seed: int) -> T:
    objects = []

    @hypothesis.seed(seed)
    @hypothesis.given(hypothesis.strategies.from_type(cls))
    @hypothesis.settings(max_examples=10)
    def f(o):
        objects.append(o)

    f()

    # 第一个例子太简单,返回最后一个
    return objects[-1]

print(generate(Parent, 123))
print(generate(Parent, 456))

输出里会出现f2=None的情况,这正是你要避免的。

解决方案:自定义策略,替换可选类型的默认行为

这里有两种靠谱的方式,都不用硬编码字段,能自动适配类结构变动:

方式1:针对Dataclass的字段递归处理(推荐,无全局副作用)

直接解析dataclass的字段,对X | None这种可选类型,强制生成X类型的随机值,递归处理嵌套结构:

from typing import TypeVar, get_origin, get_args
import hypothesis
import hypothesis.strategies as st
T = TypeVar("T")

def generate(cls: type[T], seed: int) -> T:
    objects = []

    # 自定义复合策略:递归生成实例,跳过可选类型的None
    @st.composite
    def non_none_optional_generator(draw, target_cls):
        # 处理dataclass类型
        if hasattr(target_cls, "__dataclass_fields__"):
            field_values = {}
            for field_name, field in target_cls.__dataclass_fields__.items():
                field_type = field.type
                origin = get_origin(field_type)
                # 判断是否是X | None的可选类型
                if origin is Union and len(get_args(field_type)) == 2 and get_args(field_type)[1] is type(None):
                    # 只生成X类型的随机值,跳过None
                    field_values[field_name] = draw(st.from_type(get_args(field_type)[0]))
                else:
                    # 递归处理嵌套的类(比如Parent里的Child)
                    field_values[field_name] = draw(non_none_optional_generator(field_type))
            return target_cls(**field_values)
        else:
            # 非dataclass类型用默认策略生成
            return draw(st.from_type(target_cls))

    @hypothesis.seed(seed)
    @hypothesis.given(non_none_optional_generator(cls))
    @hypothesis.settings(max_examples=10)
    def f(o):
        objects.append(o)

    f()
    return objects[-1]

# 测试输出,f2永远是int不会为None
print(generate(Parent, 123))
print(generate(Parent, 456))

方式2:全局临时替换可选类型的策略(代码更简洁,注意副作用)

如果你的场景里不会同时用其他Hypothesis策略,可以临时注册全局的可选类型策略,用完再恢复:

from typing import TypeVar, Union, Optional, get_args
import hypothesis
import hypothesis.strategies as st
T = TypeVar("T")

def generate(cls: type[T], seed: int) -> T:
    objects = []
    original_optional_strategy = None

    try:
        # 保存原来的可选类型策略,用完恢复
        original_optional_strategy = st.type_strategies.get(Optional)
        
        # 自定义可选类型策略:只生成X类型,不生成None
        @st.cacheable
        def override_optional_strategy(target_type):
            x_type = get_args(target_type)[0]
            return st.from_type(x_type)
        
        # 注册新的策略
        st.register_type_strategy(Optional, override_optional_strategy)

        @hypothesis.seed(seed)
        @hypothesis.given(st.from_type(cls))
        @hypothesis.settings(max_examples=10)
        def f(o):
            objects.append(o)

        f()
    finally:
        # 恢复原来的策略,避免影响其他代码
        if original_optional_strategy:
            st.type_strategies[Optional] = original_optional_strategy
        else:
            st.type_strategies.pop(Optional, None)

    return objects[-1]

# 测试输出,所有可选字段都不会有None
print(generate(Parent, 123))
print(generate(Parent, 456))

效果验证

两种方式生成的实例里,Child.f2都会是随机int,再也不会出现None了,而且不管类结构怎么变(加字段、改嵌套),只要是X | None的可选类型,都会自动适配。

备注:内容来源于stack exchange,提问作者obk

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.14 13:54:32