pydantic进阶用法示例详解

作者:it_miclon 时间:2022-01-31 07:00:03 

pydantic是一个Python的数据验证和转换库,它的特点是轻量、快速、可扩展、可配置。笔者常用的用于数据接口schema定义与检查。

具体的基本用法本文不再做过多的介绍,可以参考pydantic官方文档。本文主要是结合实际项目开发中遇到的问题和解题思路,介绍一些pydantic的高阶玩法。

当前现状

在项目中,pydantic的定义是在数据的出口进行规范化,从而使得下游接受方能更快地去解析和清洗这些数据。

from pydantic import BaseModel, Field

# 定义数据模型
class Project(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数据中查找和提取数据。

from jmespath import search
project_data = {
   "detail": {
       "article": {
           "title": "项目标题",
       }
   }
}

title = search("detail.article.title", project_data)
assert title == "项目标题"  # True

# 即使是path不存在,也不会异常,而是返回None
assert search("detail.article.title1", project_data) is None  # True

所以我打算做一个结合pydanticjmespath的方式来解决这个问题。


class Project(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)
   def data_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")
   def url_validator(cls, v):
       # 由于这里的v是拿到的ID,需要组合成url
       return f"https://xxxxx/{v}"

从代码中可以知道,我是在root_validator中提前做了数据的转换,将jmespath的查询结果赋值给对应的字段。

但是做完之后我越看越变扭,我为了做这个事情,先要申明所有字段,还要对这些字段一一映射。

于是,我想到了pydanticConfig类,它可以用来配置pydantic的一些行为。而且通过查看源码,我认为我可以通过Field类中输入一个path变量,告诉未来的处理器,这个path是用来做数据提取的。

class Project(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允许我们自定义数据的取值来源。

class ProjectGetter(GetterDict):

def get(self, key: str, default: Any) -> Any:  # noqa
       # 由于getter_dict所能拿到的“数据权限”相对较低,
       # 也就是它的权限仅仅是处理数据,而不是处理模型,
       # 所以我们需要自己去拿到模型,然后再去拿到path
       model, data = self._obj['model'], self._obj['data']
       for name, field in model.__fields__.items():
           path = field.field_info.extra.get('path')
           if path and name == key:
               return search(path, data)
       return default

class Project(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")
   def url_validator(cls, v):
       return f"https://www.baidu.com/{v}"

class Config:
       # 通过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或者是在数据进入前做一堆的数据转换。

来源:https://juejin.cn/post/7210028235623055415

标签:pydantic,进阶,用法
0
投稿

猜你喜欢

  • Oracle REGEXP_LIKE模糊查询用法例子

    2023-07-19 08:26:22
  • PHP实现sha-256哈希算法实例代码

    2023-05-25 01:05:23
  • php使用ZipArchive函数实现文件的压缩与解压缩

    2023-07-12 20:58:19
  • Oracle 下的开发日积月累

    2009-02-28 11:08:00
  • python 按不同维度求和,最值,均值的实例

    2023-06-12 15:08:23
  • margin双倍边距问题:ie和firefox显示

    2008-09-07 15:05:00
  • 变态输入框——再谈校验包容性(一)

    2009-10-10 13:23:00
  • 如何防止Application对象在多线程访问中出现错误?

    2009-11-22 19:18:00
  • 好用的JS图片预加载类

    2007-08-13 13:49:00
  • python异常的传递知识点总结

    2023-07-12 19:19:58
  • Python操作MongoDB数据库PyMongo库使用方法

    2023-03-16 00:13:14
  • 异步加载Google Adsense 更新到Wordpress 2.62

    2008-09-11 13:09:00
  • Python3 pandas 操作列表实例详解

    2021-11-30 14:24:12
  • React开发进阶redux saga使用原理详解

    2023-08-06 09:08:33
  • pytorch实现好莱坞明星识别的示例代码

    2022-11-28 14:26:47
  • python实现五子棋游戏

    2021-09-24 13:52:37
  • python tkinter实现弹窗的输入输出

    2021-10-03 14:58:42
  • opencv+mediapipe实现人脸检测及摄像头实时示例

    2022-08-11 17:58:44
  • 个人网站与动网整合非官方方法

    2009-07-05 18:42:00
  • 提高javascript的速度系列(序)

    2008-07-15 12:38:00
  • asp之家 网络编程 m.aspxhome.com