英文:
Is there a Pydantic convention for a Model attribute that will default to a function of another attribute?
问题
我想定义一个具有以下属性的Pydantic BaseModel:
- 两个
int属性a和b。 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的保证。对于实际构造的任何A,b 永远不会是 None,因此它的类型应该是 int,而不是 Optional[int]。
英文:
I want to define a Pydantic BaseModel with the following properties:
- Two
intattributesaandb. ais a required attributebis optional, and will default toa+1if 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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。


评论