更新多个一起验证的Pydantic字段

4

如何更新一个pydantic模型上的多个属性,这些属性一起进行验证并相互依赖?

以下是一个虚构但简单的示例:

from pydantic import BaseModel, root_validator

class Example(BaseModel):
    a: int
    b: int

    @root_validator
    def test(cls, values):
        if values['a'] != values['b']:
            raise ValueError('a and b must be equal')  
        return values

    class Config:
        validate_assignment = True

example = Example(a=1, b=1)

example.a = 2 # <-- error raised here because a is 2 and b is still 1
example.b = 2 # <-- don't get a chance to do this

错误:

ValidationError: 1 validation error for Example
__root__
  a and b must be equal (type=value_error)

值得注意的是,ab都为2是有效的,但是如果单独更新它们中的一个,就会触发验证错误。

有没有办法暂停验证,直到两个值都设置好?或者有没有办法同时更新它们?谢谢!

2个回答

2

我找到了几个解决方案,适用于我的使用情况。

  1. 手动触发验证,然后直接更新 pydantic 实例的 __dict__(如果通过)-- 参见 update 方法
  2. 一个上下文管理器,延迟验证直到上下文退出 -- 参见 delay_validation 方法
from pydantic import BaseModel, root_validator
from contextlib import contextmanager
import copy

class Example(BaseModel):
    a: int
    b: int

    @root_validator
    def enforce_equal(cls, values):
        if values['a'] != values['b']:
            raise ValueError('a and b must be equal')  
        return values

    class Config:
        validate_assignment = True

    def update(self, **kwargs):
        self.__class__.validate(self.__dict__ | kwargs)
        self.__dict__.update(kwargs)

    @contextmanager
    def delay_validation(self):
        original_dict = copy.deepcopy(self.__dict__)

        self.__config__.validate_assignment = False
        try:
            yield
        finally:
            self.__config__.validate_assignment = True
        
        try:
            self.__class__.validate(self.__dict__)
        except:
            self.__dict__.update(original_dict)
            raise

example = Example(a=1, b=1)

# ================== This didn't work: ===================

# example.a = 2 # <-- error raised here because a is 2 and b is still 1
# example.b = 2 # <-- don't get a chance to do this

# ==================== update method: ====================

# No error raised
example.update(a=2, b=2) 

# Error raised as expected - a and b must be equal
example.update(a=3, b=4) 

# Error raised as expected - a and b must be equal
example.update(a=5) 

# # =============== delay validation method: ===============

# No error raised
with example.delay_validation():
    example.a = 2
    example.b = 2

# Error raised as expected - a and b must be equal
with example.delay_validation():
    example.a = 3
    example.b = 4

# Error raised as expected - a and b must be equal
with example.delay_validation():
    example.a = 5

1
看到你在另一个帖子中回复了我的评论。是的,这是我见过的最优雅的解决方案。遗憾的是,pydantic没有更好的解决此问题的方法。对于任何寻找另一个示例场景的人:有2个日期时间字段(开始、停止),并有一个根验证器来强制执行开始<=停止。 - Taylor Vance

1
你可以通过编写setter方法来实现一个解决方案。
from pydantic import BaseModel, root_validator


class Example(BaseModel):
    a: int
    b: int


    @root_validator
    def test(cls, values):
        if values['a'] != values['b']:
            raise ValueError('a and b must be equal')
        return values

    class Config:
        validate_assignment = True

    def set_a_and_b(self, value):
        self.Config.validate_assignment = False
        self.a, self.b = value, value
        self.Config.validate_assignment = True

PoC:

>>> example = Example(a=1, b=1)
>>> example.a = 2
Traceback (most recent call last):
  File "D:\temp\venv\lib\site-packages\IPython\core\interactiveshell.py", line 3398, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-4-950b5db07c46>", line 1, in <cell line: 1>
    example.a =2
  File "pydantic\main.py", line 393, in pydantic.main.BaseModel.__setattr__
pydantic.error_wrappers.ValidationError: 1 validation error for Example
__root__
  a and b must be equal (type=value_error)

>>> example.set_a_and_b(2) # <========= workaround 
>>> example
Example(a=2, b=2)
>>> example.a = 3
Traceback (most recent call last):
  File "D:\temp\venv\lib\site-packages\IPython\core\interactiveshell.py", line 3398, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-8-d93e8eb8a0e3>", line 1, in <cell line: 1>
    example.a = 3
  File "pydantic\main.py", line 393, in pydantic.main.BaseModel.__setattr__
pydantic.error_wrappers.ValidationError: 1 validation error for Example
__root__
  a and b must be equal (type=value_error)

但也许在您的实际情况中,您应该使用一些setter和getter方法来代替(或与)标准验证一起使用。


感谢您的建议 @JacekK!暂时覆盖 validate_assignment 属性是个好主意。对于在更新属性后仍然运行验证的想法有何想法?在编写始终通过验证的简单 setter 更加困难的情况下,这可能很有用。希望有一种方法可以在不知道验证检查的情况下更改模型上的属性。然后在属性更新后运行验证。这听起来有意义吗? - Zachary Duvall
@ZacharyDuvall 我不认为完全无法忽略验证,但在类似情况下,我想出的解决方案是在setter中实现根验证。因此,在这种情况下,在重新启用validate_assignment之前,您需要进行检查if a!= b:raise ValueError('a和b必须相等') - Benjamin
1
这在pydantic版本2.4.2上不起作用。我不得不这样做: self.model_config['validate_assignment'] = False - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接