如何创建兼容Pydantic的托管属性并实现值验证?
在Pydantic V2中实现带验证的托管属性
问题描述
我有一个模型,需要在每次设置值时执行一些操作,通常会使用托管属性。但我希望在设置值时仍能通过Pydantic验证新值。我尝试使用@validate_call装饰器,但它并未对无效输入抛出预期的ValidationError。示例代码如下:
from typing import Optional, TypeVar, Generic import weakref from pydantic import BaseModel, validate_call P = TypeVar("P", covariant=True) class ManagedNode(BaseModel, Generic[P]): _parent: Optional[weakref.ref] = None @property def parent(self) -> Optional[P]: return self._parent() if self._parent is not None else None @parent.setter @validate_call def parent(self, parent: P): # type: ignore self._parent = weakref.ref(parent) if parent is not None else None class A: pass class B: pass ManagedNode[A]().parent = B() # 此处未抛出预期的ValidationError
(注:使用Pydantic V2版本2.9.2,P作为协变类型用于方法参数存在单独问题,因此添加了# type: ignore)
问题原因
@validate_call装饰器无法直接对泛型类型参数进行运行时验证:一方面Python泛型的类型信息在运行时会被擦除,@validate_call无法获取ManagedNode实例绑定的泛型参数;另一方面协变类型变量用于方法参数本身不符合类型系统规则(协变类型仅适用于返回值,参数应使用逆变类型),这也导致验证逻辑失效。
可行解决方案
方案一:使用TypeAdapter手动验证
利用Pydantic的TypeAdapter工具,在setter中手动完成类型验证,同时保留泛型的类型绑定信息:
from typing import Optional, TypeVar, Generic, Type import weakref from pydantic import BaseModel, TypeAdapter, ValidationError # 改为逆变类型变量,适配参数位置的类型要求 P = TypeVar("P", contravariant=True) class ManagedNode(BaseModel, Generic[P]): _parent: Optional[weakref.ref] = None # 存储泛型类型引用,用于运行时验证 _parent_type: Type[P] def __init__(self, parent_type: Type[P], **data): super().__init__(**data) self._parent_type = parent_type @property def parent(self) -> Optional[P]: return self._parent() if self._parent is not None else None @parent.setter def parent(self, parent: Optional[P]): # 用TypeAdapter验证输入类型 adapter = TypeAdapter(self._parent_type) try: validated_parent = adapter.validate_python(parent) except ValidationError as e: raise e # 执行自定义逻辑 self._parent = weakref.ref(validated_parent) if validated_parent is not None else None class A: pass class B: pass # 创建实例时传入泛型类型 node = ManagedNode[A](parent_type=A) try: node.parent = B() except ValidationError as e: print(e) # 会抛出预期的类型验证错误
方案二:基于模型私有字段与手动类型检查
贴合Pydantic模型机制,通过私有字段存储数据,在setter中手动完成类型校验:
from typing import Optional, TypeVar, Generic import weakref from pydantic import BaseModel, ValidationError P = TypeVar("P", contravariant=True) class ManagedNode(BaseModel, Generic[P]): _parent_ref: Optional[weakref.ref] = None @property def parent(self) -> Optional[P]: return self._parent_ref() if self._parent_ref is not None else None @parent.setter def parent(self, value: Optional[P]): # 从泛型基类中提取绑定的类型参数 expected_type = self.__orig_bases__[0].__args__[0] if value is not None and not isinstance(value, expected_type): raise ValidationError([{ "loc": ("parent",), "msg": f"预期类型 {expected_type.__name__},实际得到 {type(value).__name__}", "type": "type_error" }]) self._parent_ref = weakref.ref(value) if value is not None else None class A: pass class B: pass node = ManagedNode[A]() try: node.parent = B() except ValidationError as e: print(e)
关键说明
- 泛型方差修正:协变类型变量仅适用于返回值,参数位置需使用逆变类型变量(
contravariant=True),符合LSP原则,避免类型注解冲突。 - 运行时类型信息获取:Python泛型类型参数会被擦除,需通过存储类型引用或从
__orig_bases__中提取绑定的泛型参数,才能在运行时进行类型验证。 - TypeAdapter工具:Pydantic V2的
TypeAdapter是运行时类型验证的核心工具,支持泛型类型,可准确校验输入值是否符合指定类型。
内容的提问来源于stack exchange,提问作者joudan
相关产品推荐
相关产品推荐

