pydantic是一个Python的数据验证和转化库,它的特点是轻量、快速、可扩展、可装备。笔者常用的用于数据接口schema界说与查看。
具体的基本用法本文不再做过多的介绍,能够参考pydantic官方文档。本文主要是结合实际项目开发中遇到的问题和解题思路,介绍一些pydantic的高阶玩法。
当时现状
在项目中,pydantic的界说是在数据的出口进行规范化,从而使得下流接受方能更快地去解析和清洗这些数据。
frompydanticimportBaseModel,Field
#界说数据模型
classProject(BaseModel):
url:str=Field(...)
title:str=Field(...)
content:str=Field(...)
company:List[Dict]=Field(default=[])
industry:str=Field(...)
以上是简略的一个数据模型界说,代码仅为示例,隐去了一些字段和装备。也就是咱们必须传输给Project模型对应的数据才能够经过它的数据校验,否则就无法继续向下(可能是发往下流)
这么做一直以来没什么问题,直到本次项目中的接口回来呈现了大更新,使得之前的一切代码层做的数据字段映射必须重新对应匹配。
比如之前title字段对应的是title,现在变成了detail–article–title。
这使得咱们必须在代码层做诸如:
#project_data均为接口回来的数据,加数据演示
#之前的代码
project_data={
"url":"https://www.baidu.com",
"title":"百度一下,你就知道",
}
project=Project(
**project_data
)
#现在的代码
project_data={
"detail":{
"url":"xxx"
"article":{
"title":"项目标题",
}
}
}
project=Project(
url=project_data["detail"]["url"],
title=project_data["detail"]["article"]["title"],
)
以上代码取值变得杂乱,这还没考虑到数据可能存在犯错的问题,比如detail字段不存在,这样就会导致KeyError反常。
并且这并不是夸张的举例(因为现实状况更杂乱)。
我怎么能容忍这种状况呢?
处理方案
我当然不是想摒弃掉pydantic,而是想找到一种结合它更优雅的方式来处理这个问题。
所以我第一时间想到了jmespath模块,因为它是一个JSON查询语言,能够用来在JSON数据中查找和提取数据。
fromjmespathimportsearch
project_data={
"detail":{
"article":{
"title":"项目标题",
}
}
}
title=search("detail.article.title",project_data)
asserttitle=="项目标题"#True
#即使是path不存在,也不会反常,而是回来None
assertsearch("detail.article.title1",project_data)isNone#True
所以我计划做一个结合pydantic和jmespath的方式来处理这个问题。
classProject(BaseModel):
url:str=Field(...)
title:str=Field(...)
content:str=Field(...)
company:List[Dict]=Field(default=[])
industry:str=Field(...)
@root_validator(pre=True,skip_on_failure=True)
defdata_converter(cls,v):
return{
"url":search("detail.id",v),
"title":search("detail.article.title",v),
"content":search("detail.article.content",v),
"company":search("company[*].name",v),
"industry":search("industry",v)
}
@validator("url")
defurl_validator(cls,v):
#因为这儿的v是拿到的ID,需求组合成url
returnf"https://xxxxx/{v}"
从代码中能够知道,我是在root_validator中提早做了数据的转化,将jmespath的查询成果赋值给对应的字段。
但是做完之后我越看越变扭,我为了做这个工作,先要声明一切字段,还要对这些字段逐个映射。
所以,我想到了pydantic的Config类,它能够用来装备pydantic的一些行为。并且经过查看源码,我以为我能够经过Field类中输入一个path变量,告知未来的处理器,这个path是用来做数据提取的。
classProject(BaseModel):
url:str=Field(...,path="temporaryLibrary.id")
company_names:str=Field(...,path="company[0].enterprise.name")
versions:List[str]=Field(...,path="versionList[*].id")
当然现在代码是没有任何含义的,因为path是咱们自界说的,pydantic并不知道如何处理它。
所以下一步咱们要做的是,如何更好的让pydantic知道如何处理path。
在屡次翻阅它源代码,并结合官方文档中对Model类的介绍,我找到了一个可行的方案。
Pydantic models can be created from arbitrary class instances to support models that map to ORM objects.
也就是说,我能够将原始数据经过from_orm传递给pydantic的模型,然后经过Data binding的方式,将数据绑定到模型中。Data binding答应咱们自界说数据的取值来历。
classProjectGetter(GetterDict):
defget(self,key:str,default:Any)->Any:#noqa
#因为getter_dict所能拿到的“数据权限”相对较低,
#也就是它的权限仅仅是处理数据,而不是处理模型,
#所以咱们需求自己去拿到模型,然后再去拿到path
model,data=self._obj['model'],self._obj['data']
forname,fieldinmodel.__fields__.items():
path=field.field_info.extra.get('path')
ifpathandname==key:
returnsearch(path,data)
returndefault
classProject(BaseModel):
url:str=Field(...,path="detail.id")
company_names:str=Field(...,path="company[0].enterprise.name")
versions:List[str]=Field(...,path="versionList[*].id")
@validator("url")
defurl_validator(cls,v):
returnf"https://www.baidu.com/{v}"
classConfig:
#经过orm_mode指定数据的来历
orm_mode=True
#经过getter_dict指定数据的获取方式
getter_dict=ProjectGetter
project_data={
"detail":{
"id":1,
"article":{
"title":"项目标题",
}
},
"company":[
{
"enterprise":{
"name":"企业名称1"
}
},
{
"enterprise":{
"name":"企业名称2"
}
}
],
"versionList":[{"id":"1.0"},{"id":"2.0"}]
}
project=Project.from_orm({"model":Project,"data":project_data})
print(project)
#url='https://www.baidu.com/1'company_names='企业名称1'versions=['1.0','2.0']
这样咱们在业务端,只需求对Field指定其对应数据提取的path,而不需求再去写一堆的validator或者是在数据进入前做一堆的数据转化。
总结
经过这个小例子,咱们能够看到,pydantic的灵活性是十分强的,它能够经过Config类来装备一些行为,并且它的Field类也能够经过extra参数来传递一些额外的信息。这大大的提高了咱们的对数据的处理才能。笔者也会在后续的文章中,继续分享pydantic的一些运用技巧。
