Pydantic

来源:https://www.cnblogs.com/fengqiang626/archive/2020/07/15/13307771.html
-Advertisement-
Play Games

Pydantic 是一個使用Python類型提示來進行數據驗證和設置管理的庫。Pydantic定義數據應該如何使用純Python規範用併進行驗證。PEP 484 從Python3.5開始引入了類型提示的功能,PEP 526 使用Python3.6中的變數註釋語法對其進行了拓展。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 <[email protected] >',
    email_and_name='Samuel Colvin <[email protected] >',
    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': '[email protected]',
    'email_and_name': <NameEmail("Samuel Colvin <[email protected]>")>,
    '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 的導入,要麼全局地聲明類型。

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 繼Golang學習系列第二天:變數、常量、數據類型和流程語句之後,今天開始學習數據類型之高級類型: 派生類型。 學過java的人都知道,java其實就8種基本類型:byte、short、int、long、float、double、char、boolean,但它有引用數據類型:字元串、數組、集合、類、 ...
  • 今天我們繼續來學習C語言的入門知識點 11. 作用域規則 任何一種編程中,作用域是程式中定義的變數所存在的區域,超過該區域變數就不能被訪問。C 語言中有三個地方可以聲明變數: 在函數或塊內部的局部變數 在所有函數外部的全局變數 在形式參數的函數參數定義中 讓我們來看看什麼是局部變數、全局變數和形式參 ...
  • 本文源碼:GitHub·點這裡 || GitEE·點這裡 一、集群環境搭建 1、環境概覽 ES版本6.3.2,集群名稱esmaster,虛擬機centos7。 服務群 角色劃分 說明 en-master master 主節點:esnode1 en-node01 slave 從節點:esnode2 e ...
  • 列表生成式即List Comprehensions,是Python內置的非常簡單卻強大的可以用來創建list的生成式。 通常我們在迴圈遍歷一個列表時,都是通過for迴圈來完成 L = [] for i in range(1,11) L.append(x*x) 結果如下: [1,4,9,16,25,3 ...
  • 1、一切皆對象 一、 類也是對象 在大多數編程語言中,類就是一組用來描述如何生成一個對象的代碼段,在Python中這一點仍然成立。但是,Python中的類還遠不止如此。類同樣也是一種對象。只要你使用關鍵字class,Python解釋器在執行的時候就會創建一個對象。下麵的代碼段: class MyCl ...
  • 一、基本概念 程式(program): 是為完成特定任務、用某種語言編寫的一組指令的集合。即指一 段靜態的代碼,靜態對象。 進程(process):是程式的一次執行過程,或是正在運行的一個程式。是一個動態 的過程:有它自身的產生、存在和消亡的過程。——生命周期 運行中的QQ,運行中的MP3播放器 程 ...
  • 簡介 道可道,非常道。這裡常道指的永恆不變的道理,常有不變的意思。顧名思義和變數相比,常量在聲明之後就不可改變,它的值是在編譯期間就確定的。 下麵簡單的聲明一個常量: const p int = 1 聲明常量的時候可以指定類型也可以類似:=簡單聲明一樣,不指定類型如下: const p = 1 也可 ...
  • from typing import Listclass Solution: # 第一種是我想的辦法 def singleNumber(self, nums: List[int]) -> int: # 首先進行排序 nums.sort() # 然後判斷重覆的數字,數組中的數字必定為奇數個, # 如果 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...