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 的導入,要么全局地聲明類型。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM