python 是否可以在pydantic中更改输出别名?

dy1byipe  于 2022-10-30  发布在  Python
关注(0)|答案(4)|浏览(334)

设定:


# Pydantic Models

class TMDB_Category(BaseModel):
    name: str = Field(alias="strCategory")
    description: str = Field(alias="strCategoryDescription")

class TMDB_GetCategoriesResponse(BaseModel):
    categories: list[TMDB_Category]

@router.get(path="category", response_model=TMDB_GetCategoriesResponse)
async def get_all_categories():
    async with httpx.AsyncClient() as client:
        response = await client.get(Endpoint.GET_CATEGORIES)
        return TMDB_GetCategoriesResponse.parse_obj(response.json())

问题:

创建响应时使用了别名,我希望避免使用别名。我只需要此别名来正确Map传入数据,但在返回响应时,我希望使用实际的字段名称。

实际响应:

{
  "categories": [
    {
      "strCategory": "Beef",
      "strCategoryDescription": "Beef is ..."
    },
    {
      "strCategory": "Chicken",
      "strCategoryDescription": "Chicken is ..."
    }
}

预期响应:

{
  "categories": [
    {
      "name": "Beef",
      "description": "Beef is ..."
    },
    {
      "name": "Chicken",
      "description": "Chicken is ..."
    }
}
nukf8bse

nukf8bse1#

切换别名和字段名称,并使用allow_population_by_field_namemodel config选项:

class TMDB_Category(BaseModel):
    strCategory: str = Field(alias="name")
    strCategoryDescription: str = Field(alias="description")

    class Config:
        allow_population_by_field_name = True

让别名配置您要返回的字段的名称,但要使allow_population_by_field_name能够解析使用不同字段名称的数据。

mkshixfv

mkshixfv2#

另一个选项(可能不会 * 那么流行 *)是使用一个反序列化库而不是pydantic。例如,Dataclass Wizard库就是一个支持这个特定用例的库。如果您需要Field(alias=...)提供的相同的往返行为,您可以将all参数传递给json_field函数。注意,使用这样的库,您确实失去了执行完整类型验证的能力,而这可以说是Pydantic最大的优势之一;但是它会以类似于pydantic的方式执行类型转换。还有一些原因让我觉得验证并不那么重要,我在下面列出了这些原因。

我认为数据验证是一个 * 很好拥有 * 的特性的原因:

  • 如果您自己构建和传递输入,则您很可能相信自己知道自己在做什么,并且传递了正确的数据类型。
  • 如果你从另一个API获取输入,那么假设这个API有很好的文档,你可以从他们的文档中抓取一个示例响应,并使用它来建模你的类结构。如果一个API清楚地记录了它的响应结构,你通常不需要任何验证。
  • 数据验证需要时间,因此与只执行类型转换并捕获可能发生的任何错误而不事先验证输入类型相比,它可能会稍微减慢该过程。

为了演示这一点,下面是使用dataclass-wizard库(依赖于dataclasses的使用,而不是pydantic模型)的上述用例的一个简单示例:

from dataclasses import dataclass

from dataclass_wizard import JSONWizard, json_field

@dataclass
class TMDB_Category:
    name: str = json_field('strCategory')
    description: str = json_field('strCategoryDescription')

@dataclass
class TMDB_GetCategoriesResponse(JSONWizard):
    categories: list[TMDB_Category]

运行它的代码如下所示:

input_dict = {
  "categories": [
    {
      "strCategory": "Beef",
      "strCategoryDescription": "Beef is ..."
    },
    {
      "strCategory": "Chicken",
      "strCategoryDescription": "Chicken is ..."
    }
  ]
}

c = TMDB_GetCategoriesResponse.from_dict(input_dict)
print(repr(c))

# TMDB_GetCategoriesResponse(categories=[TMDB_Category(name='Beef', description='Beef is ...'), TMDB_Category(name='Chicken', description='Chicken is ...')])

print(c.to_dict())

# {'categories': [{'name': 'Beef', 'description': 'Beef is ...'}, {'name': 'Chicken', 'description': 'Chicken is ...'}]}
测量性能

如果有人好奇的话,我已经建立了一个快速基准测试,来比较pydantic和dataclasses的反序列化和序列化时间:

from dataclasses import dataclass
from timeit import timeit

from pydantic import BaseModel, Field

from dataclass_wizard import JSONWizard, json_field

# Pydantic Models

class Pydantic_TMDB_Category(BaseModel):
    name: str = Field(alias="strCategory")
    description: str = Field(alias="strCategoryDescription")

class Pydantic_TMDB_GetCategoriesResponse(BaseModel):
    categories: list[Pydantic_TMDB_Category]

# Dataclasses

@dataclass
class TMDB_Category:
    name: str = json_field('strCategory', all=True)
    description: str = json_field('strCategoryDescription', all=True)

@dataclass
class TMDB_GetCategoriesResponse(JSONWizard):
    categories: list[TMDB_Category]

# Input dict which contains sufficient data for testing (100 categories)

input_dict = {
  "categories": [
    {
      "strCategory": f"Beef {i * 2}",
      "strCategoryDescription": "Beef is ..." * i
    }
    for i in range(100)
  ]
}

n = 10_000

print('=== LOAD (deserialize)')
print('dataclass-wizard: ',
      timeit('c = TMDB_GetCategoriesResponse.from_dict(input_dict)',
             globals=globals(), number=n))
print('pydantic:         ',
      timeit('c = Pydantic_TMDB_GetCategoriesResponse.parse_obj(input_dict)',
             globals=globals(), number=n))

c = TMDB_GetCategoriesResponse.from_dict(input_dict)
pydantic_c = Pydantic_TMDB_GetCategoriesResponse.parse_obj(input_dict)

print('=== DUMP (serialize)')
print('dataclass-wizard: ',
      timeit('c.to_dict()',
             globals=globals(), number=n))
print('pydantic:         ',
      timeit('pydantic_c.dict()',
             globals=globals(), number=n))

性能指标评测结果(在Mac OS Big Sur、Python 3.9.0上测试):

=== LOAD (deserialize)
dataclass-wizard:  1.742989194
pydantic:          5.31538175
=== DUMP (serialize)
dataclass-wizard:  2.300118940
pydantic:          5.582638598

在他们的文档中,pydantic声称自己是最快的库,但是要证明它不是最快的库是相当简单的。正如你所看到的,对于上面的数据集,pydantic在反序列化和序列化过程中大约慢2倍。值得注意的是,pydantic已经相当快了。

mw3dktmi

mw3dktmi3#

也许你可以用这种方法

from pydantic import BaseModel, Field

class TMDB_Category(BaseModel):
    name: str = Field(alias="strCategory")
    description: str = Field(alias="strCategoryDescription")

data = {
    "strCategory": "Beef",
    "strCategoryDescription": "Beef is ..."
}

obj = TMDB_Category.parse_obj(data)

# {'name': 'Beef', 'description': 'Beef is ...'}

print(obj.dict())
ogq8wdun

ogq8wdun4#

我尝试做一些类似的事情(将字段pattern迁移到patterns列表中,同时优雅地处理旧版本的数据)。我能找到的最好的解决方案是在__init__方法中进行字段Map。用OP的术语来说,这将是这样的:

class TMDB_Category(BaseModel):
    name: str
    description: str
    def __init__(self,**data):
        if "strCategory" in data:
            data["name"] = data.pop("strCategory")
        if "strCategoryDescription" in data:
            data["description"] = data.pop("strCategoryDescription")
        super().__init__(**data)

然后我们有:

>>> TMDB_Category(strCategory="name", strCategoryDescription="description").json()
'{"name": "name", "description": "description"}'

如果需要使用字段别名来实现这一点,但仍然在代码中使用名称/描述字段,一个选择是修改Hernán Alarcón的解决方案以使用属性:

class TMDB_Category(BaseModel):
    strCategory: str = Field(alias="name")
    strCategoryDescription: str = Field(alias="description")
    class Config:
        allow_population_by_field_name = True
    @property
    def name(self):
        return self.strCategory
    @name.setter
    def name(self, value):
        self.strCategory = value
    @property
    def description(self):
        return self.strCategoryDescription
    @description.setter
    def description(self, value):
        self.strCategoryDescription = value

这仍然有点尴尬,因为repr使用了“别名”:

>>> TMDB_Category(name="name", description="description")
TMDB_Category(strCategory='name', strCategoryDescription='description')

相关问题