【问题标题】:Why does FastAPI execute the Pydantic constructor twice when returning from the route function?为什么FastAPI从路由函数返回时会执行两次Pydantic构造函数?
【发布时间】:2021-11-04 11:28:51
【问题描述】:

我有一个用例,我想在我的类中创建一些关于模型构造的值。但是,当我在调用 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

控制台输出为:

public_value='hello' secret_value=1
public_value='hello' secret_value=1

但是 Web API 中的 JSON 是:

{
  "public_value": "hello",
  "secret_value": 2
}

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

先说建设something = SomeModel(public_value="hello")

第二个出乎我意料的是,在 API 处理程序 exec_test 中调用 return something

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

【问题讨论】:

    标签: python fastapi pydantic


    【解决方案1】:

    当您使用response_model 时,这应该是预期的行为。文档中没有完全解释清楚,但在 Response Model 部分中,它说:

    FastAPI 将使用这个response_model 来:

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

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

    我之前也遇到过同样的问题:Why does the response_model seem to __init__ the same object twice?,这导致了这个老问题:[BUG] Double validation pydantic model when used response_model。大部分回复都是“意料之中”的:

    这是意料之中的事情,也是ResponseModel 的重点,它确保您的数据顺序正确。

    它没有完全复制。它在内部验证一次(如果您添加 response_model),但在您的端点函数上,您将再次手动验证。

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

    从那以后我学到的解决方案是永远像这样改变__init__ 上的模型。我也认为使用Field 或验证器的answer provided by alex_noname 是避免此问题的最佳方法。

    如果您真的需要 __init__ 上的突变,这里是其他解决方法:

    1. 跳过对路由功能的验证

      some_function 中,您正在实例化SomeModel,如果参数错误(例如public_value={'a': 1})),这将已经引发验证错误。使用response_model 重复验证可能是多余的。甚至还有一个PR on FastAPI to skip validation on 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
      
    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
      

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

    3. 与 #2 相关,有 2 个单独的模型,1 个供内部使用,1 个用于放入 response_model。这类似于从 FastAPI 文档中获得单独的 input and output models 示例:

      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
      

    【讨论】:

      【解决方案2】:

      由于您使用response_model 进行路径操作,因此您的返回值将根据它进行验证。但由于您返回的是已验证的模型实例,因此会发生两次。如果您不使用变异的__init__ 方法,无论输入值如何,它都不会为模型的每个实例化生成不同的值。

      我认为最可取的解决方案是使用default_factory函数,因为在这种情况下,动态值只会在你的代码中的对象实例化时生成,而现成的值将是返回时在模型验证过程中使用。

      class SomeModel(BaseModel):
          public_value: str
          secret_value: int = Field(default_factory=lambda: randint(1, 5))
      

      如果由于某种原因您不想使用上述方法,那么您可以使用always validator,这将允许您检查该值是否已传递或需要创建:

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

      【讨论】:

        猜你喜欢
        • 2011-12-01
        • 1970-01-01
        • 2011-03-21
        • 1970-01-01
        • 2016-10-02
        • 2021-01-04
        • 2020-02-23
        相关资源
        最近更新 更多