Pydantic是否有一个模型属性的约定,该属性将默认为另一个属性的函数?

huangapple go评论73阅读模式
英文:

Is there a Pydantic convention for a Model attribute that will default to a function of another attribute?

问题

我想定义一个具有以下属性的Pydantic BaseModel

  • 两个int属性 ab
  • a 是一个必填属性。
  • b 是可选的,如果没有设置,将默认为 a+1

有没有办法实现这个?这是我的尝试。

from typing import Optional

from pydantic import BaseModel, validator


class A(BaseModel):
    a: int
    b: Optional[int] = None  # 希望如果没有设置默认为 a+1
    
    @validator('b', always=True)
    def default_b_value(cls, b, values):
        if b is None and 'a' in values:
            return values['a']+1
        return b

我对这个的问题是它不符合A的保证。对于实际构造的任何Ab 永远不会是 None,因此它的类型应该是 int,而不是 Optional[int]

英文:

I want to define a Pydantic BaseModel with the following properties:

  • Two int attributes a and b.
  • a is a required attribute
  • b is optional, and will default to a+1 if not set.

Is there a way to achieve this? This is what I've tried.

from typing import Optional

from pydantic import BaseModel, validator


class A(BaseModel):
    a: int
    b: Optional[int] = None  # want to default to a+1 if not set
    
    @validator('b', always=True)
    def default_b_value(cls, b, values):
        if b is None and 'a' in values:
            return values['a']+1
        return b

My problem with this is it doesn't match the guarantees of A. For any A that's actually constructed, b will never be None so its type should be int, not Optional[int].

答案1

得分: 3

I would not presume to know about conventions for this, but to achieve the behavior you want, you have a few options.

One way is to define b as being of the type int and setting a default_factory to return a sentinel value, then identity-check for that value in the validator, similar to what you did with None. I suppose the cleanest way (in the type safety sense) would be to subclass int just for that purpose to have an object that is actually distinct while still being of the correct type.

from typing import Any, cast

from pydantic import BaseModel, Field, validator


class _SentinelInt(int):
    pass


_sentinel_int = _SentinelInt()


class Foo(BaseModel):
    a: int
    b: int = Field(default_factory=lambda: _sentinel_int)

    @validator("b", always=True)
    def default_b_value(cls, v: int, values: dict[str, Any]) -> int:
        if v is _sentinel_int:
            return cast(int, values["a"] + 1)
        return v

Side note: You can be sure that the a value is present in the values dictionary because validation occurs in the order that fields are defined.

Demo:

foo1 = Foo(a=42)
foo2 = Foo(a=-1, b=69)
print(foo1.json(indent=4))
print(foo2.json(indent=4))

Output:

{
    "a": 42,
    "b": 43
}
{
    "a": -1,
    "b": 69
}

An arguably less clean way would be to simply stick with None as the default but still type b as int and simply set a specific type: ignore directive for the default assignment:

from typing import Any, cast

from pydantic import BaseModel, validator


class Foo(BaseModel):
    a: int
    b: int = None  # type: ignore[assignment]

    @validator("b", always=True)
    def default_b_value(cls, v: int, values: dict[str, Any]) -> int:
        if v is None:
            return cast(int, values["a"] + 1)
        return v

From the perspective of the type checker and for all intents and purposes at runtime the value of the b field of any instance will always be an int, so I would say this approach is well justified. Even though it may not be strictly type safe, the intent is still crystal clear.

英文:

I would not presume to know about conventions for this, but to achieve the behavior you want, you have a few options.

One way is to define b as being of the type int and setting a default_factory to return a sentinel value, then identity-check for that value in the validator, similar to what you did with None. I suppose the cleanest way (in the type safety sense) would be to subclass int just for that purpose to have an object that is actually distinct while still being of the correct type.

from typing import Any, cast

from pydantic import BaseModel, Field, validator


class _SentinelInt(int):
    pass


_sentinel_int = _SentinelInt()


class Foo(BaseModel):
    a: int
    b: int = Field(default_factory=lambda: _sentinel_int)

    @validator("b", always=True)
    def default_b_value(cls, v: int, values: dict[str, Any]) -> int:
        if v is _sentinel_int:
            return cast(int, values["a"] + 1)
        return v

Side note: You can be sure that the a value is present in the values dictionary because validation occurs in the order that fields are defined.

Demo:

foo1 = Foo(a=42)
foo2 = Foo(a=-1, b=69)
print(foo1.json(indent=4))
print(foo2.json(indent=4))

Output:

{
    "a": 42,
    "b": 43
}
{
    "a": -1,
    "b": 69
}

An arguably less clean way would be to simply stick with None as the default but still type b as int and simply set a specific type: ignore directive for the default assignment:

from typing import Any, cast

from pydantic import BaseModel, validator


class Foo(BaseModel):
    a: int
    b: int = None  # type: ignore[assignment]

    @validator("b", always=True)
    def default_b_value(cls, v: int, values: dict[str, Any]) -> int:
        if v is None:
            return cast(int, values["a"] + 1)
        return v

From the perspective of the type checker and for all intents and purposes at runtime the value of the b field of any instance will always be an int, so I would say this approach is well justified. Even though it may not be strictly type safe, the intent is still crystal clear.

huangapple
  • 本文由 发表于 2023年4月13日 21:57:15
  • 转载请务必保留本文链接:https://go.coder-hub.com/76006315.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定