Pydantic


Pydantic 是一个使用Python类型提示来进行数据验证和设置管理的库。Pydantic定义数据应该如何使用纯Python规范用并进行验证。PEP 484 从Python3.5开始引入了类型提示的功能,PEP 526 使用Python3.6中的变量注释语法对其进行了拓展。Pydantic使用这些注释来验证不受信任的数据是否采用了您想要的形式。
示例:

from datetime import datetime
from typing import List
from pydantic import BaseModel

class User(BaseModel):
    id: int
    name = 'John Doe'
    signup_ts: datetime = None
    friends: List[int] = []

external_data = {'id': '123', 'signup_ts': '2017-06-01 12:22', 'friends': [1, '2', b'3']}
user = User(**external_data)
print(user)
# > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3]
print(user.id)
# > 123

这里发生了什么:

  • id 是 int 类型;注释声明告诉pydantic该字段是必须的。如果可能,字符串、字节或浮点数将强制转换为int,否则将引发异常。
  • name 从默认值推断为其为 str 类型,该字段不是必须的,因为它有默认值。
  • signup_ts 是 datetime 类型,该字段不是必须的,默认值为 None。pydantic会将表示unix时间戳(例如1496498400)的 int 类型或表示时间和日期的字符串处理成 datetime 类型。
  • friends 使用Python的 typing 系统,该字段是必须的,并且必须是元素为整数的列表,默认值为一个空列表。

如果验证失败,pydantic会抛出一个错误,列出错误的原因:

from pydantic import ValidationError
try:
    User(signup_ts='broken', friends=[1, 2, 'not number'])
except ValidationError as e:
    print(e.json())

"""
[
  {
    "loc": [
      "id"
    ],
    "msg": "field required",
    "type": "value_error.missing"
  },
  {
    "loc": [
      "signup_ts"
    ],
    "msg": "invalid datetime format",
    "type": "type_error.datetime"
  },
  {
    "loc": [
      "friends",
      2
    ],
    "msg": "value is not a valid integer",
    "type": "type_error.integer"
  }
]
"""

基本原理

pydantic使用了一些很酷的新语言特性,但我为什么要使用它呢?

  • 不需要太高的学习成本

不需要学习新的模式定义微语言。如果您了解Python(也许略读了 类型提示文档),您就知道如何使用pydantic。

  • 与你的IDE/linter/brain配合得很好

因为pydantic数据结构只是您定义的类的实例;自动完成、linting、mypy和您的直觉都应该能够正确地处理经过验证的数据。

  • 多用途

pydantic的 BaseSettions 类允许在 验证此请求数据 上下文和 加载我的系统设置 上下文中使用它。主要区别在于,系统设置可以由环境变量更改默认值,而且通常需要更复杂的对象,如DSNs和Python对象。

  • 快速

pydantic比其他所有测试库都要快。

  • 可以验证复杂结构

使用递归pydantic模型、typing 模块的 List 和 Dict 等,并且验证器可以清晰、轻松地定义复杂的数据模式,然后进行检查。

  • 可拓展

pydantic允许定义自定义数据类型,或者您可以使用使用validator装饰器装饰的模型上的方法来扩展验证器。

安装

pip install pydantic

Pydantic除了Python3.6或Python3.7(和Python3.6中的 dataclasses 包)之外,不需要其他依赖项。

如果想让pydantic更加快速的解析JSON,你可以添加 ujson 作为可选的依赖项。类似的,pydantic 的email验证依赖于 email-validator

pip install pydantic[ujson]
# or
pip install pydantic[email]
# or just
pip install pydantic[ujson,email]

当然,你也可以使用 pip install … 手动安装这些依赖项。

用法

pydantic使用 typing 类型定义更复杂的对象:

from typing import Dict, List, Optional, Sequence, Set, Tuple, Union
from pydantic import BaseModel


class Model(BaseModel):
    simple_list: list = None
    list_of_ints: List[int] = None

    simple_tuple: tuple = None
    tuple_of_different_types: Tuple[int, float, str, bool] = None

    simple_dict: dict = None
    dict_str_float: Dict[str, float] = None

    simple_set: set = None
    set_bytes: Set[bytes] = None

    str_or_bytes: Union[str, bytes] = None
    none_or_str: Optional[str] = None

    sequence_of_ints: Sequence[int] = None
    compound: Dict[Union[str, bytes], List[Set[int]]] = None


print(Model(simple_list=['1', '2', '3']).simple_list)  # > ['1', '2', '3']
print(Model(list_of_ints=['1', '2', '3']).list_of_ints)  # > [1, 2, 3]

print(Model(simple_tuple=(1, 2, 3, 4)).simple_tuple)  # > (1, 2, 3, 4)
print(Model(tuple_of_different_types=[1, 2, 3, 4]).tuple_of_different_types)  # > (1, 2.0, '3', True)

print(Model(simple_dict={'a': 1, b'b': 2}).simple_dict)  # > {'a': 1, b'b': 2}
print(Model(dict_str_float={'a': 1, b'b': 2}).dict_str_float)  # > {'a': 1.0, 'b': 2.0}

print(Model(simple_set={1, 2, 3, 4}).simple_set)
print(Model(set_bytes={b'1', b'2', b'3', b'4'}).set_bytes)

print(Model(str_or_bytes='hello world').str_or_bytes)
print(Model(str_or_bytes=b'hello world').str_or_bytes)
print(Model(none_or_str='hello world').none_or_str)
print(Model(none_or_str=None).none_or_str)

print(Model(sequence_of_ints=[1, 2, 3, 4]).sequence_of_ints)  # > [1, 2, 3, 4]
print(Model(compound={'name': [{1, 2, 3}], b'entitlement': [{10, 5}]}).compound)

dataclasses

# 注意:v0.14 版本的新功能。

如果不希望使用pydantic的 BaseModel,则可以在标准 dataclasses 上获得相同的数据验证(在Python 3.7中引入)。

dataclasses 在Python3.6中使用 dataclasses backport package 来工作。

from datetime import datetime
from pydantic.dataclasses import dataclass

@dataclass
class User:
    id: int
    name: str = 'John Doe'
    signup_ts: datetime = None


user = User(id='42', signup_ts='2032-06-21T12:00')
print(user)
# > User(id=42, name='John Doe', signup_ts=datetime.datetime(2032, 6, 21, 12, 0))

您可以使用所有标准的pydantic字段类型,得到的数据类将与使用标准库的 dataclass 装饰器创建的数据类相同。

pydantic.dataclasses.dataclass 的参数与标准装饰器相同,除了一个与 Config 具有相同含义的额外关键字参数 config

注意:作为pydantic的 dataclasses 能够很好地处理 mypy的副作用, 配置参数将在IDE和mypy中显示为不可用。使用 @dataclass(..., config=Config) # type: ignore 作为解决方案。查看 python/mypy#6239 来了解为什么要这样。

嵌套的dataclasses

从 v0.17 版本开始,dataclasses 和正常的模型都支持嵌套的 dataclasses

from pydantic import UrlStr
from pydantic.dataclasses import dataclass

@dataclass
class NavbarButton:
    href: UrlStr

@dataclass
class Navbar:
    button: NavbarButton

navbar = Navbar(button=('https://example.com',))
print(navbar)
# > Navbar(button=NavbarButton(href='https://example.com'))

dataclasses 属性可以由元组、字典或该 dataclass 的示例来填充。

选择

Pydantic使用Python的标准 enum 类定义选择:

from enum import Enum, IntEnum

from pydantic import BaseModel


class FruitEnum(str, Enum):
    pear = 'pear'
    banana = 'banana'


class ToolEnum(IntEnum):
    spanner = 1
    wrench = 2


class CookingModel(BaseModel):
    fruit: FruitEnum = FruitEnum.pear
    tool: ToolEnum = ToolEnum.spanner


print(CookingModel())
# > CookingModel fruit=<FruitEnum.pear: 'pear'> tool=<ToolEnum.spanner: 1>
print(CookingModel(tool=2, fruit='banana'))
# > CookingModel fruit=<FruitEnum.banana: 'banana'> tool=<ToolEnum.wrench: 2>
print(CookingModel(fruit='other'))
# will raise a validation error

验证器

自定义验证和对象之间的复杂关系可以使用 validator 装饰器来获得。

validator 装饰器通过装饰类中的方法来验证字段,被装饰的方法必须是 类方法

validator 装饰器有几个可选的参数:

  • fields:要调用装饰器进行验证的字段。一个验证器可以应用于多个字段,可以通过显式指定字段名称的方式逐一指定字段,也可以通过 * 以解包的方式指定所有的字段,这意味着将为所有字段调用验证器。
  • pre:是否应该在标准验证器之前调用此验证器(否则在之后)。如果为 True,则在调用标准验证器之前调用,否则在之后调用。
  • whole:对于复杂的对象。例如 set 或者 list,是验证对象中的每个元素或者验证整个对象。如果为 True,则验证对象本身,如果为 False,则验证对象中的每个元素。
  • always:是否在值缺失的情况下仍然调这个方法和其他验证器。
  • check_fields:是否检查模型上是否存在字段。
from pydantic import BaseModel, ValidationError, validator


class UserModel(BaseModel):
    name: str
    password1: str
    password2: str

    @validator('name')
    def name_must_contain_space(cls, v):
        if ' ' not in v:
            raise ValueError('must contain a space')
        return v.title()

    @validator('password2')
    def passwords_match(cls, v, values, **kwargs):
        if 'password1' in values and v != values['password1']:
            raise ValueError('passwords do not match')
        return v


UserModel(name='samuel colvin', password1='zxcvbn', password2='zxcvbn')
# <UserModel name='Samuel Colvin' password1='zxcvbn' password2='zxcvbn'>
try:
    UserModel(name='samuel', password1='zxcvbn', password2='zxcvbn2')
except ValidationError as e:
    print(e)

# 2 validation errors
# name
#   must contain a space (type=value_error)
# password2
#   passwords do not match (type=value_error)

需要注意的几件事情:

  • validator 装饰的是类方法。它接收的第一个值是 UserModel 而不是 UserModel 的实例。
  • 它们的签名可以是 (cls, value) 或 (cls, value, values, config, field)。从 v0.20开始,任何 (values, config, field) 的子集都是允许的。例如 (cls, value, field),但是,由于检查 validator 的方式,可变的关键字参数(**kwargs)必须叫做 kwargs。
  • validator 应该返回新的值或者引发 ValueError 或 TypeError 异常。
  • 当验证器依赖于其他值时,应该知道:
    • 验证是按照字段的定义顺序来完成的,例如。这里 password2 可以访问 password1 (和 name),但是password1 不能访问 password2。应该注意以下关于字段顺序和必填字段的警告。
    • 如果在其另一个字段上验证失败(或者那个字段缺失),则其不会被包含在 values 中,因此,上面的例子中包含 if 'password1' in values and … 语句。
注意:从v0.18开始,磨人不会对字典的键调用验证器。如果希望验证键,请使用 whole 。

pre和whole验证

import json
from typing import List
from pydantic import BaseModel, ValidationError, validator


class DemoModel(BaseModel):
    numbers: List[int] = []
    people: List[str] = []

    @validator('people', 'numbers', pre=True, whole=True)
    def json_decode(cls, v):
        if isinstance(v, str):
            try:
                return json.loads(v)
            except ValueError:
                pass
        return v

    @validator('numbers')
    def check_numbers_low(cls, v):
        if v > 4:
            raise ValueError(f'number too large {v} > 4')
        return v

    @validator('numbers', whole=True)
    def check_sum_numbers_low(cls, v):
        if sum(v) > 8:
            raise ValueError(f'sum of numbers greater than 8')
        return v


DemoModel(numbers='[1, 2, 1, 3]')
# <DemoModel numbers=[1, 2, 1, 3] people=[]>
try:
    DemoModel(numbers='[1, 2, 5]')
except ValidationError as e:
    print(e)

# 1 validation error
# numbers -> 2
#   number too large 5 > 4 (type=value_error)
try:
    DemoModel(numbers=[3, 3, 3])
except ValidationError as e:
    print(e)

# 1 validation error
# numbers
#   sum of numbers greater than 8 (type=value_error)

always验证

出于性能原因,默认情况下,对于不提供值的字段不调用验证器。然而,在某些情况下,总是调用验证器是有用的或必需的,例如设置动态默认值。

from datetime import datetime
from pydantic import BaseModel, validator


class DemoModel(BaseModel):
    ts: datetime = None

    @validator('ts', pre=True, always=True)
    def set_ts_now(cls, v):
        return v or datetime.now()


DemoModel()
# <DemoModel ts=datetime.datetime(2019, 4, 26, 8, 26, 13, 74477)>
DemoModel(ts='2017-11-08T14:00')
# <DemoModel ts=datetime.datetime(2017, 11, 8, 14, 0)>

dataclass验证器

验证器在 dataclasses 上也可以工作:

from datetime import datetime
from pydantic import validator
from pydantic.dataclasses import dataclass


@dataclass
class DemoDataclass:
    ts: datetime = None

    @validator('ts', pre=True, always=True)
    def set_ts_now(cls, v):
        return v or datetime.now()


DemoDataclass()
DemoDataclass(ts=None)
DemoDataclass(ts='2017-11-08T14:00')
DemoDataclass(ts=datetime.datetime(2017, 11, 8, 14, 0))

字段检查

在类创建时,将检查验证器,以确认它们指定的字段实际上存在于模型中。

但是,有时并不需要这样做:当定义一个验证器来验证继承模型上的字段时。在这种情况下,您应该在验证器上设置 check_fields=False

递归模型

更复杂的层次数据结构可以使用模型作为注解中的类型来定义。

... 只表示与上面的注解声明相同的 Required

from typing import List
from pydantic import BaseModel


class Foo(BaseModel):
    count: int = ...
    size: float = None


class Bar(BaseModel):
    apple = 'x'
    banana = 'y'


class Spam(BaseModel):
    foo: Foo = ...
    bars: List[Bar] = ...


s = Spam(foo={'count': 4}, bars=[{'apple': 'x1'}, {'apple': 'x2'}])

# <Spam foo=<Foo count=4 size=None> bars=[<Bar apple='x1' banana='y'>, <Bar apple='x2' banana='y'>]>
s.dict()
# {'foo': {'count': 4, 'size': None}, 'bars': [{'apple': 'x1', 'banana': 'y'}, {'apple': 'x2', 'banana': 'y'}]}

模式创建

Pydantic会自动从模型创建JSON Schema。

from enum import Enum
from pydantic import BaseModel, Schema


class FooBar(BaseModel):
    count: int
    size: float = None


class Gender(str, Enum):
    male = 'male'
    female = 'female'
    other = 'other'
    not_given = 'not_given'


class MainModel(BaseModel):
    """
    This is the description of the main model
    """
    foo_bar: FooBar = Schema(...)
    gender: Gender = Schema(
        None,
        alias='Gender',
    )
    snap: int = Schema(
        42,
        title='The Snap',
        description='this is the value of snap',
        gt=30,
        lt=50,
    )

    class Config:
        title = 'Main'


print(MainModel.schema())
# {'title': 'Main', 'description': 'This is the description of the main model', 'type': 'object', 'properties': {'foo_bar': {'$ref': '#/definitions/FooBar'}, 'Gender': {'title': 'Gender', 'enum': ['male', 'female', 'other', 'not_given'], 'type': 'string'}, 'snap': {'title': 'The Snap', 'description': 'this is the value of snap', 'default': 42, 'exclusiveMinimum': 30, 'exclusiveMaximum': 50, 'type': 'integer'}}, 'required': ['foo_bar'], 'definitions': {'FooBar': {'title': 'FooBar', 'type': 'object', 'properties': {'count': {'title': 'Count', 'type': 'integer'}, 'size': {'title': 'Size', 'type': 'number'}}, 'required': ['count']}}}
print(MainModel.schema_json(indent=4))

输出:

{
    "title": "Main",
    "description": "This is the description of the main model",
    "type": "object",
    "properties": {
        "foo_bar": {
            "$ref": "#/definitions/FooBar"
        },
        "Gender": {
            "title": "Gender",
            "enum": [
                "male",
                "female",
                "other",
                "not_given"
            ],
            "type": "string"
        },
        "snap": {
            "title": "The Snap",
            "description": "this is the value of snap",
            "default": 42,
            "exclusiveMinimum": 30,
            "exclusiveMaximum": 50,
            "type": "integer"
        }
    },
    "required": [
        "foo_bar"
    ],
    "definitions": {
        "FooBar": {
            "title": "FooBar",
            "type": "object",
            "properties": {
                "count": {
                    "title": "Count",
                    "type": "integer"
                },
                "size": {
                    "title": "Size",
                    "type": "number"
                }
            },
            "required": [
                "count"
            ]
        }
    }
}

生成的模式符合以下规范: JSON Schema Core,JSON Schema Validation and OpenAPI。

BaseModel.schema 会返回一个表示JSON Schema的字典对象。

BaseModel.schema_json 会返回一个表示表示JSON Schema的字符串。

使用的子模型被添加到JSON Schema中,并根据规范进行引用。所有子模型(及其子模型)模式都会被直接放在JSON Schema的顶级键值对中,便于重用和引用。所有通过 Schema 类进行修改的 子模型 ,比如自定义标题、描述和默认值,都会被递归地包含,而不是引用。模型的描述从类的文档字符串或 Schema 类的参数描述中获得。

Schema类以可选的方式提供关于字段和验证、参数的额外信息:

  • default:位置参数。因为 Schema 类会替换字段的默认值,它的第一个参数用来设置默认值。使用 ... 表示这个字段是必须的。
  • alias:字段的公共名称。
  • title:如果未指定该参数,则使用 field_name.title()。
  • description:如果未指定该参数,并且注解是一个子模型,将使用子模型的文档字符串。
  • gt:对于数值型的值(int, float, Decimal),在JSON Schema中添加一个 大于 验证和一个 exclusiveMinimum 注解。
  • ge:对于数值型的值(int, float, Decimal),在JSON Schema中添加一个 大于等于 验证和一个 minimum 注解。
  • lt:对于数值型的值(int, float, Decimal),在JSON Schema中添加一个 小于 验证和一个 exclusiveMaximum 注解。
  • le:对于数值型的值(int, float, Decimal),在JSON Schema中添加一个 小于等于 验证和一个 maximum 注解。
    multiple_of:对于数值型的值(int, float, Decimal),在JSON Schema中添加一个 倍数 验证和一个 multipleOf 注解。
  • min_length:对于字符串类型的值,在JSON Schema中添加一个相应的验证和 minLength 注释。
  • max_length:对于字符串类型的值,在JSON Schema中添加一个相应的验证和 maxLength 注释。
  • regex:对于字符串类型的值,在JSON Schema中添加一个由给定的字符串生成的正则表达式验证和一个 pattern 注解。
  • **:任何其他关键字参数(例如 example )将会被逐个添加到字段的模式。

Config 类的 fields 特性代替 Schema 类用以设置以上参数中除过 default 参数的其他参数的值。

默认情况下,模式是使用别名作为键生成的,也可以使用模型属性名而不是使用别名生成:

MainModel.schema/schema_json(by_alias=False)

当有一个等价物可用时,类型、自定义字段类型和约束(如 max_length)映射到相应的 JSON Schema Core 规范格式,接下来, JSON Schema Validation, OpenAPI Data Types(基于JSON模式)等将以标准的JSON Schema验证关键字 format 为更复杂的 string 类型定义Pydantic子类型扩展。

要查看从Python/Pydantic 到 JSON Schema的字段模式映射,请参考 字段模式映射。

您还可以生成一个顶级JSON模式,该JSON模式的 definitions 中只包含一个模型列表及其所有相关子模块:

import json
from pydantic import BaseModel
from pydantic.schema import schema


class Foo(BaseModel):
    a: str = None


class Model(BaseModel):
    b: Foo


class Bar(BaseModel):
    c: int


top_level_schema = schema([Model, Bar], title='My Schema')
print(json.dumps(top_level_schema, indent=4))
# {
#     "title": "My Schema",
#     "definitions": {
#         "Foo": {
#             "title": "Foo",
#             "type": "object",
#             "properties": {
#                 "a": {
#                     "title": "A",
#                     "type": "string"
#                 }
#             }
#         },
#         "Model": {
#             "title": "Model",
#             "type": "object",
#             "properties": {
#                 "b": {
#                     "$ref": "#/definitions/Foo"
#                 }
#             },
#             "required": [
#                 "b"
#             ]
#         },
#         "Bar": {
#             "title": "Bar",
#             "type": "object",
#             "properties": {
#                 "c": {
#                     "title": "C",
#                     "type": "integer"
#                 }
#             },
#             "required": [
#                 "c"
#             ]
#         }
#     }
# }

您可以自定义生成的 $ref$ref 的值的仍然在键定义中指定,您仍然可以从键定义中获取键值,但是引用将指向你定义的前缀,而不是默认的前缀。

扩展或修改JSON模式的默认定义位置非常有用,例如使用OpenAPI:

import json
from pydantic import BaseModel
from pydantic.schema import schema


class Foo(BaseModel):
    a: int


class Model(BaseModel):
    a: Foo


top_level_schema = schema([Model], ref_prefix='#/components/schemas/')  # Default location for OpenAPI
print(json.dumps(top_level_schema, indent=4))
# {
#     "definitions": {
#         "Foo": {
#             "title": "Foo",
#             "type": "object",
#             "properties": {
#                 "a": {
#                     "title": "A",
#                     "type": "integer"
#                 }
#             },
#             "required": [
#                 "a"
#             ]
#         },
#         "Model": {
#             "title": "Model",
#             "type": "object",
#             "properties": {
#                 "a": {
#                     "$ref": "#/components/schemas/Foo"
#                 }
#             },
#             "required": [
#                 "a"
#             ]
#         }
#     }
# }

错误处理

当Pydantic在它正在验证的数据中发现错误时,就会引发 ValidationError 异常。

注意:验证代码不应该引发 ValidationError 异常,而应该引发将会被捕获并用于填充 ValidationError异常的 ValueError 或 TypeError (或其子类)异常。。

无论发现多少错误,都只会引发一个异常,ValidationError 将包含关于所有错误及其发生方式的信息。

你可以通过下面几种方式访问这些错误:

  • e.errors():将输入数据中发现的错误作为一个列表返回。
  • e.json():返回表示 e.errors 的JSON。
  • str(e):将 e.errors 以人类可读的字符串返回。

每一个 error 对象包含以下属性:

  • loc:表示错误位置的列表,列表中的第一项是错误发生的字段,后续项将表示子模型在使用时发生错误的字段。
  • type:计算机可读的错误的唯一标识符。
  • msg:人类可读的错误的说明。
  • ctx:一个可选对象,其中包含呈现错误消息所需的值。

下面的例子展示了错误处理的过程:

from typing import List
from pydantic import BaseModel, ValidationError, conint


class Location(BaseModel):
    lat = 0.1
    lng = 10.1


class Model(BaseModel):
    is_required: float
    gt_int: conint(gt=42)
    list_of_ints: List[int] = None
    a_float: float = None
    recursive_model: Location = None


data = dict(
    list_of_ints=['1', 2, 'bad'],
    a_float='not a float',
    recursive_model={'lat': 4.2, 'lng': 'New York'},
    gt_int=21,
)
try:
    Model(**data)
except ValidationError as e:
    print(e)

# 5 validation errors
# is_required
#   field required (type=value_error.missing)
# gt_int
#   ensure this value is greater than 42 (type=value_error.number.not_gt; limit_value=42)
# list_of_ints -> 2
#   value is not a valid integer (type=type_error.integer)
# a_float
#   value is not a valid float (type=type_error.float)
# recursive_model -> lng
#   value is not a valid float (type=type_error.float)
try:
    Model(**data)
except ValidationError as e:
    print(e.json())

# [
#   {
#     "loc": [
#       "is_required"
#     ],
#     "msg": "field required",
#     "type": "value_error.missing"
#   },
#   {
#     "loc": [
#       "gt_int"
#     ],
#     "msg": "ensure this value is greater than 42",
#     "type": "value_error.number.not_gt",
#     "ctx": {
#       "limit_value": 42
#     }
#   },
#   {
#     "loc": [
#       "list_of_ints",
#       2
#     ],
#     "msg": "value is not a valid integer",
#     "type": "type_error.integer"
#   },
#   {
#     "loc": [
#       "a_float"
#     ],
#     "msg": "value is not a valid float",
#     "type": "type_error.float"
#   },
#   {
#     "loc": [
#       "recursive_model",
#       "lng"
#     ],
#     "msg": "value is not a valid float",
#     "type": "type_error.float"
#   }
# ]

如果你自定义了数据类型或者验证器,你应该使用 TypeErrorValueError 引发错误:

from pydantic import BaseModel, ValidationError, validator


class Model(BaseModel):
    foo: str

    @validator('foo')
    def name_must_contain_space(cls, v):
        if v != 'bar':
            raise ValueError('value must be "bar"')
        return v


try:
    Model(foo='ber')
except ValidationError as e:
    print(e.errors())

# [{'loc': ('foo',), 'msg': 'value must be "bar"', 'type': 'value_error'}]

您还可以定义自己的错误类,并且指定错误代码、消息模板和上下文:

from pydantic import BaseModel, PydanticValueError, ValidationError, validator


class NotABarError(PydanticValueError):
    code = 'not_a_bar'
    msg_template = 'value is not "bar", got "{wrong_value}"'


class Model(BaseModel):
    foo: str

    @validator('foo')
    def name_must_contain_space(cls, v):
        if v != 'bar':
            raise NotABarError(wrong_value=v)
        return v


try:
    Model(foo='ber')
except ValidationError as e:
    print(e.json())

# [
#   {
#     "loc": [
#       "foo"
#     ],
#     "msg": "value is not \"bar\", got \"ber\"",
#     "type": "value_error.not_a_bar",
#     "ctx": {
#       "wrong_value": "ber"
#     }
#   }
# ]

datetime类型

Pydantic支持以下的datetime类型:

  • datetime 字段可以是:

    • datetime:已存在的datetime对象
    • int或float:假定为自 1970年1月1日的Unix时间,例如,秒(如果小于等于2e10)或毫秒(如果大于2e10)
    • str:支持如下格式:
      • YYYY-MM-DD[T]HH:MM[:SS[.ffffff]][Z[±]HH[:]MM]]]
      • 表示int或float的字符串(假设为Unix时间)
  • date字段可以是:

    • date:已存在的date对象
    • int或float:请参考datetime字段。
    • str:支持如下格式:
      • YYYY-MM-DD
      • 表示int或float的字符串
  • timedelta字段可以是:

    • timedelta:已存在的timedelta对象
    • int或float:假定为秒
    • str:支持如下格式:
      • [HH:MM]SS[.ffffff]
      • [[±]P[DD]DT[HH]H[MM]M[SS]S (ISO 8601 格式的 timedelta)
from datetime import date, datetime, time, timedelta
from pydantic import BaseModel


class Model(BaseModel):
    d: date = None
    dt: datetime = None
    t: time = None
    td: timedelta = None


m = Model(
    d=1966280412345.6789,
    dt='2032-04-23T10:20:30.400+02:30',
    t=time(4, 8, 16),
    td='P3DT12H30M5S'
)

m.dict()
# {'d': datetime.date(2032, 4, 22),
#  'dt': datetime.datetime(2032, 4, 23, 10, 20, 30, 400000, tzinfo=datetime.timezone(datetime.timedelta(seconds=9000))),
#  't': datetime.time(4, 8, 16), 'td': datetime.timedelta(days=3, seconds=45005)}

Exotic类型

Pydantic附带了许多用于解析或验证公共对象的实用工具。

import uuid
from decimal import Decimal
from ipaddress import IPv4Address, IPv6Address, IPv4Interface, IPv6Interface, IPv4Network, IPv6Network
from pathlib import Path
from uuid import UUID

from pydantic import (DSN, UUID1, UUID3, UUID4, UUID5, BaseModel, DirectoryPath, EmailStr, FilePath, NameEmail,
                      NegativeFloat, NegativeInt, PositiveFloat, PositiveInt, PyObject, UrlStr, conbytes, condecimal,
                      confloat, conint, constr, IPvAnyAddress, IPvAnyInterface, IPvAnyNetwork, SecretStr, SecretBytes)


class Model(BaseModel):
    cos_function: PyObject = None

    path_to_something: Path = None
    path_to_file: FilePath = None
    path_to_directory: DirectoryPath = None

    short_bytes: conbytes(min_length=2, max_length=10) = None
    strip_bytes: conbytes(strip_whitespace=True)

    short_str: constr(min_length=2, max_length=10) = None
    regex_str: constr(regex='apple (pie|tart|sandwich)') = None
    strip_str: constr(strip_whitespace=True)

    big_int: conint(gt=1000, lt=1024) = None
    mod_int: conint(multiple_of=5) = None
    pos_int: PositiveInt = None
    neg_int: NegativeInt = None

    big_float: confloat(gt=1000, lt=1024) = None
    unit_interval: confloat(ge=0, le=1) = None
    mod_float: confloat(multiple_of=0.5) = None
    pos_float: PositiveFloat = None
    neg_float: NegativeFloat = None

    email_address: EmailStr = None
    email_and_name: NameEmail = None

    url: UrlStr = None

    password: SecretStr = None
    password_bytes: SecretBytes = None

    db_name = 'foobar'
    db_user = 'postgres'
    db_password: str = None
    db_host = 'localhost'
    db_port = '5432'
    db_driver = 'postgres'
    db_query: dict = None
    dsn: DSN = None
    decimal: Decimal = None
    decimal_positive: condecimal(gt=0) = None
    decimal_negative: condecimal(lt=0) = None
    decimal_max_digits_and_places: condecimal(max_digits=2, decimal_places=2) = None
    mod_decimal: condecimal(multiple_of=Decimal('0.25')) = None
    uuid_any: UUID = None
    uuid_v1: UUID1 = None
    uuid_v3: UUID3 = None
    uuid_v4: UUID4 = None
    uuid_v5: UUID5 = None
    ipvany: IPvAnyAddress = None
    ipv4: IPv4Address = None
    ipv6: IPv6Address = None
    ip_vany_network: IPvAnyNetwork = None
    ip_v4_network: IPv4Network = None
    ip_v6_network: IPv6Network = None
    ip_vany_interface: IPvAnyInterface = None
    ip_v4_interface: IPv4Interface = None
    ip_v6_interface: IPv6Interface = None

m = Model(
    cos_function='math.cos',
    path_to_something='/home',
    path_to_file='/home/file.py',
    path_to_directory='home/projects',
    short_bytes=b'foo',
    strip_bytes=b'   bar',
    short_str='foo',
    regex_str='apple pie',
    strip_str='   bar',
    big_int=1001,
    mod_int=155,
    pos_int=1,
    neg_int=-1,
    big_float=1002.1,
    mod_float=1.5,
    pos_float=2.2,
    neg_float=-2.3,
    unit_interval=0.5,
    email_address='Samuel Colvin <s@muelcolvin.com >',
    email_and_name='Samuel Colvin <s@muelcolvin.com >',
    url='http://example.com',
    password='password',
    password_bytes=b'password2',
    decimal=Decimal('42.24'),
    decimal_positive=Decimal('21.12'),
    decimal_negative=Decimal('-21.12'),
    decimal_max_digits_and_places=Decimal('0.99'),
    mod_decimal=Decimal('2.75'),
    uuid_any=uuid.uuid4(),
    uuid_v1=uuid.uuid1(),
    uuid_v3=uuid.uuid3(uuid.NAMESPACE_DNS, 'python.org'),
    uuid_v4=uuid.uuid4(),
    uuid_v5=uuid.uuid5(uuid.NAMESPACE_DNS, 'python.org'),
    ipvany=IPv4Address('192.168.0.1'),
    ipv4=IPv4Address('255.255.255.255'),
    ipv6=IPv6Address('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'),
    ip_vany_network=IPv4Network('192.168.0.0/24'),
    ip_v4_network=IPv4Network('192.168.0.0/24'),
    ip_v6_network=IPv6Network('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128'),
    ip_vany_interface=IPv4Interface('192.168.0.0/24'),
    ip_v4_interface=IPv4Interface('192.168.0.0/24'),
    ip_v6_interface=IPv6Interface('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128')
)
print(m.dict())
"""
{
    'cos_function': <built-in function cos>,
    'path_to_something': PosixPath('/home'),
    'path_to_file': PosixPath('/home/file.py'),
    'path_to_directory': PosixPath('/home/projects'),
    'short_bytes': b'foo',
    'strip_bytes': b'bar',
    'short_str': 'foo',
    'regex_str': 'apple pie',
    'strip_str': 'bar',
    'big_int': 1001,
    'mod_int': 155,
    'pos_int': 1,
    'neg_int': -1,
    'big_float': 1002.1,
    'mod_float': 1.5,
    'pos_float': 2.2,
    'neg_float': -2.3,
    'unit_interval': 0.5,
    'email_address': 's@muelcolvin.com',
    'email_and_name': <NameEmail("Samuel Colvin <s@muelcolvin.com>")>,
    'url': 'http://example.com',
    'password': SecretStr('**********'),
    'password_bytes': SecretBytes(b'**********'),
    ...
    'dsn': 'postgres://postgres@localhost:5432/foobar',
    'decimal': Decimal('42.24'),
    'decimal_positive': Decimal('21.12'),
    'decimal_negative': Decimal('-21.12'),
    'decimal_max_digits_and_places': Decimal('0.99'),
    'mod_decimal': Decimal('2.75'),
    'uuid_any': UUID('ebcdab58-6eb8-46fb-a190-d07a33e9eac8'),
    'uuid_v1': UUID('c96e505c-4c62-11e8-a27c-dca90496b483'),
    'uuid_v3': UUID('6fa459ea-ee8a-3ca4-894e-db77e160355e'),
    'uuid_v4': UUID('22209f7a-aad1-491c-bb83-ea19b906d210'),
    'uuid_v5': UUID('886313e1-3b8a-5372-9b90-0c9aee199e5d'),
    'ipvany': IPv4Address('192.168.0.1'),
    'ipv4': IPv4Address('255.255.255.255'),
    'ipv6': IPv6Address('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'),
    'ip_vany_network': IPv4Network('192.168.0.0/24'),
    'ip_v4_network': IPv4Network('192.168.0.0/24'),
    'ip_v6_network': IPv4Network('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128'),
    'ip_vany_interface': IPv4Interface('192.168.0.0/24'),
    'ip_v4_interface': IPv4Interface('192.168.0.0/24'),
    'ip_v6_interface': IPv6Interface('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128')
}
"""

字段也可以是 Callable 类型:

from typing import Callable
from pydantic import BaseModel

class Foo(BaseModel):
    callback: Callable[[int], int]

m = Foo(callback=lambda x: x)
print(m)
# Foo callback=<function <lambda> at 0x7f16bc73e1e0>

#警告:Callable 字段只执行参数是否可调用的简单检查,不执行参数、参数的类型或返回类型的验证。

Secret类型

可以使用 SecretStrSecretBytes 数据类型来存储您不希望在日志记录或回溯中可见的敏感信息。SecretStrSecretBytes 将在转换为JSON时被格式化为 ********** 或空

from typing import List
from pydantic import BaseModel, SecretStr, SecretBytes, ValidationError


class SimpleModel(BaseModel):
    password: SecretStr
    password_bytes: SecretBytes


sm = SimpleModel(password='IAmSensitive', password_bytes=b'IAmSensitiveBytes')
print(sm)
# SimpleModel password=SecretStr('**********') password_bytes=SecretBytes(b'**********')
print(sm.password.get_secret_value())
# IAmSensitive
print(sm.password_bytes.get_secret_value())
b'IAmSensitiveBytes'
try:
    SimpleModel(password=[1, 2, 3], password_bytes=[1, 2, 3])
except ValidationError as e:
    print(e)

# 2 validation errors
# password
#   str type expected (type=type_error.str)
# password_bytes
#   byte type expected (type=type_error.bytes)

JSON类型

可以使用JSON数据类型:Pydantic将首先解析原始JSON字符串,然后根据定义的JSON结构验证已解析的对象(如果提供了该对象)。

from typing import List
from pydantic import BaseModel, Json, ValidationError


class SimpleJsonModel(BaseModel):
    json_obj: Json


class ComplexJsonModel(BaseModel):
    json_obj: Json[List[int]]


SimpleJsonModel(json_obj='{"b": 1}')
# <SimpleJsonModel json_obj={'b': 1}>
ComplexJsonModel(json_obj='[1, 2, 3]')
# <ComplexJsonModel json_obj=[1, 2, 3]>
try:
    ComplexJsonModel(json_obj=12)
except ValidationError as e:
    print(e)

# 1 validation error
# json_obj
# JSON object must be str, bytes or bytearray (type=type_error.json)
try:
    ComplexJsonModel(json_obj='[a, b]')
except ValidationError as e:
    print(e)

# 1 validation error
# json_obj
#   Invalid JSON (type=value_error.json)
try:
    ComplexJsonModel(json_obj='["a", "b"]')
except ValidationError as e:
    print(e)

# 2 validation errors
# json_obj -> 0
#   value is not a valid integer (type=type_error.integer)
# json_obj -> 1
#   value is not a valid integer (type=type_error.integer)

自定义数据类型

你也可以定义你自己的数据类型。类方法 __get_validators__ 将会被调用,用以获取验证器来解析和验证数据。

注意:从v0.17 开始,__get_validators__ 变更成 get_validators,原来的名称仍然可以使用,但是在将来的版本中可能会被移除。
from pydantic import BaseModel, ValidationError


class StrictStr(str):
    @classmethod
    def __get_validators__(cls):
        yield cls.validate

    @classmethod
    def validate(cls, v):
        if not isinstance(v, str):
            raise ValueError(f'strict string: str expected not {type(v)}')
        return v


class Model(BaseModel):
    s: StrictStr


Model(s='hello world')
# <Model s='hello world'>
try:
    print(Model(s=123))
except ValidationError as e:
    print(e.json())

# [
#   {
#     "loc": [
#       "s"
#     ],
#     "msg": "strict string: str expected not <class 'int'>",
#     "type": "value_error"
#   }
# ]

帮助函数

Pydantic在模型中提供了3个帮助函数来解析数据,这三个帮助函数都是类方法。

  • parse_obj:这个方法几乎与模型的 init 方法相同,除过当传递的对象不是 dict,将会引发 ValidationError(而不是Python引发 TypeError)。
  • parse_raw:接收一个字符串或字节对象并解析成JSON,或者使用 pickle 将其反序列化然后传递给 parse_obj 方法。传递的数据的类型通过 content_type 参数来推断,否则假定为JSON。
  • parse_file:读取文件并将其内容传递给 parse_raw 方法,如果未指定 content_type 参数的值,将会由文件拓展名来推断文件内容所表示的Python对象的类型。
import pickle
from datetime import datetime
from pydantic import BaseModel, ValidationError


class User(BaseModel):
    id: int
    name = 'John Doe'
    signup_ts: datetime = None


m = User.parse_obj({'id': 123, 'name': 'bob'})
# <User id=123 name='bob' signup_ts=None>
try:
    User.parse_obj(['not', 'a', 'dict'])
except ValidationError as e:
    print(e)

# 1 validation error
# __obj__
#   User expected dict not list (type=type_error)
m = User.parse_raw('{"id": 123, "name": "James"}')
# <User id=123 name='James' signup_ts=None>
# pickle_data = pickle.dumps({'id': 123, 'name': 'James', 'signup_ts': datetime(2017, 7, 14)})
m = User.parse_raw(pickle_data, content_type='application/pickle', allow_pickle=True)
# <User id=123 name='James' signup_ts=datetime.datetime(2017, 7, 14, 0, 0)>

# 注意:pickle 允许对复杂对象进行编码,要使用它,需要显式地将解析函数的 allow_pickle 参数的值设置为 True。

模型的 Config 类

Pydantic的行为可以通过模型中的 Config 类来控制。

Config 类包含如下一些类属性:

  • anystr_strip_whitespace:是否消除字符串或者字节的前导和后置空白空格。默认为 False。
  • min_anystr_length:字符串或字节的最小长度。默认为0
  • max_anystr_length:字符串或字节的最大长度。默认为 2 ** 16
  • validate_all:是否验证字段的默认值。默认为 False。
  • extra:是否忽略、允许或禁止模型中的额外属性。可以使用字符串值 ignor,allow或 forbid,也可以使用 Extra 的枚举值。默认值是 Extra.ignore。
  • allow_mutation:模型是否为伪不可变类型。默认值为 True。
  • use_enum_values:是否使用枚举的 value 特性填充模型,而不是使用原始枚举。这在您希望序列化 model.dict() 时非常有用。默认值为 False。
  • fields:每个字段的模式信息。这等价于使用 schema 类。默人在 None。
  • validate_assignment:是否在为属性赋值时执行验证。默认为 False。
  • allow_population_by_alias:是否可以按照模型属性(而不是严格的别名)给出的名称填充别名字段;在启用此功能之前,请务必阅读下面的警告。默认值为 False。
    error_msg_templates:用于重写默认的错误消息模版。传入一个字典,其中的键与要覆盖的错误消息匹配。默认值为空的字典。
  • arbitrary_types_allowed:是否允许字段使用任意的用户自定义数据类型(只需检查值是否是该类型的实例即可验证它们)。如果该参数值为 False,在模型声明中使用自定义的数据类型将会引发 RuntimeError 异常。默认值为 False。
  • json_encoders:定制将类型编码为JSON的方式,请参阅JSON序列化了解更多细节。
警告:在启用 allow_population_by_alias 之前请三思!启用它可能会导致先前正确的代码变得不正确。例如,假设您有一个名为 card_number 的字段和别名 cardNumber。在 allow_population_by_alias 属性值为 False 时,只使用键 card_number 尝试解析对象将会失败。但是,如果 allow_population_by_alias 属性值设置为 True,那么现在可以从 cardNumber 或 card_number 填充 card_number 字段,并且先前无效的示例对象现在将是有效的。对于某些用例,这可能是需要的,但是在其他用例中(比如这里给出的例子),放松对别名的严格限制可能会引入bug。
from pydantic import BaseModel, ValidationError


class Model(BaseModel):
    v: str

    class Config:
        max_anystr_length = 10
        error_msg_templates = {'value_error.any_str.max_length': 'max_length:{limit_value}'}


try:
    Model(v='x' * 20)
except ValidationError as e:
    print(e)

# 1 validation error
# v
#   max_length:10 (type=value_error.any_str.max_length; limit_value=10)

基于 @dataclass 装饰器版本的模型:

from datetime import datetime
from pydantic import ValidationError
from pydantic.dataclasses import dataclass


class MyConfig:
    max_anystr_length = 10
    validate_assignment = True
    error_msg_templates = {
        'value_error.any_str.max_length': 'max_length:{limit_value}',
    }


@dataclass(config=MyConfig)
class User:
    id: int
    name: str = 'John Doe'
    signup_ts: datetime = None


user = User(id='42', signup_ts='2032-06-21T12:00')
try:
    user.name = 'x' * 20
except ValidationError as e:
    print(e)

# 1 validation error
# name
#   max_length:10 (type=value_error.any_str.max_length; limit_value=10)

设置

Pydantic最有用的应用之一是定义默认设置,并允许它们被环境变量或关键字参数覆盖(例如在单元测试中)。

from typing import Set
from pydantic import BaseModel, DSN, BaseSettings, PyObject


class SubModel(BaseModel):
    foo = 'bar'
    apple = 1


class Settings(BaseSettings):
    redis_host = 'localhost'
    redis_port = 6379
    redis_database = 0
    redis_password: str = None
    auth_key: str = ...
    invoicing_cls: PyObject = 'path.to.Invoice'
    db_name = 'foobar'
    db_user = 'postgres'
    db_password: str = None
    db_host = 'localhost'
    db_port = '5432'
    db_driver = 'postgres'
    db_query: dict = None
    dsn: DSN = None
    # to override domains:
    # export MY_PREFIX_DOMAINS = '["foo.com", "bar.com"]'
    domains: Set[str] = set()
    # to override more_settings:
    # export MY_PREFIX_MORE_SETTINGS = '{"foo": "x", "apple": 1}'
    more_settings: SubModel = SubModel()

    class Config:
        env_prefix = 'MY_PREFIX_'  # defaults to 'APP_'
        fields = {
            'auth_key': {
                'alias': 'my_api_key'
            }
        }

这里的 redis_port 可以通过 export MY_PREFIX_REDIS_PORT=6380 修改,auth_key 可以通过 exportmy_api_key=6380 修改。

默认情况下,BaseSettings 按照以下优先级来获取字段值(其中3的优先级最高,并会重写其他两项):

  • Settings 中设置的值。

  • 环境变量中设置的值。例如上面的 MY_PREFIX_REDIS_PORT

  • 初始化 Settings 类时传递的参数。

可以通过重写 BaseSettings 类的 _build_values 方法来更改这个默认行为。

复杂的类型,如 listdictset 和子模型可以通过JSON环境变量来设置。

可以以不区分大小写的方式读取环境变量:

from pydantic import BaseSettings

class Settings(BaseSettings):
  redis_host = 'localhost'
  class Config:
    case_insensitive = True

这里的 redis_port 可以通过 export APP_REDIS_HOSTexport app_redis_hostexport app_REDIS_host 修改。

动态模型创建

在某些情况下,模型的形状直到运行时才知道,由于这个原因,Pydantic提供 create_model 方法来允许动态创建模型。

from pydantic import BaseModel, create_model

DynamicFoobarModel = create_model('DynamicFoobarModel', foo=(str, ...), bar=123)

class StaticFoobarModel(BaseModel):
    foo: str
    bar: int = 123

这里,DynamicFoobarModelStaticFoobarModel 是完全相同的。

字段要么由表单的一个元组 (<type>, <default value>) 定义,要么仅由一个默认值定义。特殊的关键字参数 __config____base__ 可以用来定制新模型。这包括使用额外的字段扩展基本模型。

from pydantic import BaseModel, create_model


class FooModel(BaseModel):
    foo: str
    bar: int = 120


BarModel = create_model('BarModel', apple='russet', banana='yellow', __base__=FooModel)
# <class 'BarModel'>
# ', '.join(BarModel.__fields__.keys())
# 'foo, bar, apple, banana'

与mypy一起使用

Pydantic和mypy一起工作,可以让你使用所需变量的 仅注释 版本。

from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, NoneStr


class Model(BaseModel):
    age: int
    first_name = 'John'
    last_name: NoneStr = None
    signup_ts: Optional[datetime] = None
    list_of_ints: List[int]
    

m = Model(age=42, list_of_ints=[1, '2', b'3'])
try:
    Model()
except ValidationError as e:
    print(e)

# 2 validation errors
# age
#   field required (type=value_error.missing)

也可以通过mypy以如下方式运行:

mypy --ignore-missing-imports --follow-imports=skip --strict-optional pydantic_mypy_test.py

严格的可选项

要给代码传递 --strict-optional 选项,需要对所有没有缺省值的字段使用 Optional[] 或 Optional[] 的别名,这是mypy的标准做法。

Pydantic提供了一些有用的可选或联合类型:

  • NoneStr 又叫做 Optional[str]
  • NoneBytes 又叫做 Optional[bytes]
  • StrBytes 又叫做 Union[str, bytes]
  • NoneStrBytes 又叫做 Optional[StrBytes]

如果这些还不够,你可以定义。

必须字段和mypy

省略号符号 不能与mypy一同工作。您需要像上面的示例那样使用注释字段。

警告:请注意,仅使用注释字段将更改元数据中字段的顺序和错误:始终以仅有注释的字段优先,但仍然按照字段定义的顺序。

要解决这个问题,可以使用 Required(通过 pydantic import Required)字段作为仅使用省略号或注释的字段的别名。

伪不可变性

可以通过 allow_mutation = False 将模型配置为不可变的,这将防止更改模型的属性。

警告:Python中的不变性从来都不是严格的。如果开发人员意志坚定/愚蠢,他们总是可以修改所谓的 不可变 对象。
from pydantic import BaseModel


class FooBarModel(BaseModel):
    a: str
    b: dict

    class Config:
        allow_mutation = False


foobar = FooBarModel(a='hello', b={'apple': 'pear'})
try:
    foobar.a = 'different'
except TypeError as e:
    print(e)

试图更改 a 导致了一个错误,并且它仍然保持不变,但是字典对象 b 是可变的,foobar 的不变性不会阻止 b 的值的更改。

复制

dict 方法返回一个包含模型属性的字典。子模型被递归地转换为字典, copy 方法允许模型被复制,这对于不可变模型特别有用。

dict、copy 和 json 方法(如下所述)都使用可选的 include 和 exclude 关键字参数来控制返回或复制哪些属性。copy 方法接受额外的关键字参数 update,它接受将属性映射到新值的类型为字典对象的值,这些新值将在复制模型并进行深度复制时应用。

dict 和 json 采用可选的 skip_defaults 关键字参数,该参数将跳过未显式设置的属性。这对于减少具有许多不经常更改的默认字段的模型的序列化大小非常有用。

from pydantic import BaseModel


class BarModel(BaseModel):
    whatever: int


class FooBarModel(BaseModel):
    banana: float
    foo: str
    bar: BarModel


m = FooBarModel(banana=3.14, foo='hello', bar={'whatever': 456})
m.dict()
# {'banana': 3.14, 'foo': 'hello', 'bar': {'whatever': 456}}
m.dict(include={'foo', 'bar'})
# {'foo': 'hello', 'bar': {'whatever': 456}}
m.dict(exclude={'foo', 'bar'})
# {'banana': 3.14}
m.copy()
# <FooBarModel banana=3.14 foo='hello' bar=<BarModel whatever=456>>
m.copy(include={'foo', 'bar'})
# <FooBarModel foo='hello' bar=<BarModel whatever=456>>
m.copy(exclude={'foo', 'bar'})
# <FooBarModel banana=3.14>
m.copy(update={'banana': 0})
# <FooBarModel banana=0 foo='hello' bar=<BarModel whatever=456>>
id(m.bar), id(m.copy().bar)
# (4417630280, 4417630280)
id(m.bar), id(m.copy(deep=True).bar)
# (4417630280, 4417630928)

序列化

Pydantic支持将数据序列化为JSON和Pickle,当然可以通过处理 dict() 方法的结果将数据序列化为您喜欢的任何其他格式。

JSON序列化

json()方法将模型序列化为JSON,然后 json() 方法调用 dict() 方法并序列化其结果。

可以通过在模型上使用 json_encoders 配置属性定制序列化,键应该是类型,值应该是序列化该类型的函数,参见下面的示例。

如果这还不够,json() 方法接受一个可选的 encoder 参数,该参数允许完全控制如何将非标准类型编码为JSON。

from datetime import datetime, timedelta
from pydantic import BaseModel
from pydantic.json import timedelta_isoformat


class BarModel(BaseModel):
    whatever: int


class FooBarModel(BaseModel):
    foo: datetime
    bar: BarModel


m = FooBarModel(foo=datetime(2032, 6, 1, 12, 13, 14), bar={'whatever': 400})
m.json()


# '{"foo": "2032-06-01T12:13:14", "bar": {"whatever": 400}}'
class WithCustomEncoders(BaseModel):
    dt: datetime
    diff: timedelta

    class Config:
        json_encoders = {
            datetime: lambda v: (v - datetime(1970, 1, 1)).total_seconds(),
            timedelta: timedelta_isoformat
        }


m = WithCustomEncoders(dt=datetime(2032, 6, 1), diff=timedelta(hours=100))
m.json()
# '{"dt": 1969660800.0, "diff": "P4DT4H0M0.000000S"}'

默认情况下,时间增量被编码为一个简单的浮点数,以总秒为单位。timedelta_isoformat 作为一个可选选项提供,它实现了ISO 8601时间差异编码。

Pickle序列化

使用与 copy() 相同的管道,Pydantic支持有效的 pickleunpick

import pickle
from pydantic import BaseModel


class FooBarModel(BaseModel):
    a: str
    b: int


m = FooBarModel(a='hello', b=100)
# <FooBarModel a='hello' b=100>
data = pickle.dumps(m)
# b'\x80\x03c__main__\nFooBarModel\nq\x00)\x81q\x01}q\x02(X\n\x00\x00\x00__values__q\x03}q\x04(X\x01\x00\x00\x00aq\x05X\x05\x00\x00\x00helloq\x06X\x01\x00\x00\x00bq\x07KduX\x0e\x00\x00\x00__fields_set__q\x08cbuiltins\nset\nq\t]q\n(h\x07h\x05e\x85q\x0bRq\x0cub.'
m2 = pickle.loads(data)
# <FooBarModel a='hello' b=100>

抽象基类

Pydantic模型可以与Python的抽象基类(ABC)一起使用。

import abc
from pydantic import BaseModel


class FooBarModel(BaseModel):
    a: str
    b: int

    @abc.abstractmethod
    def my_abstract_method(self):
        pass

延迟注解

注意:通过 future 导入和 ForwardRef 的延迟注解都需要Python 3.7+。

全局延迟注解在Pydantic中应该 只是能工作

from __future__ import annotations
from typing import List
from pydantic import BaseModel


class Model(BaseModel):
    a: List[int]


Model(a=('1', 2, 3))
# <Model a=[1, 2, 3]>

在内部,Pydantic将调用类似于 typing.get_type_hints 的方法用于解析注释。

要使用 ForwardRef ,您可能需要在创建模型之后调用 model .update_forward_refs(),这是因为在下面的示例中,Foo 在创建之前并不存在(显然),所以 ForwardRef 不能首先被解析。您必须等到 Foo 创建之后,然后调用 update_forward_refs 来正确地设置类型,然后才能使用模型。

from typing import ForwardRef
from pydantic import BaseModel

Foo = ForwardRef('Foo')


class Foo(BaseModel):
    a: int = 123
    b: Foo = None


Foo.update_forward_refs()
Foo()
# <Foo a=123 b=None>
Foo(b={'a': '321'})
# <Foo a=123 b=<Foo a=321 b=None>>
# 警告:要将字符串(类型名称)解析成注解(类型),pydantic需要查找 moduel.__dict__ ,就像 get_type_hints 所做的一样 。这意味着pydantic不能很好地处理模块全局范围内未定义的类型。

# 例如,这样可以正常工作:
from __future__ import annotations
from typing import List  # <-- List is defined in the module's global scope
from pydantic import BaseModel

def this_works():
    class Model(BaseModel):
        a: List[int]
    print(Model(a=(1, 2)))
# 但是这样将不能:
from __future__ import annotations
from pydantic import BaseModel

def this_is_broken():
    from typing import List  # <-- List is defined inside the function so is not in the module's global scope
    class Model(BaseModel):
        a: List[int]
    print(Model(a=(1, 2)))
# 解决这个问题超出了pydantic的调用:要么删除 future 的导入,要么全局地声明类型。
posted @ 2020-07-15 20:29  626  阅读(5407)  评论(1编辑  收藏  举报