python 我可以覆盖Pydantic父模型中的字段,使它们成为可选的吗?

ma8fv8wu  于 2023-05-27  发布在  Python
关注(0)|答案(7)|浏览(249)

我有两个这样的pydantic类。

class Parent(BaseModel):
    id: int
    name: str
    email: str

class ParentUpdate(BaseModel):
    id: Optional[int]
    name: Optional[str]
    email: Optional[str]

这两个类实际上是相同的,但Parent类使所有字段都是必需的。我想在FastAPI中使用Parent类作为POST请求体,因此所有字段都应该是必需的。但是我想使用后者作为PUT请求体,因为用户可以设置选择性字段,其余字段保持不变。我已经看了一下必填的可选字段,但它们与我想做的不对应。
如果有一种方法,我可以继承ParentUpdate中的Parent类,并修改Parent中的所有字段,使它们成为Optional,这将减少混乱。另外,在Parent类中有一些验证器,我必须在ParentUpdate类中重写,这也是我想避免的。
有什么办法可以做到这一点吗?

vql8enpb

vql8enpb1#

可以在子类中将可选字段设为必填字段,但不能在子类中将必填字段设为可选字段。在fastapi作者tiangolo的样板项目中,他在示例中使用了这样的模式:

class ParentBase(BaseModel):
    """Shared properties."""
    name: str
    email: str

class ParentCreate(ParentBase):
    """Properties to receive on item creation."""
    # dont need id here if your db autocreates it
    pass

class ParentUpdate(ParentBase):
    """Properties to receive on item update."""
    # dont need id as you are likely PUTing to /parents/{id}
    # other fields should not be optional in a PUT
    # maybe what you are wanting is a PATCH schema?
    pass

class ParentInDBBase(ParentBase):
    """Properties shared by models stored in DB - !exposed in create/update."""
    # primary key exists in db, but not in base/create/update
    id: int                             

class Parent(ParentInDBBase):
    """Properties to return to client."""
    # optionally include things like relationships returned to consumer
    # related_things: List[Thing]
    pass

class ParentInDB(ParentInDBBase):
    """Additional properties stored in DB."""
    # could be secure things like passwords?
    pass

是的,我同意这是令人难以置信的冗长,我希望它不是。您仍然可能会在UI中使用更特定于特定表单的其他模式。显然,您可以删除其中的一些,因为它们在本例中是不必要的,但根据数据库中的其他字段,它们可能是需要的,或者您可能需要设置默认值、验证等。
根据我对验证器的经验,你必须重新声明它们,但你可以使用一个共享函数,即:

def clean_article_url(cls, v):
    return clean_context_url(v.strip())

class MyModel(BaseModel):
    article_url: str

    _clean_url = pydantic.validator("article_url", allow_reuse=True)(clean_article_url)
zazmityj

zazmityj2#

覆盖字段是可能且容易的。(有人提到不可能将必填字段覆盖为可选字段,但我不同意)。
这个例子没有任何问题:

class Parent(BaseModel):
    id: int
    name: str
    email: str

class ParentUpdate(Parent): ## Note that this inherits 'Parent' class (not BaseModel)
    id: Optional[int]  # this will convert id from required to optional
xmakbtuz

xmakbtuz3#

我的建议是不要发明困难的模式,我也对pydantic功能感兴趣,但所有这些功能看起来都很难看,很难理解(甚至不适合某些任务,并且有限制)。参见pydantic维护者的Python pydantic, make every field of ancestor are Optional答案

fruv7luv

fruv7luv4#

正如在回答类似问题时所概述的那样,我使用以下方法(归功于Aron Podrigal):

import inspect   
from pydantic import BaseModel   

def optional(*fields):
    """Decorator function used to modify a pydantic model's fields to all be optional.
    Alternatively, you can  also pass the field names that should be made optional as arguments
    to the decorator.
    Taken from https://github.com/samuelcolvin/pydantic/issues/1223#issuecomment-775363074
    """   
    def dec(_cls):
        for field in fields:
            _cls.__fields__[field].required = False
        return _cls

    if fields and inspect.isclass(fields[0]) and issubclass(fields[0], BaseModel):
        cls = fields[0]
        fields = cls.__fields__
        return dec(cls)

    return dec

在你的例子中,你会这样使用它:

@optional
class ParentUpdate(Parent):
    pass
kqlmhetl

kqlmhetl5#

我提前道歉,我肯定这是一个可怕的解决方案,但它对我有效:

def make_child_fields_optional(parent_class: Type[BaseModel], child_class: Type[BaseModel]):
    for key in parent_class.__fields__:
        child_class.__fields__.get(key).required = False
class BasePerson(BaseModel):
    name: str
    email: str
    login: str
class UpdatePerson(BasePerson):
    pass  # or whatever

make_child_fields_optional(BasePerson, UpdatePerson)
fdbelqdn

fdbelqdn6#

对于我的情况,创建一个新类是唯一有效的解决方案,但打包到一个函数中非常方便:

from pydantic import BaseModel, create_model
from typing import Optional

def make_optional(baseclass):
    # Extracts the fields and validators from the baseclass and make fields optional
    fields = baseclass.__fields__
    validators = {'__validators__': baseclass.__validators__}
    optional_fields = {key: (Optional[item.type_], None) for key, item in fields.items()}
    return create_model(f'{baseclass.__name__}Optional', **optional_fields, __validators__=validators)

class Parent(BaseModel):
    id: int
    name: str
    email: str

ParentUpdate = make_optional(Parent)

对比之前和之后:

Parent.__fields__

{'id': ModelField(name='id', type=int, required=True),
 'name': ModelField(name='name', type=str, required=True),
 'email': ModelField(name='email', type=str, required=True)}

ParentUpdate.__fields__

{'id': ModelField(name='id', type=Optional[int], required=False, default=None),
 'name': ModelField(name='name', type=Optional[str], required=False, default=None),
 'email': ModelField(name='email', type=Optional[str], required=False, default=None)}

它确实有效,而且如果需要的话,它还允许你过滤掉类的一些字段。
此外,对于FastApi,您可以直接使用make_optional(Parent)作为API调用中的类型提示,这将正确生成文档。这种方法的另一个优点是它可以大大减少样板文件。

g0czyy6m

g0czyy6m7#

如果你想只覆盖一些给定的字段,而不重复类型提示,你可以使用这样的装饰器来实现:

from typing import Optional

from pydantic import BaseModel

def set_fields_optional(*field_names):
    def decorator(cls: BaseModel):
        for field_name in field_names:
            field = cls.__fields__[field_name]
            field.required = False
            field.annotation = Optional[field.annotation]
            field.allow_none = True

        return cls

    return decorator

根据文档,__fields__是一个模型属性,它可以用来通过名称访问模型的字段并修改它们的属性。
应用装饰器,其中包含您希望设置为可选的字段的名称:

class BaseWithOnlyRequiredFields(BaseModel):
    x: int
    y: str
    z: float

@set_fields_optional('x', 'z')
class DerivedWithSomeOptionalFields(BaseWithOnlyRequiredFields):
    pass

DerivedWithSomeOptionalFields(y='y-value', x=None)
# DerivedWithSomeOptionalFields(x=None, y='y-value', z=None)

相关问题