为什么 FastAPI 从路由函数返回时会执行 Pydantic 构造函数两次?

Rya*_*oss 8 python pydantic fastapi

我有一个用例,我想在构建模型的类中创建一些值。但是,当我在调用 API 时将类返回到 FastAPI 以便转换为 JSON 时,构造函数会再次运行,并且我可以获得与原始实例不同的值。

这是一个人为的例子来说明:

class SomeModel(BaseModel):
    public_value: str
    secret_value: Optional[str]

    def __init__(self, **data):
        super().__init__(**data)
        # this could also be done with default_factory
        self.secret_value = randint(1, 5)


def some_function() -> SomeModel:
    something = SomeModel(public_value="hello")
    print(something)
    return something


@app.get("/test", response_model=SomeModel)
async def exec_test():
    something = some_function()
    print(something)
    return something
Run Code Online (Sandbox Code Playgroud)

控制台输出为:

public_value='hello' secret_value=1
public_value='hello' secret_value=1
Run Code Online (Sandbox Code Playgroud)

但 Web API 中的 JSON 是:

{
  "public_value": "hello",
  "secret_value": 2
}
Run Code Online (Sandbox Code Playgroud)

当我单步执行代码时,我可以看到__init__被调用两次。

首先是在建设上something = SomeModel(public_value="hello")

其次,令我意想不到的是,在调用的 API 处理程序exec_testreturn something

如果这是在类中设置某些内部数据的错误方法,请告诉我正确的使用方法。否则,这似乎是其中一个模块的意外行为。

Gin*_*pin 8

当您使用 时,这应该是预期的行为response_model。文档中没有很清楚地解释,但在响应模型部分中,它说:

FastAPI 将使用它response_model来:

  • 将输出数据转换为其类型声明。
  • 验证数据。
    ...

当您return something到达路由函数末尾时exec_test,FastAPI 会将其转换为另一个 SomeModel实例,进行验证,然后返回该经过验证的实例。因此,您得到的实例与最初返回的实例不同。

我以前也遇到过同样的问题:为什么response_model 似乎__init__两次出现在同一个对象上?,这导致了这个老问题:[BUG]使用response_model时双重验证pydantic模型。大多数回复都是“这是预料之中的”:

这是预期的事情,也是 的重点ResponseModel,它确保您的数据处于正确的顺序。

它并不完全重复。在内部它会验证一次(如果您添加response_model),但在端点函数上您将再次手动验证。

我相信答案是:这是预期的结果。

我后来学到的解决方案是永远不要像那样改变模型__init__我也认为alex_noname使用或 验证器提供的答案Field是避免此问题的最佳方法。

如果您确实需要该突变,以下是其他解决方法__init__

  1. 只需跳过对路线功能的验证

    some_function实例化的地方,如果参数错误,SomeModel就会引发public_value={'a': 1})验证错误(例如,重复验证response_model可能是多余的。甚至在 FastAPI 上有一个PR 可以跳过验证response_model,但从未合并。

    您只需删除response_model并将其替换为responses即可使用 OpenAPI 维护文档。

    # @app.get("/test", response_model=SomeModel)
    @app.get("/test", responses={200: {"model": SomeModel}})
    async def exec_test():
        something = some_function()
        print(something)
        return something
    
    Run Code Online (Sandbox Code Playgroud)
  2. 让我们some_function返回模型的原始值,而不是模型本身的实例。基本上,将模型的实例化和验证移动/延迟到路由函数上。

    def some_function() -> dict:
        # something = SomeModel(public_value="hello")
        something = {"public_value": "hello"}
        print(something)
        return something
    
    @app.get("/test", response_model=SomeModel)
    async def exec_test():
        something = some_function()
        print(something)
        return something
    
    Run Code Online (Sandbox Code Playgroud)

    正如文档所说,FastAPI 会将返回值转换为response_model类型,从而实例化模型。在这里这样做意味着验证将很晚发生。此外,您还必须面对失去在任何地方使用 Pydantic 模型的便利性的问题。

  3. 与 #2 相关,有 2 个独立的模型,1 个用于内部使用,1 个用于放入response_model. 这类似于FastAPI 文档中的单独输入和输出模型示例:

    class InternalModel(BaseModel):
        public_value: str
    
    class OutputModel(BaseModel):
        public_value: str
        secret_value: Optional[str]
    
        def __init__(self, **data):
            super().__init__(**data)
            self.secret_value = randint(1, 5)
    
    def some_function() -> InternalModel:
        something = InternalModel(public_value="hello")
        print(something)
        return something
    
    @app.get("/test", response_model=OutputModel)
    async def exec_test():
        something = some_function()
        print(something)
        return something
    
    Run Code Online (Sandbox Code Playgroud)


ale*_*ame 3

由于您正在使用response_model路径操作,因此您的返回值将根据它进行验证。但由于您返回的是已验证的模型实例,因此这种情况会发生两次。如果您不使用可以__init__为模型的每个实例化生成不同值(无论输入值如何)的变异方法,则不会引人注意。

\n

我认为最好的解决方案是使用该default_factory函数,因为在这种情况下,动态值 \xe2\x80\x8b\xe2\x80\x8b 仅在代码中实例化对象时生成,并且现成的返回值将在模型验证过程中使用。

\n
class SomeModel(BaseModel):\n    public_value: str\n    secret_value: int = Field(default_factory=lambda: randint(1, 5))\n
Run Code Online (Sandbox Code Playgroud)\n

如果由于某种原因您不想使用上面的方法,那么您可以使用始终验证器,这将允许您检查值是否已传递或是否需要创建:

\n
class SomeModel(BaseModel):\n    public_value: str\n    secret_value: int = None\n\n    @validator(\'secret_value\', pre=True, always=True)\n    def secret_validator(cls, v):\n        return randint(1, 5) if v is None else v\n
Run Code Online (Sandbox Code Playgroud)\n