0-pydantic校驗管理


1. 簡介

1.7.1 版本的文檔。

使用Python類型注解進行數據驗證和設置管理。

Pydantic 在運行時強制執行類型提示,並在數據無效時提供用戶友好的錯誤信息。

定義數據如何表示為純粹和規范的 Python ,並使用 pydantic 對其進行驗證。

1.1 示例:

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


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


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

這里發生了什么:

  • id 是 int 類型;注釋聲明告訴pydantic該字段是必須的。如果可能,字符串、字節或浮點數將強制轉換為int,否則將引發異常。
  • name 從默認值推斷為其為 str 類型,該字段不是必須的,因為它有默認值。
  • signup_ts 是 datetime 類型,該字段不是必須的,默認值為 None。pydantic會將表示unix時間戳(例如1496498400)的 int 類型或表示時間和日期的字符串處理成 datetime 類型。
  • friends 使用Python的 typing 系統,需要一個整數列表,就像 id 字段一樣,類整數的對象將會被轉換為整數。

如果驗證失敗,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": "value_error.datetime"
  },
  {
    "loc": [
      "friends",
      2
    ],
    "msg": "value is not a valid integer",
    "type": "type_error.integer"
  }
]

1.2 基本原理

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

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

    不需要學習新的模式定義微語言。如果您知道如何使用 Python 的類型提示,也就知道如何使用 pydantic。

    因為pydantic數據結構只是您使用類型注解定義的類的實例,所以自動完成、linting、mypy、IDE(尤其是 PyCharm)和您的直覺都應該能夠正確地處理經過驗證的數據。

  • 多用途

    pydantic的 [BaseSettings](# 3.9 設置管理) 類允許在 “驗證此請求數據” 上下文和 “加載我的系統設置” 上下文中使用。主要區別在於,系統設置可以從環境變量讀取,並且通常需要更復雜的對象,如DSN和Python對象。

  • 快速

    pydantic比其他所有測試庫都要快。

  • 可以驗證復雜結構

    使用[遞歸pydantic模型](# 3.1.2 遞歸模型)、typing 的標准類型 (如 List、Tuple 和 Dict 等) 和驗證器,可以很清晰且容易地定義、驗證和解析復雜數據模式。

  • 可拓展

    pydantic允許定義[自定義數據類型](# 3.2.7 自定義數據類型),或者您可以使用被 validator 裝飾器裝飾的模型上的方法來擴展驗證。

  • dataclasses 集成

    和 BaseModel 一樣,pydantic提供了一個 [dataclass](# 3.7 Dataclasses) 裝飾器,它創建帶有輸入數據解析和驗證的(幾乎)普通的Python數據類。

2. 安裝

pip install pydantic

Pydantic除了Python3.6、3.7、3.8 或 3.9(和Python3.6中的 dataclasses 包)之外,不需要其他依賴項。

Pydantic 可以可選的使用 Cython 進行編譯,將會帶來 30%-50%的性能提升。

PyPI可以為Linux、MacOS和64位Windows提供二進制文件。如果您是手動安裝,請在安裝pydantic之前安裝 cython,這樣編譯就會自動進行。

要測試pydantic 是否已經編譯,可以使用如下方法:

>>> import pydantic
>>> print(pydantic.compiled)

Pydantic 有三個可選依賴:

  • 如果需要 email 驗證,可以添加 email-validator。
  • 要在 Python3.8之前的版本中使用 Literal,需要安裝 typing-extensions。
  • 使用 Settings 的 dotenv 文件支持需要安裝 python-dotenv。

要將這些與 pydantic 一起安裝,可以使用如下方式:

pip install pydantic[email]
# or
pip install pydantic[typing_extensions]
# or
pip install pydantic[dotenv]
# or just
pip install pydantic[email,typing_extensions,dotenv]

當然,你可以使用 pip install email-validator 和/或 pip install typing_extensions 手動安裝這些依賴。

如果想從存儲庫直接安裝 pydantic,可以使用:

pip install git+git://github.com/samuelcolvin/pydantic@master#egg=pydantic
# or with extras
pip install git+git://github.com/samuelcolvin/pydantic@master#egg=pydantic[email,typing_extensions]

3.用法詳解

3.1 模型

在pydantic中定義對象的主要方法是通過模型(模型只是繼承自 BaseModel 的類)。

您可以將模型看作嚴格類型語言中的類型,或者看作API中單個端點的需求。

不受信任的數據可以傳遞給模型,在解析和驗證之后,pydantic保證結果模型實例的字段將符合模型上定義的字段類型。

注意

pydantic主要是一個解析庫,而不是驗證庫。驗證是達到目的的一種手段:構建符合所提供的類型和約束的模型。

換句話說,pydantic保證輸出模型的類型和約束,而不是輸入數據。

這聽起來像是一個深奧的區別,但其實不然。如果您不確定這是什么意思或者它如何影響您的使用,那么您應該閱讀下面關於[數據轉換](# 3.1.16 數據轉換)的部分。

3.1.1 基本模型的使用

from pydantic import BaseModel

class User(BaseModel):
    id: int
    name = 'Jane Doe'

在這里,User 是具有兩個字段的模型,其中字段 id 是整數類型,並且是必需的;name 字段是字符串類型,但不是必需的(它有默認值)。name 的類型是從其默認值推斷來的,因此,類型注解不是必需的(但是,當某些字段沒有類型注解時,請注意[這個](# 3.1.11 字段排序)關於字段順序的警告)。

user = User(id='123')

這里的 userUser 的一個實例。對象的初始化會執行所有解析和驗證,如果沒有引發 ValidationError 異常,則表明結果模型實例是有效的。

assert user.id == 123

模型的字段可以作為User 對象的普通屬性訪問。字符串 ‘123’ 根據字段類型被強制轉換為 int 類型:

assert user.name == 'Jane Doe'

初始化 User 對象時未設置 name 的值,所以它的值是默認值:

assert user.__fields_set__ == {'id'}

User 對象初始化時提供的字段:

assert user.dict() == dict(user) == {'id': 123, 'name': 'Jane Doe'}

.dict()dict(user) 都將會提供一個字段的字典,但 .dict() 可以接受許多其他參數:

user.id = 321
assert user.id == 321

這個模型是可變模型,所以字段值可以更改。

3.1.1.1 模型屬性

上面的例子只展示了模型所能做的事情的冰山一角。模型具有以下方法和屬性:

  • dict()

    返回模型的字段和值的字典。參見 [導出模型](# 3.6.1 model.dict(…))。

  • json()

    返回表示 dict() 的 JSON 字符串。參見 [導出模型](# 3.6.1 model.dict(…))。

  • copy()

    返回模型的副本(默認情況下為淺副本)。參見 [導出模型](# 3.6.1 model.dict(…))。

  • parse_obj()

    如果一個對象不是字典,可以使用該方法將其加載到具有錯誤處理的模型中。參見 [幫助函數](# 3.1.5 幫助函數)。

  • parse_raw()

    用於加載多種格式字符串的實用程序。參見 [幫助函數](# 3.1.5 幫助函數)。

  • parse_file()

    與 parse_raw() 類似,但是作用於文件路徑。參見 [幫助函數](# 3.1.5 幫助函數)。

  • from_orm()

    從任意類加載數據到模型中。參見 [ORM 模式](# 3.1.3 ORM 模式)。

  • schema()

    返回一個將模型表示為 JSON 模式的字典。參見 [模式](# 3.5 模式)。

  • schema_json()

    返回表示 schema() 的 JSON 字符串。參見 [模式](# 3.5 模式)。

  • construct()

    用於創建模型而不執行驗證的類方法;參見 [創建未經驗證的模型](# 3.1.5.1 創建未經驗證的模型)。

  • fields_set

    當模型實例初始化時設置的字段名稱集合。

  • config

    模型的配置類。參見 [模型配置](# 3.4 模型配置)。

3.1.2 遞歸模型

可以通過在注解中使用模型本身作為類型來定義更復雜的分層數據結構。

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]

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

對於自引用模型,參見 [延遲注解](# 3.10 延遲注解)。

3.1.3 ORM 模式

可以從任意類實例創建Pydantic模型,以支持映射到ORM對象的模型。

要達到這個目的,需要:

  • 模型的內部類 [Config](# 3.4 模型配置) 的 orm_mode 屬性必須設置為 True。
  • 必須使用特殊的構造函數 from_orm 來創建模型實例。

這里的示例使用SQLAlchemy,但是相同的方法應該適用於任何ORM。

from typing import List
from sqlalchemy import Column, Integer, String
from sqlalchemy.dialects.postgresql import ARRAY
from sqlalchemy.ext.declarative import declarative_base
from pydantic import BaseModel, constr

Base = declarative_base()


class CompanyOrm(Base):
    __tablename__ = 'companies'
    id = Column(Integer, primary_key=True, nullable=False)
    public_key = Column(String(20), index=True, nullable=False, unique=True)
    name = Column(String(63), unique=True)
    domains = Column(ARRAY(String(255)))


class CompanyModel(BaseModel):
    id: int
    public_key: constr(max_length=20)
    name: constr(max_length=63)
    domains: List[constr(max_length=255)]

    class Config:
        orm_mode = True


co_orm = CompanyOrm(
    id=123,
    public_key='foobar',
    name='Testing',
    domains=['example.com', 'foobar.com'],
)
print(co_orm)
#> <models_orm_mode.CompanyOrm object at 0x7f2e727a27c0>
co_model = CompanyModel.from_orm(co_orm)
print(co_model)
#> id=123 public_key='foobar' name='Testing' domains=['example.com',
#> 'foobar.com']

3.1.3.1 保留名稱

您可能想使用保留的SQLAlchemy字段命名列。在這種情況下,可以使用字段別名:

import typing

from pydantic import BaseModel, Field
import sqlalchemy as sa
from sqlalchemy.ext.declarative import declarative_base


class MyModel(BaseModel):
    metadata: typing.Dict[str, str] = Field(alias='metadata_')

    class Config:
        orm_mode = True


BaseModel = declarative_base()


class SQLModel(BaseModel):
    __tablename__ = 'my_table'
    id = sa.Column('id', sa.Integer, primary_key=True)
    # 'metadata' is reserved by SQLAlchemy, hence the '_'
    metadata_ = sa.Column('metadata', sa.JSON)


sql_model = SQLModel(metadata_={'key': 'val'}, id=1)

pydantic_model = MyModel.from_orm(sql_model)

print(pydantic_model.dict())
#> {'metadata': {'key': 'val'}}
print(pydantic_model.dict(by_alias=True))
#> {'metadata_': {'key': 'val'}}

注意

上面的示例之所以能夠工作,是因為對於字段填充,別名優先於字段名。訪問 SQLModel 的 metadata 屬性將導致ValidationError。

3.1.3.2 遞歸 ORM 模型

ORM實例將使用 from_orm 遞歸解析,也可以在頂層解析。

這里使用一個普通類來演示這個原理,但是可以使用任何ORM類。

from typing import List
from pydantic import BaseModel


class PetCls:
    def __init__(self, *, name: str, species: str):
        self.name = name
        self.species = species


class PersonCls:
    def __init__(self, *, name: str, age: float = None, pets: List[PetCls]):
        self.name = name
        self.age = age
        self.pets = pets


class Pet(BaseModel):
    name: str
    species: str

    class Config:
        orm_mode = True


class Person(BaseModel):
    name: str
    age: float = None
    pets: List[Pet]

    class Config:
        orm_mode = True


bones = PetCls(name='Bones', species='dog')
orion = PetCls(name='Orion', species='cat')
anna = PersonCls(name='Anna', age=20, pets=[bones, orion])
anna_model = Person.from_orm(anna)
print(anna_model)
#> name='Anna' age=20.0 pets=[Pet(name='Bones', species='dog'),
#> Pet(name='Orion', species='cat')]

Pydantic使用 GetterDict 類 (請參閱 util.py) 處理任意類,該類試圖為任何類提供一個類似於字典的接口。可以通過將 GetterDict 的自定義子類設置為 Config.getter_dict 的值來覆蓋默認行為(參考 [模型配置](# 3.4 模型配置))。

您還可以使用帶有 pre=True 的 root_validators 定制類驗證。在這種情況下,validator 函數將被傳遞一個可以復制和修改的GetterDict 實例。

3.1.4 錯誤處理

每當在正在驗證的數據中發現錯誤時,pydantic都會引發 ValidationError。

注意

驗證代碼不應該引發 ValidationError 本身,而是引發 ValueErrorTypeErrorAssertionError(或 ValueErrorTypeError 的子類),這些異常將被捕獲並用於填充 ValidationError

無論發現的錯誤數量如何,都只會出現一個異常,ValidationError 將包含關於所有錯誤及其發生方式的信息。

你可以通過幾種方式訪問這些錯誤:

  • e.errors()

    返回輸入數據中發現的錯誤的列表。

  • e.json()

    返回一個表示 errors 的 JSON。

  • str(e)

    返回人類可讀的錯誤表示。

每一個錯誤對象包含:

  • loc

    錯誤的位置列表。列表中的第一項將是發生錯誤的字段,如果該字段是[子模型](# 3.1.2 遞歸模型),則將出現后續項以指示錯誤的嵌套位置。

  • 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 for Model
    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"
      }
    ]
    """

json()方法默認設置了 indent=2,但是我在這里和下面對JSON 進行了調整,使其更加簡潔。

3.1.4.1 定制錯誤

在您的自定義數據類型或驗證器中,您應該使用 ValueError、TypeError 或 AssertionError 來引發錯誤。

有關 @validator 裝飾器的使用細節,請參閱 [驗證器](# 3.3 驗證器)。

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"
        }
      }
    ]
    """

3.1.5 幫助函數

Pydantic 在模型上提供了三個 classmethod 幫助函數用於解析數據:

  • parse_obj

    這與模型中的 init 方法非常相似,除了它使用的是 dict 而不是關鍵字參數。如果傳遞的對象不是 dict,則會引發ValidationError。

  • parse_raw

    它接受一個 str 或 bytes 並將其解析為 json,然后將結果傳遞給 parse_obj。適當地設置 content_type 參數也支持解析pickle 數據。

  • parse_file

    它接受一個文件路徑,讀取文件並將內容傳遞給 parse_raw。如果省略了 content_type,將從文件的擴展名進行推斷。

import pickle
from datetime import datetime
from pathlib import Path

from pydantic import BaseModel, ValidationError


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


m = User.parse_obj({'id': 123, 'name': 'James'})
print(m)
#> id=123 signup_ts=None name='James'

try:
    User.parse_obj(['not', 'a', 'dict'])
except ValidationError as e:
    print(e)
    """
    1 validation error for User
    __root__
      User expected dict not list (type=type_error)
    """

# assumes json as no content type passed
m = User.parse_raw('{"id": 123, "name": "James"}')
print(m)
#> id=123 signup_ts=None name='James'

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
)
print(m)
#> id=123 signup_ts=datetime.datetime(2017, 7, 14, 0, 0) name='James'

path = Path('data.json')
path.write_text('{"id": 123, "name": "James"}')
m = User.parse_file(path)
print(m)
#> id=123 signup_ts=None name='James'

警告

引用 pickle 的官方文檔,“pickle模塊對於錯誤或惡意構造的數據是不安全的。切勿從不可信或未經身份驗證的來源獲取數據。”

注意

由於它可能導致任意代碼的執行,因此作為一種安全措施,您需要顯式地將 allow_pickle 傳遞給解析函數,以便加載pickle數據。

3.1.5.1 創建未經驗證的模型

Pydantic還提供了 construct() 方法,允許創建未經驗證的模型。當數據已經被驗證或來自可信的源,並且你想要盡可能高效地創建一個模型 (使用 construct() 方法創建模型通常比創建完整驗證的模型塊 30 倍)。

警告

construct() 不做任何驗證,這意味着它可以創建無效的模型。您務必只對已經經過驗證或您信任的數據使用 construct() 方法。

from pydantic import BaseModel


class User(BaseModel):
    id: int
    age: int
    name: str = 'John Doe'


original_user = User(id=123, age=32)

user_data = original_user.dict()
print(user_data)
#> {'id': 123, 'age': 32, 'name': 'John Doe'}
fields_set = original_user.__fields_set__
print(fields_set)
#> {'id', 'age'}

# ...
# pass user_data and fields_set to RPC or save to the database etc.
# ...

# you can then create a new instance of User without
# re-running validation which would be unnecessary at this point:
new_user = User.construct(_fields_set=fields_set, **user_data)
print(repr(new_user))
#> User(name='John Doe', id=123, age=32)
print(new_user.__fields_set__)
#> {'id', 'age'}

# construct can be dangerous, only use it with validated data!:
bad_user = User.construct(id='dog')
print(repr(bad_user))
#> User(name='John Doe', id='dog')

construct() 方法的 _fields_set 關鍵字參數是可選的,但是允許您更精確地知道哪些字段是初始化時設置的,哪些是具有默認值的。如果該參數被省略,那么 fields_set 將只包含數據提供的鍵。

例如,在上面的示例中,如果未提供 _fields_set 參數,new_user.fields_set 將會是 {'id', 'age', 'name'}。

3.1.6 泛型模型

Pydantic支持創建泛型模型,以便更容易重用公共模型結構。

警告

泛型模型只在Python >=3.7中得到支持,這是因為在python 3.6和python 3.7之間實現泛型的方式有許多細微的變化。

為了聲明泛型模型,必須執行下面的步驟:

  • 聲明一個或多個 typing.TypeVar 實例來參數化您的模型。
  • 聲明一個繼承自 pydantic.generics.GenericModel 和 typing.Generic 的 pydantic 模型,其中將 TypeVar 實例作為參數傳遞給 typing.Generic 。
  • 使用 TypeVar 實例對將要使用其他類型或pydantic模型進行替換的字段進行注解。

下面是一個使用 GenericModel 創建一個易於重用的HTTP響應有效負載包裝器的例子:

from typing import Generic, TypeVar, Optional, List

from pydantic import BaseModel, validator, ValidationError
from pydantic.generics import GenericModel

DataT = TypeVar('DataT')


class Error(BaseModel):
    code: int
    message: str


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


class Response(GenericModel, Generic[DataT]):
    data: Optional[DataT]
    error: Optional[Error]

    @validator('error', always=True)
    def check_consistency(cls, v, values):
        if v is not None and values['data'] is not None:
            raise ValueError('must not provide both data and error')
        if v is None and values.get('data') is None:
            raise ValueError('must provide data or error')
        return v


data = DataModel(numbers=[1, 2, 3], people=[])
error = Error(code=404, message='Not found')

print(Response[int](data=1))
#> data=1 error=None
print(Response[str](data='value'))
#> data='value' error=None
print(Response[str](data='value').dict())
#> {'data': 'value', 'error': None}
print(Response[DataModel](data=data).dict())
"""
{
    'data': {'numbers': [1, 2, 3], 'people': []},
    'error': None,
}
"""
print(Response[DataModel](error=error).dict())
"""
{
    'data': None,
    'error': {'code': 404, 'message': 'Not found'},
}
"""
try:
    Response[int](data='value')
except ValidationError as e:
    print(e)
    """
    2 validation errors for Response[int]
    data
      value is not a valid integer (type=type_error.integer)
    error
      must provide data or error (type=value_error)
    """

如果您在泛型模型定義中設置 Config 或使用 validator,則其將以從 BaseModel 繼承時相同的方式應用於具體的子類。在泛型類上定義的任何方法也將被繼承。

Pydantic的泛型也正確地與mypy集成,因此如果您不使用 GenericModel 聲明類型,就可以得到mypy所提供的所有類型檢查。

注意

在內部,pydantic在運行時使用 create_model 生成(緩存的)具體 BaseModel,因此使用GenericModel基本上不會帶來任何開銷。

為了在不替換 TypeVar 實例的情況下繼承 GenericModel,類也必須繼承 type.Generic:

from typing import TypeVar, Generic
from pydantic.generics import GenericModel

TypeX = TypeVar('TypeX')


class BaseClass(GenericModel, Generic[TypeX]):
    X: TypeX


class ChildClass(BaseClass[TypeX], Generic[TypeX]):
    # Inherit from Generic[TypeX]
    pass


# Replace TypeX by int
print(ChildClass[int](X=1))
#> X=1

您還可以創建 GenericModel 的泛型子類,以部分或全部替換超類中的類型參數。

from typing import TypeVar, Generic
from pydantic.generics import GenericModel

TypeX = TypeVar('TypeX')
TypeY = TypeVar('TypeY')
TypeZ = TypeVar('TypeZ')


class BaseClass(GenericModel, Generic[TypeX, TypeY]):
    x: TypeX
    y: TypeY


class ChildClass(BaseClass[int, TypeY], Generic[TypeY, TypeZ]):
    z: TypeZ


# Replace TypeY by str
print(ChildClass[str, int](x=1, y='y', z=3))
#> x=1 y='y' z=3

如果具體子類的名稱很重要,您還可以重寫默認行為:

from typing import Generic, TypeVar, Type, Any, Tuple

from pydantic.generics import GenericModel

DataT = TypeVar('DataT')


class Response(GenericModel, Generic[DataT]):
    data: DataT
        
    @classmethod
    def __concrete_name__(cls: Type[Any], params: Tuple[Type[Any], ...]) -> str:
        return f'{params[0].__name__.title()}Response'


int_resp = Response[int](data=1)
print(int_resp)
#> data=1
print(repr(int_resp))
#> IntResponse(data=1)
str_resp = Response[str](data='a') 
print(str_resp)
#> data='a'
print(repr(str_resp))
#> StrResponse(data='a')

在嵌套模型中使用相同類型允許您在模型的不同點強制類型關系:

from typing import Generic, TypeVar

from pydantic import ValidationError
from pydantic.generics import GenericModel

T = TypeVar('T')


class InnerT(GenericModel, Generic[T]):
    inner: T


class OuterT(GenericModel, Generic[T]):
    outer: T
    nested: InnerT[T]


nested = InnerT[int](inner=1)
print(OuterT[int](outer=1, nested=nested))
#> outer=1 nested=InnerT[T][int](inner=1)
try:
    nested = InnerT[str](inner='a')
    print(OuterT[int](outer='a', nested=nested))
except ValidationError as e:
    print(e)
    """
    2 validation errors for OuterT[int]
    outer
      value is not a valid integer (type=type_error.integer)
    nested -> inner
      value is not a valid integer (type=type_error.integer)
    """

Pydantic對待 GenericModel 的方式與它對待內置泛型類型(如 List 和 Dict )的方式類似,比如讓它們保持未參數化,或者使用有界(bounded)類型實例:

  • 如果在實例化泛型模型之前沒有指定參數,那么它們將被視為 Any。
  • 您可以使用一個或多個有界(bounded)參數對模型進行參數化,以添加子類檢查。

另外,與 List 和 Dict 一樣,使用 TypeVar 指定的任何參數都可以在以后用具體類型替換。

from typing import Generic, TypeVar

from pydantic import ValidationError
from pydantic.generics import GenericModel

AT = TypeVar('AT')
BT = TypeVar('BT')


class Model(GenericModel, Generic[AT, BT]):
    a: AT
    b: BT


print(Model(a='a', b='a'))
#> a='a' b='a'

IntT = TypeVar('IntT', bound=int)
typevar_model = Model[int, IntT]
print(typevar_model(a=1, b=1))
#> a=1 b=1
try:
    typevar_model(a='a', b='a')
except ValidationError as exc:
    print(exc)
    """
    2 validation errors for Model[int, IntT]
    a
      value is not a valid integer (type=type_error.integer)
    b
      value is not a valid integer (type=type_error.integer)
    """

concrete_model = typevar_model[int]
print(concrete_model(a=1, b=1))
#> a=1 b=1

3.1.7 動態模型創建

有些情況下,模型的形狀直到運行時才知道。為此,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

在這里,StaticFooBarModelDynamicFooBarModel 是相同的。

警告

請參閱 [具有 Optional 注解的必須字段](# 3.1.12.1 具有 Optional 注解的必須字段) 中的注釋,以了解使用省略號作為字段默認值的字段與僅含有注解的字段之間的區別。更多細節請參見samuelcolvin/pydantic#1047

字段可以由 (<type>, <default value>) 形式的元組定義,也可以僅由默認值定義。__config____base__ 這兩個特殊的關鍵字參數可以用來定制新模型。這包括使用額外的字段拓展基本模型。

from pydantic import BaseModel, create_model


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


BarModel = create_model(
    'BarModel',
    apple='russet',
    banana='yellow',
    __base__=FooModel,
)
print(BarModel)
#> <class 'pydantic.main.BarModel'>
print(BarModel.__fields__.keys())
#> dict_keys(['foo', 'bar', 'apple', 'banana'])

也可以通過給 __validators__ 參數傳遞一個字典來添加驗證器:

from pydantic import create_model, ValidationError, validator


def username_alphanumeric(cls, v):
    assert v.isalnum(), 'must be alphanumeric'
    return v


validators = {
    'username_validator':
    validator('username')(username_alphanumeric)
}

UserModel = create_model(
    'UserModel',
    username=(str, ...),
    __validators__=validators
)

user = UserModel(username='scolvin')
print(user)
#> username='scolvin'

try:
    UserModel(username='scolvi%n')
except ValidationError as e:
    print(e)
    """
    1 validation error for UserModel
    username
      must be alphanumeric (type=assertion_error)
    """

3.1.8 自定義根類型

可以通過聲明 root 字段來使用自定義根類型定義Pydantic 模型。

根類型可以是 pydantic 支持的任意類型,該類型通過 root 字段上的類型提示來指定。根值可以通過模型的 init 方法的 root 參數傳遞,或者作為 parse_obj 的第一個且唯一的一個參數。

from typing import List
import json
from pydantic import BaseModel
from pydantic.schema import schema


class Pets(BaseModel):
    __root__: List[str]


print(Pets(__root__=['dog', 'cat']))
#> __root__=['dog', 'cat']
print(Pets(__root__=['dog', 'cat']).json())
#> ["dog", "cat"]
print(Pets.parse_obj(['dog', 'cat']))
#> __root__=['dog', 'cat']
print(Pets.schema())
"""
{
    'title': 'Pets',
    'type': 'array',
    'items': {'type': 'string'},
}
"""
pets_schema = schema([Pets])
print(json.dumps(pets_schema, indent=2))
"""
{
  "definitions": {
    "Pets": {
      "title": "Pets",
      "type": "array",
      "items": {
        "type": "string"
      }
    }
  }
}
"""

如果使用字典作為第一個參數調用一個具有自定義根類型的模型的 parse_obj 方法,將會應用下面的邏輯:

  • 如果自定義的根類型是映射類型(例如 Dict 或 Mapping),參數本身總是根據自定義根類型進行驗證。

  • 對於其他自定義根類型,如果字典僅有一個鍵,且其名稱為 root,則該鍵對應的值將根據自定義根類型進行驗證。

下面是一個示例:

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


class Pets(BaseModel):
    __root__: List[str]


print(Pets.parse_obj(['dog', 'cat']))
#> __root__=['dog', 'cat']
print(Pets.parse_obj({'__root__': ['dog', 'cat']}))  # not recommended
#> __root__=['dog', 'cat']


class PetsByName(BaseModel):
    __root__: Dict[str, str]


print(PetsByName.parse_obj({'Otis': 'dog', 'Milo': 'cat'}))
#> __root__={'Otis': 'dog', 'Milo': 'cat'}
try:
    PetsByName.parse_obj({'__root__': {'Otis': 'dog', 'Milo': 'cat'}})
except ValidationError as e:
    print(e)
    """
    1 validation error for PetsByName
    __root__ -> __root__
      str type expected (type=type_error.str)
    """

警告

在字典上調用 parse_obj 方法時,對於非映射的自定義根類型使用單個鍵 "__root__",目前僅為保持向后兼容而支持,但不建議這樣做,將來的版本中可能會刪除。

如果您想直接訪問剩余的 __root__ 字段中的條目,或者在這些條目上迭代,您可以實現自定義的 __iter____getitem__ 函數,如下面的例子所示。

from typing import List
from pydantic import BaseModel


class Pets(BaseModel):
    __root__: List[str]

    def __iter__(self):
        return iter(self.__root__)

    def __getitem__(self, item):
        return self.__root__[item]


pets = Pets.parse_obj(['dog', 'cat'])
print(pets[0])
#> dog
print([pet for pet in pets])
#> ['dog', 'cat']

3.1.9 偽不變性

可以通過 allow_mutation = False 將模型配置為不可變的。當設置了這個值時,嘗試更改實例屬性的值將會引發錯誤。有 Config 的更多細節,請參見[模型配置](# 3.4 模型配置)。

警告

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)
    #> "FooBarModel" is immutable and does not support item assignment

print(foobar.a)
#> hello
print(foobar.b)
#> {'apple': 'pear'}
foobar.b['apple'] = 'grape'
print(foobar.b)
#> {'apple': 'grape'}

嘗試更改 a 將會引發一個錯誤,而 a 則保持不變。然而,字典 b 是可變的,並且 foobar 的不可變性不能阻止 b 被改變。

3.1.10 抽象基類

Pydantic模型可以與 Python 的抽象基類一起使用。

import abc
from pydantic import BaseModel


class FooBarModel(BaseModel, abc.ABC):
    a: str
    b: int

    @abc.abstractmethod
    def my_abstract_method(self):
        pass

3.1.11 字段排序

由於以下原因,字段順序是很重要的:

  • 驗證是按照字段的定義順序執行的;[字段驗證器](# 3.3 驗證器) 可以訪問前一個字段的值,但不能訪問后一個字段的值。
  • 字段順序保留在模型 [模式](# 3.5 模式) 中。
  • 字段順序保留在 [驗證錯誤](# 3.1.4 錯誤處理) 中。
  • 字段順序被 .dict() 和 .json() 等保留。

從v1.0開始,所有具有注解的字段(無論是只有注解還是具有默認值)都將位於沒有注解的字段之前。在它們各自的組中,字段按照定義時的順序保存。

from pydantic import BaseModel, ValidationError


class Model(BaseModel):
    a: int
    b = 2
    c: int = 1
    d = 0
    e: float


print(Model.__fields__.keys())
#> dict_keys(['a', 'c', 'e', 'b', 'd'])
m = Model(e=2, a=1)
print(m.dict())
#> {'a': 1, 'c': 1, 'e': 2.0, 'b': 2, 'd': 0}
try:
    Model(a='x', b='x', c='x', d='x', e='x')
except ValidationError as e:
    error_locations = [e['loc'] for e in e.errors()]

print(error_locations)
#> [('a',), ('c',), ('e',), ('b',), ('d',)]

警告

如上例所示,在同一個模型中結合使用帶注釋的字段和不帶注釋的字段會導致驚人的字段順序。(這是由於Python的限制)

因此,我們建議向所有字段添加類型注解,即使默認值將自行確定類型以確保保留字段順序。

3.1.12 必需字段

要將字段聲明為必需的,可以只使用注釋來聲明它,或者可以使用省略號(...)作為值:

from pydantic import BaseModel, Field


class Model(BaseModel):
    a: int
    b: int = ...
    c: int = Field(...)

其中 Field 指的是 [字段函數](# 3.5.1 字段定制)。

在這里,a、b 和 c 都是必需的。然而,在 b 中使用省略號將會導致在 mypy 中不能很好的工作,在v1.0中大多數情況下都應該避免使用。

3.1.12.1 具有 Optional 注解的必須字段

警告

從版本v1.2開始,僅含有注解的可空(Optional[...]Union[None, ...]Any)字段與以 ... 作為默認值的可空字段不再具有相同的含義。

在某些情況下,這可能導致v1.2不能完全向后兼容早期的v1.*版本。

如果想要指定一個可接受 None 值的必須字段,可以使用具有 ...Optional

from typing import Optional
from pydantic import BaseModel, Field, ValidationError


class Model(BaseModel):
    a: Optional[int]
    b: Optional[int] = ...
    c: Optional[int] = Field(...)


print(Model(b=1, c=2))
#> a=None b=1 c=2
try:
    Model(a=1, b=2)
except ValidationError as e:
    print(e)
    """
    1 validation error for Model
    c
      field required (type=value_error.missing)
    """

在這個模型中,a、b 和 c 都可以接受 None 作為值。但 a 是可選的,而 b 和 c 都是必需的。b 和 c 需要一個值,即使這個值是 None。

3.1.13 具有動態默認值的字段

當聲明一個帶有默認值的字段時,你也許想讓這個默認值是動態的(例如,對於每一個模型來說是不同的),則可以通過 default_factory 來實現。

Beta 版本

default_factory 參數仍處於beta版本,它是在v1.5中臨時添加到pydantic的。它可能會在未來的版本中發生重大的變化,它的簽名或者行為直到v2才會穩定。在它還處於臨時階段時,來自社區的反饋將非常有用;評論#866或者創建一個新問題。

示例用法:

from datetime import datetime
from uuid import UUID, uuid4
from pydantic import BaseModel, Field


class Model(BaseModel):
    uid: UUID = Field(default_factory=uuid4)
    updated: datetime = Field(default_factory=datetime.utcnow)


m1 = Model()
m2 = Model()
print(f'{m1.uid} != {m2.uid}')
#> 27ce808f-9293-47ac-860e-aebe5d6ffac7 != a583211f-b357-4b92-9632-cd1b3bbd2e1b
print(f'{m1.updated} != {m2.updated}')
#> 2020-10-28 20:03:32.840916 != 2020-10-28 20:03:32.840934

其中 Field 引用 [字段函數](# 3.5.1 字段定制)。

警告

default_factory 希望設置字段類型,此外,如果您希望使用 validate_all 驗證默認值,則pydantic需要調用default_factory,這可能會導致副作用!

3.1.14 私有模型屬性

如果您需要使用從模型字段中排除的內部屬性,則可以使用 PrivateAttr 來聲明:

from datetime import datetime
from random import randint

from pydantic import BaseModel, PrivateAttr


class TimeAwareModel(BaseModel):
    _processed_at: datetime = PrivateAttr(default_factory=datetime.now)
    _secret_value: str = PrivateAttr()

    def __init__(self, **data):
        super().__init__(**data)
        # this could also be done with default_factory
        self._secret_value = randint(1, 5)


m = TimeAwareModel()
print(m._processed_at)
#> 2020-10-28 20:03:33.179004
print(m._secret_value)
#> 5

私有屬性的名稱必須以下划線開始,以避免與模型字段的沖突:_attr__attr__ 這兩種形式都支持。

如果 Config.underscore_attrs_are_privateTrue,任何非 ClassVar 下划線屬性都將被當做私有屬性:

from typing import ClassVar

from pydantic import BaseModel


class Model(BaseModel):
    _class_var: ClassVar[str] = 'class var value'
    _private_attr: str = 'private attr value'

    class Config:
        underscore_attrs_are_private = True


print(Model._class_var)
#> class var value
print(Model._private_attr)
#> <member '_private_attr' of 'Model' objects>
print(Model()._private_attr)
#> private attr value

在創建類時,pydantic構造由私有屬性填充的 slots

3.1.15 將數據解析到特定的類型

Pydantic包含一個獨立的實用程序函數 parse_obj_as,可以使用它以更特別的方式應用解析邏輯來填充Pydantic模型。這個函數的行為類似於 BaseModel.parse_obj,但是可以使用任意的pydantic兼容類型。

當您希望將結果解析為BaseModel 的直接子類之外的類型時,該函數將特別有用。例如:

from typing import List

from pydantic import BaseModel, parse_obj_as


class Item(BaseModel):
    id: int
    name: str


# `item_data` could come from an API call, eg., via something like:
# item_data = requests.get('https://my-api.com/items').json()
item_data = [{'id': 1, 'name': 'My Item'}]

items = parse_obj_as(List[Item], item_data)
print(items)
#> [Item(id=1, name='My Item')]

這個函數能夠將數據解析為作為 BaseModel 字段的pydantic可以處理的任何類型。

Pydantic還包括兩個類似的獨立函數 parse_file_as 和 parse_raw_as,它們類似於 BaseModel.parse_file 和 BaseModel.parse_raw。

3.1.16 數據轉換

pydantic可以對輸入數據進行強制轉換,以使其符合模型字段類型,在某些情況下,這可能會導致信息丟失。例如:

from pydantic import BaseModel


class Model(BaseModel):
    a: int
    b: float
    c: str


print(Model(a=3.1415, b=' 2.72 ', c=123).dict())
#> {'a': 3, 'b': 2.72, 'c': '123'}

這是經過深思熟慮的決定,通常是最有用的方法。在這里可以看到關於這個問題的更長的討論。

3.1.17 模型簽名

所有pydantic模型將根據其字段生成簽名:

import inspect
from pydantic import BaseModel, Field


class FooModel(BaseModel):
    id: int
    name: str = None
    description: str = 'Foo'
    apple: int = Field(..., alias='pear')


print(inspect.signature(FooModel))
#> (*, id: int, name: str = None, description: str = 'Foo', pear: int) -> None

准確的簽名對於自省目的和像 FastAPIhypothesis 這樣的庫非常有用。

生成的簽名也將尊重定制的 __init__ 函數:

import inspect

from pydantic import BaseModel


class MyModel(BaseModel):
    id: int
    info: str = 'Foo'

    def __init__(self, id: int = 1, *, bar: str, **data) -> None:
        """My custom init!"""
        super().__init__(id=id, bar=bar, **data)


print(inspect.signature(MyModel))
#> (id: int = 1, *, bar: str, info: str = 'Foo') -> None

要將一個字段包含在簽名中,其別名或名稱必須是有效的Python標識符。pydantic傾向於使用別名而不是名稱,但如果別名不是有效的python標識符,則可以使用字段名稱。

如果字段的別名和名稱都是無效標識符,則將添加一個 **data 參數。此外,如果 Config.extra 是 Extra.allow,則 **data 參數將始終出現在簽名中。

注意

模型簽名中的類型與模型注釋中聲明的類型相同,不一定是可以實際提供給該字段的所有類型。這個問題可能會在#1055被解決后被修復。

3.2 字段類型

pydantic在可能的情況下使用[標准庫類型](# 3.2.1 標准庫類型)定義字段,從而使學習曲線更加平滑。 但是,對於許多有用的應用程序,不存在標准庫類型,因此pydantic實現了[許多常用類型](#3.2.3 Pydantic 類型)。

如果現有類型無法滿足需求,您還可以使用自定義屬性和驗證來實現[與pydantic兼容的自定義數據類型](# 3.2.7 自定義數據類型)。

3.2.1 標准庫類型

pydantic支持python標准庫中的許多常見類型。如果需要更嚴格的處理,請查看[嚴格類型](# 3.2.5 Strict類型);如果您需要對允許的值進行約束(例如,要求一個正整數),請參閱[約束類型](# 3.2.4 約束類型)。

  • bool

    有關如何驗證布爾值以及允許哪些值的詳細信息,請參閱下面的[布爾類型](# 3.2.1.6 Boolean)。

  • int

    pydantic 使用 int(v) 來強制將一個值轉換為 int 類型。請參閱[這里](# 3.1.16 數據轉換)關於數據轉換期間信息丟失的警告。

  • float

    同樣的,使用 float(v) 來將一個值強制轉換為 float 類型。

  • str

    字符串按原樣接受,int、float 和 Decimal 使用 str(v) 強制轉換,bytes 和 bytearray 使用 v.decode() 轉換,繼承自 str 的枚舉使用 v.value 轉換,所有其他類型都會導致錯誤。

  • bytes

    字節是按原格式接受的,bytearray 使用 bytes(v) 進行轉換,str 使用 v.encode() 進行轉換,int、float 和 Decimal 使用 str(v).encode() 進行轉換。

  • list

    允許 list、tuple、set、frozenset、deque 或生成器,並會轉換成列表。對於子類型約束,參見下面的 typing.List 。

  • tuple

    允許 list、tuple、set、frozenset、deque 或生成器,並會轉換成元組。對於子類型約束,參見下面的 typing.Tuple 。

  • dict

    會嘗試使用 dict(v) 來轉換為字典。對於子類型約束,參見下面的 typing.Dict 。

  • set

    允許 list、tuple、set、frozenset、deque 或生成器,並會轉換成集合。對於子類型約束,參見下面的 typing.Set 。

  • frozenset

    允許 list、tuple、set、frozenset、deque 或生成器,並會轉換成凍結集合。對於子類型約束,參見下面的 typing.FrozenSet 。

  • deque

    允許 list、tuple、set、frozenset、deque 或生成器,並會轉換成雙端隊列。對於子類型約束,參見下面的 typing.Deque 。

  • datetime.date

    有關解析和驗證的更多細節,請參見下面的[日期時間類型](# 3.2.1.5 Datetime)。

  • datetime.time

    有關解析和驗證的更多細節,請參見下面的[日期時間類型](# 3.2.1.5 Datetime)。

  • datetime.datetime

    有關解析和驗證的更多細節,請參見下面的[日期時間類型](# 3.2.1.5 Datetime)。

  • datetime.timedelta

    有關解析和驗證的更多細節,請參見下面的[日期時間類型](# 3.2.2.3 Datetime)。

  • typing.Any

    允許任何值,包括 None,因此一個 Any 字段是可選的。

  • typing.TypeVar

    根據 constraints 或 bound 約束允許的值。參見 [TypeVar](# 3.2.1.9 TypeVar)。

  • typing.Union

    有關解析和驗證的更多細節,請參見[Union](# 3.2.1.3 Union)。

  • typing.Optional

    Optional[x] 是 Union[x, None] 的快捷方式。有關解析和驗證的詳細信息,請參閱[Union](# 3.2.1.3 Union);有關可以接收 None 作為值的必需字段的詳細信息,請參閱 [必需字段](# 3.1.12 必需字段)。

  • typing.List

    有關解析和驗證的更多細節,參見 [typing中的可迭代類型](# 3.2.1.1 typing 中的可迭代類型)。

  • typing.Tuple

    有關解析和驗證的更多細節,參見 [typing中的可迭代類型。](# 3.2.1.1 typing 中的可迭代類型)

  • typing.Dict

    有關解析和驗證的更多細節,參見 [typing中的可迭代類型。](# 3.2.1.1 typing 中的可迭代類型)

  • typing.Set

    有關解析和驗證的更多細節,參見 [typing中的可迭代類型。](# 3.2.1.1 typing 中的可迭代類型)

  • typing.FrozenSet

    有關解析和驗證的更多細節,參見 [typing中的可迭代類型。](# 3.2.1.1 typing 中的可迭代類型)

  • typing.Deque

    有關解析和驗證的更多細節,參見 [typing中的可迭代類型。](# 3.2.1.1 typing 中的可迭代類型)

  • typing.Sequence

    有關解析和驗證的更多細節,參見 [typing中的可迭代類型。](# 3.2.1.1 typing 中的可迭代類型)

  • typing.Iterable

    這是為不應該被消耗的可迭代對象保留的。有關解析和驗證的更多細節,請參閱下面的[無限生成器](# 3.2.1.2 無限生成器)。

  • typing.Type

    有關解析和驗證的更多細節,參見 [Type](# 3.2.1.8 Type)。

  • typing.Callable

    有關解析和驗證的更多細節,參見 [Callable](# 3.2.1.7 Callable)。

  • typing.Pattern

    將會導致輸入值被傳遞到 re.compile(v) 以創建一個正則表達式模式。

  • ipaddress.IPv4Address

    通過將值傳遞給 IPv4Address(v),簡單地使用類型本身進行驗證;有關其他自定義IP地址類型,請參閱[Pydantic類型](# 3.2.3 Pydantic 類型)。

  • ipaddress.IPv4Interface

    通過將值傳遞給 IPv4Interface(v),簡單地使用類型本身進行驗證;有關其他自定義IP地址類型,請參閱[Pydantic類型](3.2.3 Pydantic 類型)。

  • ipaddress.IPv4Network

    通過將值傳遞給 IPv4Network(v),簡單地使用類型本身進行驗證;有關其他自定義IP地址類型,請參閱[Pydantic類型](3.2.3 Pydantic 類型)。

  • ipaddress.IPv6Address

    通過將值傳遞給 IPv6Address(v),簡單地使用類型本身進行驗證;有關其他自定義IP地址類型,請參閱[Pydantic類型](3.2.3 Pydantic 類型)。

  • ipaddress.IPv6Interface

    通過將值傳遞給 IPv6Interface(v),簡單地使用類型本身進行驗證;有關其他自定義IP地址類型,請參閱[Pydantic類型](3.2.3 Pydantic 類型)。

  • ipaddress.IPv6.Network

    通過將值傳遞給 IPv6Network(v),簡單地使用類型本身進行驗證;有關其他自定義IP地址類型,請參閱[Pydantic類型](3.2.3 Pydantic 類型)。

  • enum.Enum

    檢查值是有效的 Enum 實例。

  • subclass of enum.Enum

    檢查值是有效的枚舉成員。更多細節,參見 [Enum 和 Choice](# 3.2.1.4 Enum 和 Choice)。

  • enum.IntEnum

    檢查值是有效的 IntEnum 實例。

  • subclass of enum.IntEnum

    檢查值是有效的整數枚舉成員。更多細節,參見 [Enum 和 Choice](# 3.2.1.4 Enum 和 Choice)。

  • decimal.Decimal

    pydantic 會嘗試將值轉換為字符串,然后將那個字符串傳遞個 Decimal(v)。

  • pathlib.Path

    通過將值傳遞給 Path(v) ,簡單地使用類型本身進行驗證。對於其他更嚴格的路徑類型,參見 [Pydantic類型](3.2.3 Pydantic 類型)。

  • uuid.UUID

    字符串和字節(轉換為字符串)被傳遞到 UUID(v),對於 bytes 和 bytearray ,回退到 UUID(bytes=v);有關其他更嚴格的UUID類型,請參見 [Pydantic類型](3.2.3 Pydantic 類型)。

  • ByteSize

    將帶有單位的字節字符串轉換為字節。

3.2.1.1 typing 中的可迭代類型

Pydantic使用在PEP 484中定義的標准庫 typing 來定義復雜對象。

from typing import (
    Deque, Dict, FrozenSet, 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
    frozen_set: FrozenSet[int] = 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

    deque: Deque[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_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_tuple=[1, 2, 3, 4]).simple_tuple)
#> (1, 2, 3, 4)
print(Model(tuple_of_different_types=[4, 3, 2, 1]).tuple_of_different_types)
#> (4, 3.0, '2', True)

print(Model(sequence_of_ints=[1, 2, 3, 4]).sequence_of_ints)
#> [1, 2, 3, 4]
print(Model(sequence_of_ints=(1, 2, 3, 4)).sequence_of_ints)
#> (1, 2, 3, 4)

print(Model(deque=[1, 2, 3]).deque)
#> deque([1, 2, 3])

3.2.1.2 無限生成器

如果你有一個生成器,您可以使用上面所述的 Sequence。在這種情況下,生成器將被消耗並作為列表存儲在模型中,其中的值將使用Sequence 的子類型進行驗證(例如 Sequence[int] 中的 int)。

但是如果您有一個生成器,並且不想其被消耗,例如無限生成器或遠程數據加載器,則可以用 Iterable 定義其類型:

from typing import Iterable
from pydantic import BaseModel


class Model(BaseModel):
    infinite: Iterable[int]


def infinite_ints():
    i = 0
    while True:
        yield i
        i += 1


m = Model(infinite=infinite_ints())
print(m)
#> infinite=<generator object infinite_ints at 0x7fcb8ff44580>

for i in m.infinite:
    print(i)
    #> 0
    #> 1
    #> 2
    #> 3
    #> 4
    #> 5
    #> 6
    #> 7
    #> 8
    #> 9
    #> 10
    if i == 10:
        break

警告

對於類型注解為 Iterable 的字段,只執行簡單的檢查,以確定其是可迭代的,並且該可迭代對象不會被消費。

不執行對它們的值的驗證,因為如果不消費可迭代對象,就無法進行驗證。

提示

如果您想要驗證無限生成器的值,您可以創建一個單獨的模型,並在使用生成器時使用它,並根據需要報告驗證錯誤。

pydantic無法為您自動驗證這些值,因為如果要進行驗證,則必須消費無限生成器。

3.2.1.2.1 驗證無限生成器的第一個值

您可以創建一個驗證器來驗證無限生成器中的第一個值,但仍然不完全使用它。

import itertools
from typing import Iterable
from pydantic import BaseModel, validator, ValidationError
from pydantic.fields import ModelField


class Model(BaseModel):
    infinite: Iterable[int]

    @validator('infinite')
    # You don't need to add the "ModelField", but it will help your
    # editor give you completion and catch errors
    def infinite_first_int(cls, iterable, field: ModelField):
        first_value = next(iterable)
        if field.sub_fields:
            # The Iterable had a parameter type, in this case it's int
            # We use it to validate the first value
            sub_field = field.sub_fields[0]
            v, error = sub_field.validate(first_value, {}, loc='first_value')
            if error:
                raise ValidationError([error], cls)
        # This creates a new generator that returns the first value and then
        # the rest of the values from the (already started) iterable
        return itertools.chain([first_value], iterable)


def infinite_ints():
    i = 0
    while True:
        yield i
        i += 1


m = Model(infinite=infinite_ints())
print(m)
#> infinite=<itertools.chain object at 0x7fcb8ff18c10>


def infinite_strs():
    while True:
        for letter in 'allthesingleladies':
            yield letter


try:
    Model(infinite=infinite_strs())
except ValidationError as e:
    print(e)
    """
    1 validation error for Model
    infinite -> first_value
      value is not a valid integer (type=type_error.integer)
    """

3.2.1.3 Union

Union 類型允許一個模型屬性接受不同的類型,例如:

警告

此腳本是完整的,應該 “按原樣” 運行。然而,它可能不能反映所期望的行為;見下文。

from uuid import UUID
from typing import Union
from pydantic import BaseModel


class User(BaseModel):
    id: Union[int, str, UUID]
    name: str


user_01 = User(id=123, name='John Doe')
print(user_01)
#> id=123 name='John Doe'
print(user_01.id)
#> 123
user_02 = User(id='1234', name='John Doe')
print(user_02)
#> id=1234 name='John Doe'
print(user_02.id)
#> 1234
user_03_uuid = UUID('cf57432e-809e-4353-adbd-9d5c0d733868')
user_03 = User(id=user_03_uuid, name='John Doe')
print(user_03)
#> id=275603287559914445491632874575877060712 name='John Doe'
print(user_03.id)
#> 275603287559914445491632874575877060712
print(user_03_uuid.int)
#> 275603287559914445491632874575877060712

但是,如上面所示,pydantic將嘗試 “匹配” Union 下定義的任何類型,並將使用第一個匹配的類型。在上面的示例中,user_03 的 id 被定義為 uuid.UUID 類 (在屬性的 Union 注解下定義),但由於 uuid.UUID 可以編組為一個 int,因此它選擇與 int 類型匹配,而不考慮其他類型。

因此,建議在定義 Union 注解時,首先包含最特定的類型,然后再包含不那么特定的類型。在上面的例子中,UUID 類應該在 int 和str 類之前,以排除意外的表示:

from uuid import UUID
from typing import Union
from pydantic import BaseModel


class User(BaseModel):
    id: Union[UUID, int, str]
    name: str


user_03_uuid = UUID('cf57432e-809e-4353-adbd-9d5c0d733868')
user_03 = User(id=user_03_uuid, name='John Doe')
print(user_03)
#> id=UUID('cf57432e-809e-4353-adbd-9d5c0d733868') name='John Doe'
print(user_03.id)
#> cf57432e-809e-4353-adbd-9d5c0d733868
print(user_03_uuid.int)
#> 275603287559914445491632874575877060712

提示

類型 Optional[x]Union[x, None] 的快捷方式。

Optional[x] 也可以用於指定一個必需字段也可以接受 None 作為其值。

更多細節,參見 [必需字段](# 3.1.12 必需字段)。

3.2.1.4 Enum 和 Choice

Pydantic使用 Python 的標准 enum 類來定義選擇:

from enum import Enum, IntEnum

from pydantic import BaseModel, ValidationError


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())
#> fruit=<FruitEnum.pear: 'pear'> tool=<ToolEnum.spanner: 1>
print(CookingModel(tool=2, fruit='banana'))
#> fruit=<FruitEnum.banana: 'banana'> tool=<ToolEnum.wrench: 2>
try:
    CookingModel(fruit='other')
except ValidationError as e:
    print(e)
    """
    1 validation error for CookingModel
    fruit
      value is not a valid enumeration member; permitted: 'pear', 'banana'
    (type=type_error.enum; enum_values=[<FruitEnum.pear: 'pear'>,
    <FruitEnum.banana: 'banana'>])
    """

3.2.1.5 Datetime

Pydantic 支持下面一些 datetime 了類型:

  • datetime 字段可以是:
    • datetime:存在 datetime 對象時
    • int 或 float:假定為 Unix 時間時。例如,自1970年1月1日以來的秒數 (如果 >= -2e10 或 <= 2e10) 或毫秒數 (如果 < -2e10 或 > 2e10)。
    • str:下面的格式可用時:
      • YYYY-MM-DD[T]HH:MM[:SS[.ffffff]][Z or [±]HH[:]MM]]]
      • 作為字符串的 int 或 floats (假定為Unix time)
  • date 字段可以是:
    • date:存在 date 對象時
    • int 或float: 參見 datetime
    • str:下面的格式可用時:
      • YYYY-MM-DD
      • int 或 float 參見 datetime
  • time 字段可以是:
    • time:存在 time 對象時
    • str:下面的格式可用時:
      • HH:MM[:SS[.ffffff]][Z or [±]HH[:]MM]]]
  • timedelta 字段可以是:
    • timedelta:存在 timedelta 對象時
    • int 或 float:假定為"秒"
    • str:下面的格式可用時:
      • [-][DD ][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',
)

print(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),
}
"""

3.2.1.6 Boolean

警告

從v1.0版本開始,解析 bool 字段的邏輯發生了變化。

在v1.0之前,bool 解析從來不會失敗,會導致一些意想不到的結果。新的邏輯如下所述。

如果不是下面的值之一,標准的 bool 字段將會引發 ValidationError :

  • 有效的布爾值(例如 True 或 False);
  • 整數 0 或 1;
  • 一個字符串,在轉換為小寫后,等於 0、off、f、false、n、no、1、on、t、true、y、yes 其中之一;
  • 一個 bytes,在解碼為 str 后,依據上一個規則有效。

注意

如果想要更嚴格的布爾邏輯(例如,只允許 True 或 False 的字段),可以使用 StrictBool。

下面的腳本模擬了這些行為:

from pydantic import BaseModel, ValidationError


class BooleanModel(BaseModel):
    bool_value: bool


print(BooleanModel(bool_value=False))
#> bool_value=False
print(BooleanModel(bool_value='False'))
#> bool_value=False
try:
    BooleanModel(bool_value=[])
except ValidationError as e:
    print(str(e))
    """
    1 validation error for BooleanModel
    bool_value
      value could not be parsed to a boolean (type=type_error.bool)
    """

3.2.1.7 Callable

字段也可以是 Callable 類型:

from typing import Callable
from pydantic import BaseModel


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


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

警告

類型為 Callble 的字段只執行簡單的檢查以確定參數是否可調用;不執行參數、參數類型或返回類型的驗證。

3.2.1.8 Type

Pydantic支持使用 Type[T] 來指定一個字段只可以接受類 T 的子類(而不是實例)。

from typing import Type

from pydantic import BaseModel
from pydantic import ValidationError


class Foo:
    pass


class Bar(Foo):
    pass


class Other:
    pass


class SimpleModel(BaseModel):
    just_subclasses: Type[Foo]


SimpleModel(just_subclasses=Foo)
SimpleModel(just_subclasses=Bar)
try:
    SimpleModel(just_subclasses=Other)
except ValidationError as e:
    print(e)
    """
    1 validation error for SimpleModel
    just_subclasses
      subclass of Foo expected (type=type_error.subclass; expected_class=Foo)
    """

也可以使用 Type 來指定允許任意類:

from typing import Type

from pydantic import BaseModel, ValidationError


class Foo:
    pass


class LenientSimpleModel(BaseModel):
    any_class_goes: Type


LenientSimpleModel(any_class_goes=int)
LenientSimpleModel(any_class_goes=Foo)
try:
    LenientSimpleModel(any_class_goes=Foo())
except ValidationError as e:
    print(e)
    """
    1 validation error for LenientSimpleModel
    any_class_goes
      a class is expected (type=type_error.class)
    """

3.2.1.9 TypeVar

TypeVar 支持無約束(unconstrained)、有約束(constrained) 或有綁定(with bound)。

from typing import TypeVar
from pydantic import BaseModel

Foobar = TypeVar('Foobar')
BoundFloat = TypeVar('BoundFloat', bound=float)
IntStr = TypeVar('IntStr', int, str)


class Model(BaseModel):
    a: Foobar  # equivalent of ": Any"
    b: BoundFloat  # equivalent of ": float"
    c: IntStr  # equivalent of ": Union[int, str]"


print(Model(a=[1], b=4.2, c='x'))
#> a=[1] b=4.2 c='x'

# a may be None and is therefore optional
print(Model(b=1, c=1))
#> a=None b=1.0 c=1

3.2.2 Literal類型

注意:

這是自python 3.8以來的python標准庫的一個新特性;在python 3.8之前,則需要安裝 typing-extensions。

pydantic支持使用 typing.Literal (或者在 Python3.8之前,使用 typing_extensions.Literal) 作為一種輕量級的方式來指定一個字段只能接受特定的字面量:

from typing import Literal

from pydantic import BaseModel, ValidationError


class Pie(BaseModel):
    flavor: Literal['apple', 'pumpkin']


Pie(flavor='apple')
Pie(flavor='pumpkin')
try:
    Pie(flavor='cherry')
except ValidationError as e:
    print(str(e))
    """
    1 validation error for Pie
    flavor
      unexpected value; permitted: 'apple', 'pumpkin'
    (type=value_error.const; given=cherry; permitted=('apple', 'pumpkin'))
    """

這個字段類型的一個好處是,它可以用來檢查一個或多個特定值是否相等,而不需要聲明自定義驗證器:

from typing import ClassVar, List, Union

from typing import Literal

from pydantic import BaseModel, ValidationError


class Cake(BaseModel):
    kind: Literal['cake']
    required_utensils: ClassVar[List[str]] = ['fork', 'knife']


class IceCream(BaseModel):
    kind: Literal['icecream']
    required_utensils: ClassVar[List[str]] = ['spoon']


class Meal(BaseModel):
    dessert: Union[Cake, IceCream]


print(type(Meal(dessert={'kind': 'cake'}).dessert).__name__)
#> Cake
print(type(Meal(dessert={'kind': 'icecream'}).dessert).__name__)
#> IceCream
try:
    Meal(dessert={'kind': 'pie'})
except ValidationError as e:
    print(str(e))
    """
    2 validation errors for Meal
    dessert -> kind
      unexpected value; permitted: 'cake' (type=value_error.const; given=pie;
    permitted=('cake',))
    dessert -> kind
      unexpected value; permitted: 'icecream' (type=value_error.const;
    given=pie; permitted=('icecream',))
    """

在帶注解的 Union 中使用適當的排序,你可以使用它來解析特定的降序類型(types of descreasing):

from typing import Optional, Union

from typing import Literal

from pydantic import BaseModel


class Dessert(BaseModel):
    kind: str


class Pie(Dessert):
    kind: Literal['pie']
    flavor: Optional[str]


class ApplePie(Pie):
    flavor: Literal['apple']


class PumpkinPie(Pie):
    flavor: Literal['pumpkin']


class Meal(BaseModel):
    dessert: Union[ApplePie, PumpkinPie, Pie, Dessert]


print(type(Meal(dessert={'kind': 'pie', 'flavor': 'apple'}).dessert).__name__)
#> ApplePie
print(type(Meal(dessert={'kind': 'pie', 'flavor': 'pumpkin'}).dessert).__name__)
#> PumpkinPie
print(type(Meal(dessert={'kind': 'pie'}).dessert).__name__)
#> Pie
print(type(Meal(dessert={'kind': 'cake'}).dessert).__name__)
#> Dessert

3.2.3 Pydantic 類型

Pydantic 也提供了許多其他有用的類型:

  • FilePath

    與 Path 類似,但路徑必須存在並且必須是文件。

  • DirectoryPath

    與 Path 類似,但路徑必須存在並且必須是目錄。

  • EmailStr

    需要安裝 email-validator;輸入字符串必須是有效的 email地址,輸出是一個簡單字符串。

  • NameEmail

    需要安裝 email-validator;輸入字符串必須是有效的 email地址或是 Fred Bloggs fred.bloggs@example.com 這樣的格式,輸出是一個 NameEmail 對象,該對象有兩個屬性:name 和 email。對於 Fred Bloggs fred.bloggs@example.com,名稱將會是 Fred Bloggs。對於 fred.bloggs@example.com,名稱將會是 fred.bloggs。

  • PyObject

    需要一個可調用對象或一個包含 . 的表示導入路徑的字符串。如果提供的是一個字符串,則從該字符串所表示的導入路徑加載最后一個 . 右邊的子串所表示的可導入Python 對象。 例如,如果提供了 'math.cos',則結果字段值將為函數 cos。如果提供了 pydantic.dataclasses.dataclass,則結果字段值將為 dataclass。

  • Color

    用於解析 HTML 和 CSS 顏色。參見 [Color類型](3.2.3.2 Color 類型)。

  • Json

    一個特殊的類型包裝器,在解析之前加載JSON; 參見 [JSON類型](# 3.2.3.4 Json 類型)。

  • PaymentCardNumber

    對支付卡進行解析和驗證;參見 [PaymentCardNumber 類型](3.2.3.5 PaymentCardNumber)。

  • AnyUrl

    任意 URL。參見 [URL](# 3.2.3.1 URL)

  • AnyHttpUrl

    一個 HTTP URL。參見 [URL](# 3.2.3.1 URL)

  • HttpUrl

    更嚴格的HTTP URL。參見 [URL](# 3.2.3.1 URL)。

  • PostgresDsn

    Postgres DSN 樣式的 URL。參見 [URL](# 3.2.3.1 URL)。

  • RedisDsn

    Redis DSN 樣式的 URL。參見 [URL](# 3.2.3.1 URL)。

  • stricturl

    用於任意URL約束的類型方法。參見 [URL](# 3.2.3.1 URL)。

  • UUID1

    需要類型1的有效UUID。參見[上面](# 3.2.1 標准庫類型)的UUID。

  • UUID3

    需要類型3的有效UUID。參見[上面](# 3.2.1 標准庫類型)的UUID。

  • UUID4

    需要類型4的有效UUID。參見[上面](# 3.2.1 標准庫類型)的UUID。

  • UUID5

    需要類型5的有效UUID。參見[上面](# 3.2.1 標准庫類型)的UUID。

  • SecretBytes

    值部分(partially)保密的字節。參見 [Secret](# 3.2.3.3 Secret 類型)。

  • SecretStr

    值部分(partially)保密的字符串。參見 [Secret](# 3.2.3.3 Secret 類型)。

  • IPvAnyAddress

    允許一個 IPv4Address 或一個 IPv6Address。

  • IPvAnyInterface

    允許一個 IPv4Interface 或一個 IPv6Interface。

  • IPvAnyNetwork

    允許一個 IPv4Network 或一個 IPv6Network。

  • NegativeFloat

    允許負的浮點數; 使用標准的 float 解析,然后檢查該值是否小於0; 請參閱 [約束類型](3.2.4 約束類型)。

  • NegativeInt

    允許負的整數; 使用標准的 int 解析,然后檢查該值是否小於0; 請參閱 [約束類型](3.2.4 約束類型)。

  • PositiveFloat

    允許正的浮點數; 使用標准的 float 解析,然后檢查該值是否大於0; 請參閱 [約束類型](3.2.4 約束類型)。

  • PosiviteInt

    允許正的整數; 使用標准的 int 解析,然后檢查該值是否大於0; 請參閱 [約束類型](3.2.4 約束類型)。

  • conbytes

    用於約束 bytes 的類型方法。請參閱 [約束類型](3.2.4 約束類型)。

  • condecimal

    用於約束 Decimal 的類型方法。請參閱 [約束類型](3.2.4 約束類型)。

  • confloat

    用於約束 float 的類型方法。請參閱 [約束類型](3.2.4 約束類型)。

  • conint

    用於約束 int 的類型方法。請參閱 [約束類型](3.2.4 約束類型)。

  • conlist

    用於約束 list 的類型方法。請參閱 [約束類型](3.2.4 約束類型)。

  • conset

    用於約束 set 的類型方法。請參閱 [約束類型](3.2.4 約束類型)。

  • constr

    用於約束 str 的類型方法。請參閱 [約束類型](3.2.4 約束類型)。

3.2.3.1 URL

對於 URI/URL驗證,可以使用以下類型:

  • AnyUrl

    允許任意方案,不需要TLD。

  • AnyHttpUrl

    http 或 https 方案,不需要TLD。

  • HttpUrl

    http 或 https 方案,需要TLD,最大長度 2083。

  • PostgresDsn

    postgres 或 postgresql 方案,需要用戶信息,不需要 TLD。

  • RedisDsn

    redis 方案,不需要用戶信息。不需要 TLD (更改:從v1.6開始不需要用戶信息)。

  • stricturl

帶有如下關鍵字參數的方法:

  • strip_whitespace: bool = True
  • min_length: int = 1
  • max_length: int = 2 ** 16
  • tld_required: bool = True
  • allowed_schemes: Optional[Set[str]] = None

當提供了無效的 URL 時,上面這些類型(都繼承自AnyUrl)都會嘗試給出描述性的錯誤。

from pydantic import BaseModel, HttpUrl, ValidationError


class MyModel(BaseModel):
    url: HttpUrl


m = MyModel(url='http://www.example.com')
print(m.url)
#> http://www.example.com
try:
    MyModel(url='ftp://invalid.url')
except ValidationError as e:
    print(e)
    """
    1 validation error for MyModel
    url
      URL scheme not permitted (type=value_error.url.scheme;
    allowed_schemes={'https', 'http'})
    """

try:
    MyModel(url='not a url')
except ValidationError as e:
    print(e)
    """
    1 validation error for MyModel
    url
      invalid or missing URL scheme (type=value_error.url.scheme)
    """

如果需要自定義的 URI/URL 類型,則可以按照與上面定義的類型類似的方式來創建它。

3.2.3.1.1 URL 屬性

假定輸入 URL 為 http://samuel:pass@example.com:8000/the/path/?query=here#fragment=is;this=bit,上面的類型導出如下的屬性:

  • scheme

    總是設置。URL 方案( 上面的 http)

  • host

    總是設置。URL 主機 (上面的 example.com)

  • host_type

    總是設置。描述主機類型,或

  • domain

    例如,example.com

  • int_domain

    國際域名,參見[下面](# 3.2.4.1.2 國際域名),例如 exampl£e.org

  • ipv4

    一個 IP V4 地址,例如 127.0.0.1 或

  • ipv6

    一個IP V6 地址,例如 2001:db8:ff00:42

  • user

    可選。如果包括的話,則表示用戶名 (上面的 samuel)

  • password

    可選。如果包括的話,則表示密碼 (上面的 pass)

  • tld

    可選。頂級域名 (上面的 com),注意:這對於任何兩級域都是錯誤的,例如 “co.uk”。如果您需要完整的TLD驗證,則需要實施自己的TLD列表。

  • port

    可選。表示端口(上面的 8000)

  • path

    可選。表示路徑(上面的 /the/path/)

  • query

    可選。表示 URL 查詢 (也叫查詢字符串或 GET 參數) (上面的 query=here)

  • fragment

    可選。片段 (上面的 fragment=is;this=bit)

如果需要進一步的驗證,驗證器可以使用這些屬性來強制執行特定的行為:

from pydantic import BaseModel, HttpUrl, PostgresDsn, ValidationError, validator


class MyModel(BaseModel):
    url: HttpUrl


m = MyModel(url='http://www.example.com')

# the repr() method for a url will display all properties of the url
print(repr(m.url))
#> HttpUrl('http://www.example.com', scheme='http', host='www.example.com',
#> tld='com', host_type='domain')
print(m.url.scheme)
#> http
print(m.url.host)
#> www.example.com
print(m.url.host_type)
#> domain
print(m.url.port)
#> None


class MyDatabaseModel(BaseModel):
    db: PostgresDsn

    @validator('db')
    def check_db_name(cls, v):
        assert v.path and len(v.path) > 1, 'database must be provided'
        return v


m = MyDatabaseModel(db='postgres://user:pass@localhost:5432/foobar')
print(m.db)
#> postgres://user:pass@localhost:5432/foobar

try:
    MyDatabaseModel(db='postgres://user:pass@localhost:5432')
except ValidationError as e:
    print(e)
    """
    1 validation error for MyDatabaseModel
    db
      database must be provided (type=assertion_error)
    """

3.2.3.1.2 國際域名

“國際域名” (例如,主機或TLD包含非ASCII字符的URL) 將通過punycode進行編碼(請參見本文,以了解其重要性的詳細說明):

from pydantic import BaseModel, HttpUrl


class MyModel(BaseModel):
    url: HttpUrl


m1 = MyModel(url='http://puny£code.com')
print(m1.url)
#> http://xn--punycode-eja.com
print(m1.url.host_type)
#> int_domain
m2 = MyModel(url='https://www.аррӏе.com/')
print(m2.url)
#> https://www.xn--80ak6aa92e.com/
print(m2.url.host_type)
#> int_domain
m3 = MyModel(url='https://www.example.珠寶/')
print(m3.url)
#> https://www.example.xn--pbt977c/
print(m3.url.host_type)
#> int_domain

3.2.3.1.3 主機名中的下划線

警告

在Pydantic中,除TLD之外,域的所有部分都允許使用下划線。 從技術上講,這可能是錯誤的——理論上,主機名不能帶有下划線,而子域可以。

要解釋這個,請考慮下面兩種情況:

exam_ple.co.uk 主機名是 exam_ple,這是不被允許的,因為它包含一個下划線。
foo_bar.example.com 主機名是 example,這是被允許的,因為下划線在子域中。
如果沒有詳盡的TLD列表,就不可能在這兩者之間進行區分。 因此,可以使用下划線,但是如果需要,您始終可以在驗證器中進行進一步的驗證。

另外,Chrome,Firefox和Safari當前都接受 http://exam_ple.com 作為URL,因此我們是一家優秀(至少是大型)公司。

3.2.3.2 Color 類型

您可以根據CSS3規范使用 Color 數據類型存儲顏色。 可以通過以下方式定義顏色:

  • 名稱 (例如 "Black"、"azure" )
  • 十六進制值 (例如 "0x000","#FFFFFF","7fffd4" )
  • RGB/RGBA 元組 (例如 (255, 255, 255),(255, 255, 255, 0.5) )
  • RGB/RGBA 字符串 (例如 "rgb(255, 255, 255)","rgba(255, 255, 255, 0.5)" )
  • HSL 字符串 (例如 "hsl(270, 60%, 70%)","hsl(270, 60%, 70%, .5)" )
from pydantic import BaseModel, ValidationError
from pydantic.color import Color

c = Color('ff00ff')
print(c.as_named())
#> magenta
print(c.as_hex())
#> #f0f
c2 = Color('green')
print(c2.as_rgb_tuple())
#> (0, 128, 0)
print(c2.original())
#> green
print(repr(Color('hsl(180, 100%, 50%)')))
#> Color('cyan', rgb=(0, 255, 255))


class Model(BaseModel):
    color: Color


print(Model(color='purple'))
#> color=Color('purple', rgb=(128, 0, 128))
try:
    Model(color='hello')
except ValidationError as e:
    print(e)
    """
    1 validation error for Model
    color
      value is not a valid color: string not recognised as a valid color
    (type=value_error.color; reason=string not recognised as a 

Color 有下面一些方法:

  • original

    傳遞給 Color 的原始字符串或元組。

  • as_named

    返回一個命名的CSS3的顏色;如果設置了Alpha通道或不存在這種顏色,則會失敗。如果提供 fallback=True,則回退到 as_hex。

  • as_hex

    返回格式為 #fff 或 #ffffff 的字符串; 如果設置了Alpha通道,則將包含4(或8)個十六進制值,例如 #7f33cc26 。

  • as_rgb

    如果設置了Alpha通道,則返回格式為 rgb( , , ) 或 rgba( , , , ) 的字符串。

  • as_rgb_tuple

    以RGB(a) 格式返回3元組或4元組。 alpha 關鍵字參數可用於定義是否應包含alpha通道; 選項:True——始終包含,False——從不包含,None(默認值)——如果設置,則包含。

  • as_hsl

    hsl( , <saturation %>, <lightness %>) 格式的字符串。如果設置了 alpha 通道,則為 hsl( , <saturation %>, <lightness %>, )。

  • as_hsl_tuple

    以HSL(a)格式返回3元組或4元組。 alpha 關鍵字參數可用於定義是否應包含alpha通道; 選項:True——始終包含,False——從不包含,None(默認值)——如果設置,則包含。

Color 的 str 方法返回 self.as_named(fallback=True)。

注意

as_hsl* 是指html和世界上大多數地方使用的色相,飽和度和亮度 “HSL”,而不是Python的 colorsys 中使用的 “HLS”。

3.2.3.3 Secret 類型

您可以使用 SecretStr 和 SecretBytes 數據類型來存儲不想在日志記錄或回溯中可見的敏感信息。SecretStr 和 SecretBytes 可以冪等初始化,也可以分別使用 str 或 bytes 字面量初始化。 在轉換為json時,SecretStr 和 SecretBytes 的格式將為 '**********' 或 ''。

from pydantic import BaseModel, SecretStr, SecretBytes, ValidationError


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


sm = SimpleModel(password='IAmSensitive', password_bytes=b'IAmSensitiveBytes')

# Standard access methods will not display the secret
print(sm)
#> password=SecretStr('**********') password_bytes=SecretBytes(b'**********')
print(sm.password)
#> **********
print(sm.dict())
"""
{
    'password': SecretStr('**********'),
    'password_bytes': SecretBytes(b'**********'),
}
"""
print(sm.json())
#> {"password": "**********", "password_bytes": "**********"}

# Use get_secret_value method to see the secret's content.
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 for SimpleModel
    password
      str type expected (type=type_error.str)
    password_bytes
      byte type expected (type=type_error.bytes)
    """


# If you want the secret to be dumped as plain-text using the json method,
# you can use json_encoders in the Config class.
class SimpleModelDumpable(BaseModel):
    password: SecretStr
    password_bytes: SecretBytes

    class Config:
        json_encoders = {
            SecretStr: lambda v: v.get_secret_value() if v else None,
            SecretBytes: lambda v: v.get_secret_value() if v else None,
        }


sm2 = SimpleModelDumpable(
    password='IAmSensitive', password_bytes=b'IAmSensitiveBytes'
)

# Standard access methods will not display the secret
print(sm2)
#> password=SecretStr('**********') password_bytes=SecretBytes(b'**********')
print(sm2.password)
#> **********
print(sm2.dict())
"""
{
    'password': SecretStr('**********'),
    'password_bytes': SecretBytes(b'**********'),
}
"""

# But the json method will
print(sm2.json())
#> {"password": "IAmSensitive", "password_bytes": "IAmSensitiveBytes"}

3.2.3.4 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]]


print(SimpleJsonModel(json_obj='{"b": 1}'))
#> json_obj={'b': 1}
print(ComplexJsonModel(json_obj='[1, 2, 3]'))
#> json_obj=[1, 2, 3]
try:
    ComplexJsonModel(json_obj=12)
except ValidationError as e:
    print(e)
    """
    1 validation error for ComplexJsonModel
    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 for ComplexJsonModel
    json_obj
      Invalid JSON (type=value_error.json)
    """

try:
    ComplexJsonModel(json_obj='["a", "b"]')
except ValidationError as e:
    print(e)
    """
    2 validation errors for ComplexJsonModel
    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)
    """

3.2.3.5 PaymentCardNumber

PaymentCardNumber 類型用於驗證 支付卡 (例如貸記卡或信用卡)。

from datetime import date

from pydantic import BaseModel
from pydantic.types import PaymentCardBrand, PaymentCardNumber, constr


class Card(BaseModel):
    name: constr(strip_whitespace=True, min_length=1)
    number: PaymentCardNumber
    exp: date

    @property
    def brand(self) -> PaymentCardBrand:
        return self.number.brand

    @property
    def expired(self) -> bool:
        return self.exp < date.today()


card = Card(
    name='Georg Wilhelm Friedrich Hegel',
    number='4000000000000002',
    exp=date(2023, 9, 30),
)

assert card.number.brand == PaymentCardBrand.visa
assert card.number.bin == '400000'
assert card.number.last4 == '0002'
assert card.number.masked == '400000******0002'

基於BIN,PaymentCardBrand 可以是以下之一:

  • PaymentCardBrand.amex
  • PaymentCardBrand.mastercard
  • PaymentCardBrand.visa
  • PaymentCardBrand.other

實際驗證驗證卡號為:

  • 僅含數字的字符串
  • luhn 有效
  • 如果是 Amex、Mastercard 或 Visa,基於 BIN 長度正確;對於所有其他品牌,則為12至19位數字。

3.2.4 約束類型

可以使用 con* 類型函數來約束許多常見類型的值:

from decimal import Decimal

from pydantic import (
    BaseModel,
    NegativeFloat,
    NegativeInt,
    PositiveFloat,
    PositiveInt,
    conbytes,
    condecimal,
    confloat,
    conint,
    conlist,
    conset,
    constr,
    Field,
)


class Model(BaseModel):
    short_bytes: conbytes(min_length=2, max_length=10)
    strip_bytes: conbytes(strip_whitespace=True)

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

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

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

    short_list: conlist(int, min_items=1, max_items=4)
    short_set: conset(int, min_items=1, max_items=4)

    decimal_positive: condecimal(gt=0)
    decimal_negative: condecimal(lt=0)
    decimal_max_digits_and_places: condecimal(max_digits=2, decimal_places=2)
    mod_decimal: condecimal(multiple_of=Decimal('0.25'))

    bigger_int: int = Field(..., gt=10000)

其中,Field 引用 [字段函數](# 3.5.1 字段定制)。

3.2.5 Strict類型

您可以使用 StrictStr,StrictInt,StrictFloat 和 StrictBool 類型來防止兼容類型強制轉換。 僅當已驗證的值屬於相應類型或該類型的子類型時,這些類型才會通過驗證。 此行為也通過 ConstrainedStr,ConstrainedFloat 和 ConstrainedInt 類的 strict 字段公開,並且可以與許多復雜的驗證規則結合使用。

以下警告適用:

  • StrictInt (和 ConstrainedInt 的strict 選項) 不會接受 bool 類型,即使在 Python 中 bool 是 int 的子類。其他子類則會被接受。
  • StrictFloat (和 ConstrainedFloat 的 strict 選項) 不會接受 int。
from pydantic import BaseModel, StrictBool, StrictInt, ValidationError, confloat


class StrictIntModel(BaseModel):
    strict_int: StrictInt


try:
    StrictIntModel(strict_int=3.14159)
except ValidationError as e:
    print(e)
    """
    1 validation error for StrictIntModel
    strict_int
      value is not a valid integer (type=type_error.integer)
    """


class ConstrainedFloatModel(BaseModel):
    constrained_float: confloat(strict=True, ge=0.0)


try:
    ConstrainedFloatModel(constrained_float=3)
except ValidationError as e:
    print(e)
    """
    1 validation error for ConstrainedFloatModel
    constrained_float
      value is not a valid float (type=type_error.float)
    """

try:
    ConstrainedFloatModel(constrained_float=-1.23)
except ValidationError as e:
    print(e)
    """
    1 validation error for ConstrainedFloatModel
    constrained_float
      ensure this value is greater than or equal to 0.0
    (type=value_error.number.not_ge; limit_value=0.0)
    """


class StrictBoolModel(BaseModel):
    strict_bool: StrictBool


try:
    StrictBoolModel(strict_bool='False')
except ValidationError as e:
    print(str(e))
    """
    1 validation error for StrictBoolModel
    strict_bool
      value is not a valid boolean (type=value_error.strictbool)

3.2.6 ByteSize

您可以使用 ByteSize 數據類型將字節字符串表示形式轉換為原始字節,並打印出人類可讀的字節版本。

注意

注意,1b 將會被解析為 “1 bytes”,而不是 “1 bit”。

from pydantic import BaseModel, ByteSize


class MyModel(BaseModel):
    size: ByteSize


print(MyModel(size=52000).size)
#> 52000
print(MyModel(size='3000 KiB').size)
#> 3072000

m = MyModel(size='50 PB')
print(m.size.human_readable())
#> 44.4PiB
print(m.size.human_readable(decimal=True))
#> 50.0PB

print(m.size.to('TiB'))
#> 45474.73508864641

3.2.7 自定義數據類型

你也可以定義你自己的自定義數據類型。有多種方法可以達到這個目的。

3.2.7.1 帶有 get_validators 的類

使用帶有 get_validators 類方法的自定義類。它將被調用來使驗證器解析和驗證輸入數據。

提示

這些驗證器與[驗證器](# 3.3 驗證器) 中的驗證器具有相同的語義,因此,你可以聲明參數 config、field 等。

import re
from pydantic import BaseModel

# https://en.wikipedia.org/wiki/Postcodes_in_the_United_Kingdom#Validation
post_code_regex = re.compile(
    r'(?:'
    r'([A-Z]{1,2}[0-9][A-Z0-9]?|ASCN|STHL|TDCU|BBND|[BFS]IQQ|PCRN|TKCA) ?'
    r'([0-9][A-Z]{2})|'
    r'(BFPO) ?([0-9]{1,4})|'
    r'(KY[0-9]|MSR|VG|AI)[ -]?[0-9]{4}|'
    r'([A-Z]{2}) ?([0-9]{2})|'
    r'(GE) ?(CX)|'
    r'(GIR) ?(0A{2})|'
    r'(SAN) ?(TA1)'
    r')'
)


class PostCode(str):
    """
    Partial UK postcode validation. Note: this is just an example, and is not
    intended for use in production; in particular this does NOT guarantee
    a postcode exists, just that it has a valid format.
    """

    @classmethod
    def __get_validators__(cls):
        # one or more validators may be yielded which will be called in the
        # order to validate the input, each validator will receive as an input
        # the value returned from the previous validator
        yield cls.validate

    @classmethod
    def __modify_schema__(cls, field_schema):
        # __modify_schema__ should mutate the dict it receives in place,
        # the returned value will be ignored
        field_schema.update(
            # simplified regex here for brevity, see the wikipedia link above
            pattern='^[A-Z]{1,2}[0-9][A-Z0-9]? ?[0-9][A-Z]{2}$',
            # some example postcodes
            examples=['SP11 9DG', 'w1j7bu'],
        )

    @classmethod
    def validate(cls, v):
        if not isinstance(v, str):
            raise TypeError('string required')
        m = post_code_regex.fullmatch(v.upper())
        if not m:
            raise ValueError('invalid postcode format')
        # you could also return a string here which would mean model.post_code
        # would be a string, pydantic won't care but you could end up with some
        # confusion since the value's type won't match the type annotation
        # exactly
        return cls(f'{m.group(1)} {m.group(2)}')

    def __repr__(self):
        return f'PostCode({super().__repr__()})'


class Model(BaseModel):
    post_code: PostCode


model = Model(post_code='sw8 5el')
print(model)
#> post_code=PostCode('SW8 5EL')
print(model.post_code)
#> SW8 5EL
print(Model.schema())
"""
{
    'title': 'Model',
    'type': 'object',
    'properties': {
        'post_code': {
            'title': 'Post Code',
            'pattern': '^[A-Z]{1,2}[0-9][A-Z0-9]? ?[0-9][A-Z]{2}$',
            'examples': ['SP11 9DG', 'w1j7bu'],
            'type': 'string',
        },
    },
    'required': ['post_code'],
}
"""

使用 constr(regex=...) 可以實現類似的驗證,只是該值不會用空格格式化,該模式(schema)將僅包括完整模式(pattern),並且返回的值將是一個普通字符串。

有關如何生成模型的模式的更多詳細信息,請參見 [模式](# 3.5 模式)。

3.2.7.2 允許任意類型

您可以使用模型配置中的 arbitrary_types_allowed 配置來允許任意類型。

from pydantic import BaseModel, ValidationError


# This is not a pydantic model, it's an arbitrary class
class Pet:
    def __init__(self, name: str):
        self.name = name


class Model(BaseModel):
    pet: Pet
    owner: str

    class Config:
        arbitrary_types_allowed = True


pet = Pet(name='Hedwig')
# A simple check of instance type is used to validate the data
model = Model(owner='Harry', pet=pet)
print(model)
#> pet=<types_arbitrary_allowed.Pet object at 0x7fcb902cee50> owner='Harry'
print(model.pet)
#> <types_arbitrary_allowed.Pet object at 0x7fcb902cee50>
print(model.pet.name)
#> Hedwig
print(type(model.pet))
#> <class 'types_arbitrary_allowed.Pet'>
try:
    # If the value is not an instance of the type, it's invalid
    Model(owner='Harry', pet='Hedwig')
except ValidationError as e:
    print(e)
    """
    1 validation error for Model
    pet
      instance of Pet expected (type=type_error.arbitrary_type;
    expected_arbitrary_type=Pet)
    """
# Nothing in the instance of the arbitrary type is checked
# Here name probably should have been a str, but it's not validated
pet2 = Pet(name=42)
model2 = Model(owner='Harry', pet=pet2)
print(model2)
#> pet=<types_arbitrary_allowed.Pet object at 0x7fcb902ce4f0> owner='Harry'
print(model2.pet)
#> <types_arbitrary_allowed.Pet object at 0x7fcb902ce4f0>
print(model2.pet.name)
#> 42
print(type(model2.pet))
#> <class 'types_arbitrary_allowed.Pet'>

3.2.2.3 泛型類作為類型

這是一開始可能不需要的高級技術。 在大多數情況下,使用標准的pytantic模型可能會很好。

可以使用泛型類作為字段類型並使用 get_validators 基於"類型參數" (或子類型)執行自定義驗證。

如果您要用作子類型的泛型類具有類方法(classmethod) get_validators,則無需使用 arbitrary_types_allowed 即可工作。

因為可以聲明接收當前 field 的驗證器,所以可以提取 sub_fields (從泛型類類型參數中) 並使用它們驗證數據。

from pydantic import BaseModel, ValidationError
from pydantic.fields import ModelField
from typing import TypeVar, Generic

AgedType = TypeVar('AgedType')
QualityType = TypeVar('QualityType')


# This is not a pydantic model, it's an arbitrary generic class
class TastingModel(Generic[AgedType, QualityType]):
    def __init__(self, name: str, aged: AgedType, quality: QualityType):
        self.name = name
        self.aged = aged
        self.quality = quality

    @classmethod
    def __get_validators__(cls):
        yield cls.validate

    @classmethod
    # You don't need to add the "ModelField", but it will help your
    # editor give you completion and catch errors
    def validate(cls, v, field: ModelField):
        if not isinstance(v, cls):
            # The value is not even a TastingModel
            raise TypeError('Invalid value')
        if not field.sub_fields:
            # Generic parameters were not provided so we don't try to validate
            # them and just return the value as is
            return v
        aged_f = field.sub_fields[0]
        quality_f = field.sub_fields[1]
        errors = []
        # Here we don't need the validated value, but we want the errors
        valid_value, error = aged_f.validate(v.aged, {}, loc='aged')
        if error:
            errors.append(error)
        # Here we don't need the validated value, but we want the errors
        valid_value, error = quality_f.validate(v.quality, {}, loc='quality')
        if error:
            errors.append(error)
        if errors:
            raise ValidationError(errors, cls)
        # Validation passed without errors, return the same instance received
        return v


class Model(BaseModel):
    # for wine, "aged" is an int with years, "quality" is a float
    wine: TastingModel[int, float]
    # for cheese, "aged" is a bool, "quality" is a str
    cheese: TastingModel[bool, str]
    # for thing, "aged" is a Any, "quality" is Any
    thing: TastingModel


model = Model(
    # This wine was aged for 20 years and has a quality of 85.6
    wine=TastingModel(name='Cabernet Sauvignon', aged=20, quality=85.6),
    # This cheese is aged (is mature) and has "Good" quality
    cheese=TastingModel(name='Gouda', aged=True, quality='Good'),
    # This Python thing has aged "Not much" and has a quality "Awesome"
    thing=TastingModel(name='Python', aged='Not much', quality='Awesome'),
)
print(model)
"""
wine=<types_generics.TastingModel object at 0x7fcb903271f0>
cheese=<types_generics.TastingModel object at 0x7fcb903272e0>
thing=<types_generics.TastingModel object at 0x7fcb903276a0>
"""
print(model.wine.aged)
#> 20
print(model.wine.quality)
#> 85.6
print(model.cheese.aged)
#> True
print(model.cheese.quality)
#> Good
print(model.thing.aged)
#> Not much
try:
    # If the values of the sub-types are invalid, we get an error
    Model(
        # For wine, aged should be an int with the years, and quality a float
        wine=TastingModel(name='Merlot', aged=True, quality='Kinda good'),
        # For cheese, aged should be a bool, and quality a str
        cheese=TastingModel(name='Gouda', aged='yeah', quality=5),
        # For thing, no type parameters are declared, and we skipped validation
        # in those cases in the Assessment.validate() function
        thing=TastingModel(name='Python', aged='Not much', quality='Awesome'),
    )
except ValidationError as e:
    print(e)
    """
    2 validation errors for Model
    wine -> quality
      value is not a valid float (type=type_error.float)
    cheese -> aged
      value could not be parsed to a boolean (type=type_error.bool)
    """

3.3 驗證器

可以使用 validator 裝飾器實現對象之間的自定義驗證和復雜關系。

from pydantic import BaseModel, ValidationError, validator


class UserModel(BaseModel):
    name: str
    username: 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

    @validator('username')
    def username_alphanumeric(cls, v):
        assert v.isalnum(), 'must be alphanumeric'
        return v


user = UserModel(
    name='samuel colvin',
    username='scolvin',
    password1='zxcvbn',
    password2='zxcvbn',
)
print(user)
#> name='Samuel Colvin' username='scolvin' password1='zxcvbn' password2='zxcvbn'

try:
    UserModel(
        name='samuel',
        username='scolvin',
        password1='zxcvbn',
        password2='zxcvbn2',
    )
except ValidationError as e:
    print(e)
    """
    2 validation errors for UserModel
    name
      must contain a space (type=value_error)
    password2
      passwords do not match (type=value_error)
    """

驗證器上應該注意的幾件事:

  • 驗證器是類方法(class method),所以其接收到的第一個參數值是 UserModel 類,而不是 UserModel 類的實例。

  • 第二個參數總是要驗證的字段值。

  • 可以將下面參數的任意子集添加到簽名中(參數名必須匹配)

    • values 之前已驗證字段的字段名到字段值的映射。
    • config 模型配置。
    • field 要驗證的字段。
    • **kwargs 如果提供,則包含上面這些參數中未在簽名中顯式列出的參數。
  • 驗證器應該返回解析的值或者引發 ValueError、TypeError 或 AssertionError (可以使用 assert 語句)。

警告

如果你使用 assert 語句,那么請注意, 帶有 -O 優化標志 的 Python 將會禁用 assert 語句,導致驗證器停止工作。

  • 當驗證器依賴於其他值時,你應該知道:
    • 驗證按照字段被定義的順序完成。例如,在上面的示例中,password2 可以訪問 password1 (和 name),但 password1 不能訪問 password2。關於字段如何排序的詳細信息,請參考 [字段排序](# 3.1.11 字段排序)。
    • 如果在其他字段上驗證失敗 (或那個字段缺失),則它不會被包含在 values 中,因此,在示例中使用了 if 'password1' in values and ... 來進行判斷。

3.3.1 Pre 和 per-item 驗證器

驗證器可以做更復雜的事情:

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


class DemoModel(BaseModel):
    square_numbers: List[int] = []
    cube_numbers: List[int] = []

    # '*' is the same as 'cube_numbers', 'square_numbers' here:
    @validator('*', pre=True)
    def split_str(cls, v):
        if isinstance(v, str):
            return v.split('|')
        return v

    @validator('cube_numbers', 'square_numbers')
    def check_sum(cls, v):
        if sum(v) > 42:
            raise ValueError('sum of numbers greater than 42')
        return v

    @validator('square_numbers', each_item=True)
    def check_squares(cls, v):
        assert v ** 0.5 % 1 == 0, f'{v} is not a square number'
        return v

    @validator('cube_numbers', each_item=True)
    def check_cubes(cls, v):
        # 64 ** (1 / 3) == 3.9999999999999996 (!)
        # this is not a good way of checking cubes
        assert v ** (1 / 3) % 1 == 0, f'{v} is not a cubed number'
        return v


print(DemoModel(square_numbers=[1, 4, 9]))
#> square_numbers=[1, 4, 9] cube_numbers=[]
print(DemoModel(square_numbers='1|4|16'))
#> square_numbers=[1, 4, 16] cube_numbers=[]
print(DemoModel(square_numbers=[16], cube_numbers=[8, 27]))
#> square_numbers=[16] cube_numbers=[8, 27]
try:
    DemoModel(square_numbers=[1, 4, 2])
except ValidationError as e:
    print(e)
    """
    1 validation error for DemoModel
    square_numbers -> 2
      2 is not a square number (type=assertion_error)
    """

try:
    DemoModel(cube_numbers=[27, 27])
except ValidationError as e:
    print(e)
    """
    1 validation error for DemoModel
    cube_numbers
      sum of numbers greater than 42 (type=value_error)
    """

需要注意的事情:

  • 可以通過給單個驗證器傳遞多個字段名稱將其應用於多個字段。
  • 可以通過給單個驗證器傳遞特殊的 * 值將其應用於所有字段。
  • 關鍵字參數 pre 將會導致該驗證器的調用優先於其他驗證器。
  • 傳遞 each_item=True 將會導致驗證器應用於可迭代對象中的每個值(例如,List、Dict 和 Set 等),而不是整個對象。

3.3.2 子類驗證器和 each_item

如果將驗證器與引用父類上 List 類型字段的子類一起使用,那么 each_item=True 將導致驗證器無法運行;相反,必須以編程方式遍歷該列表。

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


class ParentModel(BaseModel):
    names: List[str]


class ChildModel(ParentModel):
    @validator('names', each_item=True)
    def check_names_not_empty(cls, v):
        assert v != '', 'Empty strings are not allowed.'
        return v


# This will NOT raise a ValidationError because the validator was not called
try:
    child = ChildModel(names=['Alice', 'Bob', 'Eve', ''])
except ValidationError as e:
    print(e)
else:
    print('No ValidationError caught.')
    #> No ValidationError caught.


class ChildModel2(ParentModel):
    @validator('names')
    def check_names_not_empty(cls, v):
        for name in v:
            assert name != '', 'Empty strings are not allowed.'
        return v


try:
    child = ChildModel2(names=['Alice', 'Bob', 'Eve', ''])
except ValidationError as e:
    print(e)
    """
    1 validation error for ChildModel2
    names
      Empty strings are not allowed. (type=assertion_error)
    """

3.3.3 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()


print(DemoModel())
#> ts=datetime.datetime(2020, 10, 28, 20, 3, 33, 424317)
print(DemoModel(ts='2017-11-08T14:00'))
#> ts=datetime.datetime(2017, 11, 8, 14, 0)

注意,最好將 always=True 和 pre=True 一起使用,否則,當 always=True 時,pydantic會嘗試對默認值 None 進行驗證,從而導致錯誤。

3.3.4 復用驗證器

有時,你會想要在多個 字段/模型 上使用相同的驗證器(例如,規范化一些輸入數據)。最容易想到的方法就是編寫一個單獨的函數,然后從多個裝飾器進行調用。顯然,這需要大量的重復和樣板代碼。為了避免這種情況,已經在 v1.2 中為 pydantic.validator 添加了 allow_reuse 參數(默認值為 False):

from pydantic import BaseModel, validator


def normalize(name: str) -> str:
    return ' '.join((word.capitalize()) for word in name.split(' '))


class Producer(BaseModel):
    name: str

    # validators
    _normalize_name = validator('name', allow_reuse=True)(normalize)


class Consumer(BaseModel):
    name: str

    # validators
    _normalize_name = validator('name', allow_reuse=True)(normalize)


jane_doe = Producer(name='JaNe DOE')
john_doe = Consumer(name='joHN dOe')
assert jane_doe.name == 'Jane Doe'
assert john_doe.name == 'John Doe'

很明顯,重復減少了,模型再次變得幾乎是聲明性的。

提示

如果您有很多字段需要驗證,那么通常有必要定義一個幫助函數,使用該函數可以避免一次又一次地設置 allow_reuse=True

例如,對於上面的例子,可以將代碼改寫為:

from pydantic import validator, BaseModel


@validator('name', allow_reuse=True)
def normalize(name: str) -> str:
  return ' '.join((word.capitalize()) for word in name.split(' '))


class Producer(BaseModel):
  name: str

  # validators
  _normalize_name = normalize


class Consumer(BaseModel):
  name: str

  # validators
  _normalize_name = normalize


jane_doe = Producer(name='JaNe DOE')
john_doe = Consumer(name='joHN dOe')
assert jane_doe.name == 'Jane Doe'
assert john_doe.name == 'John Doe'

3.3.5 根驗證器

驗證也可以在整個模型的數據上執行。

from pydantic import BaseModel, ValidationError, root_validator


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

    @root_validator(pre=True)
    def check_card_number_omitted(cls, values):
        assert 'card_number' not in values, 'card_number should not be included'
        return values

    @root_validator
    def check_passwords_match(cls, values):
        pw1, pw2 = values.get('password1'), values.get('password2')
        if pw1 is not None and pw2 is not None and pw1 != pw2:
            raise ValueError('passwords do not match')
        return values


print(UserModel(username='scolvin', password1='zxcvbn', password2='zxcvbn'))
#> username='scolvin' password1='zxcvbn' password2='zxcvbn'
try:
    UserModel(username='scolvin', password1='zxcvbn', password2='zxcvbn2')
except ValidationError as e:
    print(e)
    """
    1 validation error for UserModel
    __root__
      passwords do not match (type=value_error)
    """

try:
    UserModel(
        username='scolvin',
        password1='zxcvbn',
        password2='zxcvbn',
        card_number='1234',
    )
except ValidationError as e:
    print(e)
    """
    1 validation error for UserModel
    __root__
      card_number should not be included (type=assertion_error)
    """

與字段驗證器一樣,根驗證器可以具有 pre=True,在這種情況下,它們會在字段驗證發生之前被調用(並與原始輸入數據一起提供),或者 pre=False (默認情況下),在這種情況下,它們會在字段驗證之后被調用。

如果 pre=True 時根驗證器引發錯誤,則不會進行字段驗證。與字段驗證器一樣,當 pre=False 時,即使之前的驗證器失敗,默認情況下也會調用根驗證器;可以通過為驗證器設置 skip_on_failure=True 關鍵字參數來改變這種行為。values 參數將是一個字典,其中包含已通過字段驗證的值和適用的字段默認值。

3.3.6 字段檢查

在創建類時,將檢查驗證器,以確認它們指定的字段在模型中實際存在。

但有時這是不希望看到的:例如,如果您定義了一個驗證器來驗證繼承模型上的字段。在這種情況下,應該在驗證器上設置 check_fields=False。

3.3.7 Dataclass 驗證器

驗證器也可以與pydantic 的 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()


print(DemoDataclass())
#> DemoDataclass(ts=datetime.datetime(2020, 11, 1, 15, 49, 0, 182639))
print(DemoDataclass(ts='2017-11-08T14:00'))
#> DemoDataclass(ts=datetime.datetime(2017, 11, 8, 14, 0))

3.4 模型配置

Pydantic 的行為可以通過模型上的 Config 類控制。

選項:

  • title

    聲成的 JSON 模式的標題。

  • anystr_strip_whitespace

    是否移除 str 和 bytes 類型中前導和尾隨的空白字符(默認值為 False)。

  • min_anystr_length

    str 和 bytes 類型的最小長度 (默認為 0)。

  • max_anystr_length

    str 和 bytes 類型的最小長度 (默認為 2 ** 16)。

  • validate_all

    是否驗證字段的默認值 (默認為 False)。

  • extra

    在模型初始化時是否忽略、允許或禁止額外的屬性。可接受字符串值 ignore、allow 或 forbid 以及 Extra 枚舉 (例如, Extra.ignore)。如果模型包含了額外的屬性,則 forbid 將會導致驗證失敗。ignore 將靜默忽略任何額外屬性。allow 將屬性分配給模型。

  • allow_mutation

    模型是否是偽不可變的。例如,是否允許 setattr (默認為 True)。

  • use_enum_values

    是否使用枚舉的 value 屬性而不是原始枚舉填充模型。如果之后想要序列化 model.dict() ,這可能會很有用 (默認值為 False)。

  • fields

    包含每個字段的模式信息的字典; 這等效於使用 [Field 類](# 3.5 模式) (默認為 None)。

  • validate_assignment

    是否對屬性的賦值執行驗證 (默認為 False)。

  • allow_population_by_field_name

    是否可以用模型屬性給出的名稱填充別名字段,以及別名 (默認為 False )。

注意

這個配置設置的名稱在v1.0中從 allow_population_by_alias 更改為 allow_population_by_field_name。

  • error_msg_templates

    用於覆蓋默認錯誤消息模板的字典。傳入一個字典,其中的鍵與您想要覆蓋的錯誤消息相匹配 (默認為 {})。

  • arbitrary_types_allowed

    是否允許字段使用任意用戶類型(只需檢查值是否為該類型的實例即可對它們進行驗證)。 如果為 False,則會在模型聲明時引發RuntimeError(默認為 False)。 請參閱[字段類型](# 3.2 字段類型)中的示例。

  • orm_mode

    是否允許使用 [ORM 模式](# 3.1.3 ORM 模式)。

  • getter_dict

    一個自定義類 (應該繼承自 GetterDict),用於分解ORM類進行驗證,並與 orm_mode 一起使用

  • alias_generator

    接受字段名並返回其別名的可調用對象。

  • keep_untouched

    模型的默認值的類型元組 (例如,描述符),該默認值在模型創建期間不應更改,也不會包含在模型模式中。 注意:這意味着模型上具有此類型的默認值的屬性 (而不是此類型的注釋) 將被保留。

  • schema_extra

    用於 拓展/更新 生成的 JSON 模式的字典,或用於對其進行后處理(post-process)的可調用對象。參見 [模式定制](# 3.5.5 模式定制)。

  • json_loads

    用於解碼 JSON 的自定義函數。參見 [自定義 JSON反序列化](# 3.6.4.3 自定義 JSON反序列化)。

  • json_dumps

    用於編碼 JSON的自定義函數。參見 [自定義 JSON反序列化](# 3.6.4.3 自定義 JSON反序列化)。

  • json_encoders

    用於自定義類型被編碼成JSON的方式的字典。參見 [JSON 序列化](# 3.6.4 model.json(…))。

  • underscore_attrs_are_private

    是否將任何下划線非類 (non-class) 變量屬性當做私有屬性,或讓它們保持原樣。參見 [私有模型屬性](# 3.1.14 私有模型屬性)。

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 for Model
    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 for User
    name
      max_length:10 (type=value_error.any_str.max_length; limit_value=10)
    """

3.4.1 別名生成器

如果數據源字段名稱與您的代碼樣式不匹配 (例如CamelCase字段),則可以使用 alias_generator 自動生成別名:

from pydantic import BaseModel


def to_camel(string: str) -> str:
    return ''.join(word.capitalize() for word in string.split('_'))


class Voice(BaseModel):
    name: str
    language_code: str

    class Config:
        alias_generator = to_camel


voice = Voice(Name='Filiz', LanguageCode='tr-TR')
print(voice.language_code)
#> tr-TR
print(voice.dict(by_alias=True))
#> {'Name': 'Filiz', 'LanguageCode': 'tr-TR'}

這里,“camel case” 指的是 “upper camel case”,又稱 “pascal case”,例如 CamelCase。如果您希望使用 “lower camel case”,例如 camelCase,那么修改上面的 to_camel 函數應該很簡單。

3.4.2 別名優先

警告

別名優先級邏輯在v1.4中更改,以解決以前版本中的錯誤和意外行為。在某些情況下,這可能表示一個中斷的更改,詳細信息請參閱#1178和下面的優先順序。

在一個字段的別名可能被定義在多個地方的情況下,選擇的值按如下規則確定(按優先級降序):

  • 在模型上直接通過 Field(..., alias= ) 設置。
  • 在模型上的 Config.fields 中定義。
  • 在父模型上通過 Field(..., alias= )。
  • 在父模型上的 Config.fields 中定義。
  • 由 alias_generator 生成,無論它是在模型上還是在父模型上。

注意

這意味着在子模型上定義的 alias_generator 不優先於在父模型的字段上定義的別名。

示例:

from pydantic import BaseModel, Field


class Voice(BaseModel):
    name: str = Field(None, alias='ActorName')
    language_code: str = None
    mood: str = None


class Character(Voice):
    act: int = 1

    class Config:
        fields = {'language_code': 'lang'}

        @classmethod
        def alias_generator(cls, string: str) -> str:
            # this is the same as `alias_generator = to_camel` above
            return ''.join(word.capitalize() for word in string.split('_'))


print(Character.schema(by_alias=True))
"""
{
    'title': 'Character',
    'type': 'object',
    'properties': {
        'ActorName': {'title': 'Actorname', 'type': 'string'},
        'lang': {'title': 'Lang', 'type': 'string'},
        'Mood': {'title': 'Mood', 'type': 'string'},
        'Act': {'title': 'Act', 'default': 1, 'type': 'integer'},
    },
}
"""

3.5 模式

Pydantic 允許從模型自動創建 JSON 模式:

from enum import Enum
from pydantic import BaseModel, Field


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 = Field(...)
    gender: Gender = Field(None, alias='Gender')
    snap: int = Field(
        42,
        title='The Snap',
        description='this is the value of snap',
        gt=30,
        lt=50,
    )

    class Config:
        title = 'Main'


# this is equivalent to json.dumps(MainModel.schema(), indent=2):
print(MainModel.schema_json(indent=2))

輸出如下:

{
  "title": "Main",
  "description": "This is the description of the main model",
  "type": "object",
  "properties": {
    "foo_bar": {
      "$ref": "#/definitions/FooBar"
    },
    "Gender": {
      "$ref": "#/definitions/Gender"
    },
    "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"
      ]
    },
    "Gender": {
      "title": "Gender",
      "description": "An enumeration.",
      "enum": [
        "male",
        "female",
        "other",
        "not_given"
      ],
      "type": "string"
    }
  }
}

生成的模式符合以下規范:

  • JSON Schema Core
  • JSON Schema Validation
  • OpenAPI

BaseModel.schema 將會返回模式字典,而 BaseModel.schema_json 將會返回那個字典的 JSON 字符串表示。

根據規范,使用的子模型被添加到 definitions JSON屬性並被引用。

所有子模型(及其子模型)的模式都直接放在一個頂級 definitions JSON鍵中,以便於重用和引用。

經過修改 (通過 Field 類) 的 “子模型”,如自定義標題、描述或默認值,被遞歸地包括而不是引用。

模型的 description 取自類的文檔字符串或 Field 類的 description 參數。

默認情況下使用別名作為鍵來生成模式,但是可以使用模型屬性名而不是調用 MainModel.schema/schema_json(by_alias=False) 來生成模式。

使用 ref_template 關鍵字參數調用 schema() 或 schema_json() 可以改變 $ref (上面的 #/definitions/FooBar ) 的格式。

ApplePie.schema(ref_template='/schemas/{model}.json#/') ,這里,將會使用 str.format 將 {model} 替換為模型名稱。

3.5.1 字段定制

此外,還可以使用 Field 函數提供關於字段和驗證的額外信息。它有下面一些參數:

  • default

    該參數為位置參數。表示字段的默認值。因為 Field 替換字段的默認值,所以可以使用第一個參數設置默認值。使用省略號表示字段是必需的。

  • default_factory

    一個零參數的可調用對象,當需要該字段的默認值時將調用它。除了其他用途外,它還可用於設置動態默認值。禁止同時設置default 和 default_factory。

  • alias

    字段的公開名稱。

  • title

    如果忽略,則使用 filed_name.title()。

  • description

    如果忽略並且注解是子模型,將會使用子模型的文檔字符串。

  • const

    如果該參數出現,則必須與字段的默認值相同。

  • gt

    對於數值值 (int,float,Decimal),將向 JSON 模式添加一個 “大於” 驗證和一個 exclusiveMinimum 注解。

  • ge

    對於數值值 ,將向 JSON 模式添加一個 “大於等於” 驗證和一個 minimum 注解。

  • lt

    對於數值值 ,將向 JSON 模式添加一個"小於" 驗證和一個 exclusiveMaximum 注解。

  • le

    對於數值值 ,將向 JSON 模式添加一個 “小於等於” 驗證和一個 maximum 注解。

  • multiple_of

    對於數值值,將向 JSON 模式添加一個 “倍數” 驗證和一個 multipleOf 注解。

  • min_items

    對於列表值,將向 JSON 模式添加相應的驗證和 minItems 注解。

  • max_items

    對於列表值,將向 JSON 模式添加相應的驗證和 maxItems 注解。

  • min_length

    對於字符串值,將向 JSON 模式添加相應的驗證和 minLength 注解。

  • max_length

    對於字符串值,將向 JSON 模式添加相應的驗證和 maxLength 注解。

  • regex

    對於字符串值,將向 JSON 模式添加一個從傳遞的字符串生成的正則表達式驗證和一個 pattern 注解。

注意

pydantic使用 re.match 驗證字符串,該方法將正則表達式視為隱式錨定在開始處。相反,JSON模式驗證器將模式關鍵字視為隱式非錨定的,這更像 re.search 所做的。

為了互操作性,根據你想要的行為,要么顯式地用 ^ 錨定你的正則表達式 (例如使用 ^foo 來匹配任何以 foo 開頭的字符串),要么使用 .? 顯式地允許任意的前綴用 (例如 .?foo 匹配任何包含子字符串 foo 的字符串)。

有關在v2中對pydantic行為可能進行的更改的討論,請參閱#1631。

  • **

    任何其他關鍵字參數 (例如 example) 都將逐字添加到字段的模式中。

[配置類](# 3.4 模型配置) 的 fields 屬性可以用來設置上面除 default 之外的所有參數,而不是使用 Field。

3.5.1.1 未強制執行的字段約束

如果 pydantic 找到未強制執行的約束,將引發錯誤。如果要強制約束出現在模式中,即使解析時沒有檢查該約束,可以將 Field() 的可變參數與原始模式屬性名稱一起使用:

from pydantic import BaseModel, Field, PositiveInt

try:
    # this won't work since PositiveInt takes precedence over the
    # constraints defined in Field meaning they're ignored
    class Model(BaseModel):
        foo: PositiveInt = Field(..., lt=10)
except ValueError as e:
    print(e)
    """
    On field "foo" the following field constraints are set but not enforced:
    lt.
    For more details see https://pydantic-
    docs.helpmanual.io/usage/schema/#unenforced-field-constraints
    """


# but you can set the schema attribute directly:
# (Note: here exclusiveMaximum will not be enforce)
class Model(BaseModel):
    foo: PositiveInt = Field(..., exclusiveMaximum=10)


print(Model.schema())
"""
{
    'title': 'Model',
    'type': 'object',
    'properties': {
        'foo': {
            'title': 'Foo',
            'exclusiveMaximum': 10,
            'exclusiveMinimum': 0,
            'type': 'integer',
        },
    },
    'required': ['foo'],
}
"""


# if you find yourself needing this, an alternative is to declare
# the constraints in Field (or you could use conint())
# here both constraints will be enforced:
class Model(BaseModel):
    # Here both constraints will be applied and the schema
    # will be generated correctly
    foo: int = Field(..., gt=0, lt=10)


print(Model.schema())
"""
{
    'title': 'Model',
    'type': 'object',
    'properties': {
        'foo': {
            'title': 'Foo',
            'exclusiveMinimum': 0,
            'exclusiveMaximum': 10,
            'type': 'integer',
        },
    },
    'required': ['foo'],
}
"""

3.5.2 修改自定義字段中的模式

自定義字段類型可以使用 modify_schema 類方法自定義為它們生成的模式;有關更多細節,請參見[自定義數據類型](# 3.2.7 自定義數據類型)。

3.5.3 JSON模式類型

類型,自定義字段類型和約束 (像 max_length) 按照以下優先級順序映射到相應的規范格式 (當有等效的可用格式時):

  1. JSON Schema Core
  2. JSON Schema Validation
  3. OpenAPI Data Types
  4. 標准的 format JSON字段用於為更復雜的 string 子類型定義pydantic擴展。

從Python/pydantic到JSON模式的字段模式映射請參考原始英文文檔。

3.5.4 頂層模式生成

您還可以生成一個頂層JSON模式,它的定義中只包含一列模型和相關的子模型:

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=2))

輸出如下:

{
  "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"
      ]
    }
  }
}

3.5.5 模式定制

可以定制生成的 $ref JSON位置:定義總是存儲在鍵 definitions 下,但是可以為引用使用指定的前綴。

如果需要擴展或修改JSON模式默認定義位置,這將非常有用。例如對於 OpenAPI:

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


class Foo(BaseModel):
    a: int


class Model(BaseModel):
    a: Foo


# Default location for OpenAPI
top_level_schema = schema([Model], ref_prefix='#/components/schemas/')
print(json.dumps(top_level_schema, indent=2))

輸出如下:

{
  "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"
      ]
    }
  }
}

還可以 擴展/覆蓋 模型中生成的JSON模式。

為此,使用 Config 子類屬性 schema_extra

例如,你可以在JSON模式中添加 example

from pydantic import BaseModel


class Person(BaseModel):
    name: str
    age: int

    class Config:
        schema_extra = {
            'examples': [
                {
                    'name': 'John Doe',
                    'age': 25,
                }
            ]
        }


print(Person.schema_json(indent=2))

輸出如下:

{
  "title": "Person",
  "type": "object",
  "properties": {
    "name": {
      "title": "Name",
      "type": "string"
    },
    "age": {
      "title": "Age",
      "type": "integer"
    }
  },
  "required": [
    "name",
    "age"
  ],
  "examples": [
    {
      "name": "John Doe",
      "age": 25
    }
  ]
}

對於更細粒度的控制,您可以選擇將 schema_extra 設置為可調用對象,並對生成的模式進行后期處理。可調用對象可以有一個或兩個位置參數。第一個參數是模式字典。如果有第二個參數,將是模型類。可調用對象期望原地更改模式字典;可調用對象的返回值暫時未被使用。

例如,title 鍵可以從模型的 properties 中刪除:

from typing import Dict, Any, Type
from pydantic import BaseModel


class Person(BaseModel):
    name: str
    age: int

    class Config:
        @staticmethod
        def schema_extra(schema: Dict[str, Any], model: Type['Person']) -> None:
            for prop in schema.get('properties', {}).values():
                prop.pop('title', None)


print(Person.schema_json(indent=2))

輸出如下:

{
  "title": "Person",
  "type": "object",
  "properties": {
    "name": {
      "type": "string"
    },
    "age": {
      "type": "integer"
    }
  },
  "required": [
    "name",
    "age"
  ]
}

3.6 導出模型

除了通過名稱 (例如 model.foobar ) 直接訪問模型屬性外,模型還可以通過多種方式轉換和導出:

3.6.1 model.dict(…)

這是將模型轉換為字典的主要方法。子模型會被遞歸的轉換成字典。

參數:

  • include

    包含在返回的字典中的字段。參見 [包含和排序的高級用法](# 3.6.6 包含和排序的高級用法)。

  • exclude

    從返回的字典中排除的字段。參見 [包含和排序的高級用法](# 3.6.6 包含和排序的高級用法)。

  • by_alias

    字段別名是否應該在返回的字典中作為鍵;默認為 False。

  • exclude_unset

    創建模型時未顯式設置的字段是否應從返回的字典中排除;默認為 False。在v1.0之前,exclude_unset 被稱為 skip_defaults;現在不贊成使用 skip_defaults。

  • exclude_defaults

    是否應從返回的字典中排除等於其默認值的字段 (無論是否設置);默認為 False。

  • exclude_none

    是否應從返回的字典中排除等於 None 的字段;默認為 False。

示例:

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': 123})

# returns a dictionary:
print(m.dict())
"""
{
    'banana': 3.14,
    'foo': 'hello',
    'bar': {'whatever': 123},
}
"""
print(m.dict(include={'foo', 'bar'}))
#> {'foo': 'hello', 'bar': {'whatever': 123}}
print(m.dict(exclude={'foo', 'bar'}))
#> {'banana': 3.14}

3.6.2 dict(model) 和迭代

Pydantic 模型也可以使用 dict(model) 轉換成字典,並且也可以使用 for field_name, value in model: 迭代模型上的字段。

使用這種方法,將返回原始字段值,因此子模型不會被轉換為字典。

示例:

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': 123})

print(dict(m))
"""
{
    'banana': 3.14,
    'foo': 'hello',
    'bar': BarModel(
        whatever=123,
    ),
}
"""
for name, value in m:
    print(f'{name}: {value}')
    #> banana: 3.14
    #> foo: hello
    #> bar: whatever=123

3.6.3 model.copy(…)

copy() 允許復制模型,這對不可變模型尤其有用。

參數:

  • include

    要包含在返回的字典中的字段。參見 [包含和排序的高級用法](# 3.6.6 包含和排序的高級用法)。

  • exclude

    要從返回的字典中排序的字典。參見 [包含和排序的高級用法](# 3.6.6 包含和排序的高級用法)。

  • update

    創建復制的模型時要更改的值的字典。

  • deep

    是否對新模型進行深復制;默認為 False。

示例:

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': 123})

print(m.copy(include={'foo', 'bar'}))
#> foo='hello' bar=BarModel(whatever=123)
print(m.copy(exclude={'foo', 'bar'}))
#> banana=3.14
print(m.copy(update={'banana': 0}))
#> banana=0 foo='hello' bar=BarModel(whatever=123)
print(id(m.bar), id(m.copy().bar))
#> 140512307789344 140512307789344
# normal copy gives the same object reference for `bar`
print(id(m.bar), id(m.copy(deep=True).bar))
#> 140512307789344 140512307819952
# deep copy gives a new object reference for `bar`

3.6.4 model.json(…)

.json() 方法會將模型序列化為 JSON。通常,.json() 依次調用 .dict() 並序列化其結果。(對於具有自定義根類型的模型,在調用 .dict() 之后,僅序列化 root 鍵的值)。

參數:

  • include

    要包含在返回的字典中的字段。參見 [包含和排序的高級用法](# 3.6.6 包含和排序的高級用法)。

  • exclude

    要從返回的字典中排序的字典。參見 [包含和排序的高級用法](# 3.6.6 包含和排序的高級用法)。

  • by_alias

    字段別名是否應該在返回的字典中作為鍵;默認為 False。

  • exclude_unset

    創建模型時未顯式設置的字段是否應從返回的字典中排除;默認為 False。在v1.0之前,exclude_unset 被稱為 skip_defaults;現在不贊成使用 skip_defaults。

  • exclude_defaults

    是否應從返回的字典中排除等於其默認值的字段 (無論是否設置);默認為 False。

  • exclude_none

    是否應從返回的字典中排除等於 None 的字段;默認為 False。

  • encoder

    傳遞給 json.dumps() 的 default 參數的自義編碼器函數,默認為設計用於所有常見類型的自定義編碼器。

  • **dumps_kwargs

    傳遞給 json.dumps() 的其他關鍵字參數。例如,indent。

Pydantic可以將許多常用的類型序列化為JSON(例如 datetime,date 或 UUID),這通常會失敗於一個簡單的 json.dumps(foobar)。

from datetime import datetime
from pydantic import BaseModel


class BarModel(BaseModel):
    whatever: int


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


m = FooBarModel(foo=datetime(2032, 6, 1, 12, 13, 14), bar={'whatever': 123})
print(m.json())
#> {"foo": "2032-06-01T12:13:14", "bar": {"whatever": 123}}

3.6.4.1 json_encoders

序列化可以使用 json_encoders 配置屬性在模型上定制;鍵應該是類型,值應該是序列化該類型的函數(見下面的例子):

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


class WithCustomEncoders(BaseModel):
    dt: datetime
    diff: timedelta

    class Config:
        json_encoders = {
            datetime: lambda v: v.timestamp(),
            timedelta: timedelta_isoformat,
        }


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

默認情況下,timedelta 被編碼為總秒數的簡單浮點數。timedelta_isoformat 是一個可選的替代方案,它實現了ISO 8601時間差異編碼。

3.6.4.2 序列化子類

注意

在v1.5之前,常見類型的子類不會自動序列化為JSON。

通用類型的子類會像它們的超類一樣被自動編碼:

from datetime import date, timedelta
from pydantic import BaseModel
from pydantic.validators import int_validator


class DayThisYear(date):
    """
    Contrived example of a special type of date that
    takes an int and interprets it as a day in the current year
    """

    @classmethod
    def __get_validators__(cls):
        yield int_validator
        yield cls.validate

    @classmethod
    def validate(cls, v: int):
        return date.today().replace(month=1, day=1) + timedelta(days=v)


class FooModel(BaseModel):
    date: DayThisYear


m = FooModel(date=300)
print(m.json())
#> {"date": "2020-10-27"}

3.6.4.3 自定義 JSON反序列化

為了提高JSON編碼和解碼的性能,可以通過配置中的 json_loadjson_dumps 屬性來使用其他JSON實現(例如ujson)。

from datetime import datetime
import ujson
from pydantic import BaseModel


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

    class Config:
        json_loads = ujson.loads


user = User.parse_raw('{"id": 123,"signup_ts":1234567890,"name":"John Doe"}')
print(user)
#> id=123 signup_ts=datetime.datetime(2009, 2, 13, 23, 31, 30,
#> tzinfo=datetime.timezone.utc) name='John Doe'

ujson 通常不能用於轉儲JSON,因為它不支持對像 datetime 這樣的對象進行編碼,也不接受 default 回退函數參數。為此,您可以使用另一個庫,如 orjson

from datetime import datetime
import orjson
from pydantic import BaseModel


def orjson_dumps(v, *, default):
    # orjson.dumps returns bytes, to match standard json.dumps we need to decode
    return orjson.dumps(v, default=default).decode()


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

    class Config:
        json_loads = orjson.loads
        json_dumps = orjson_dumps


user = User.parse_raw('{"id":123,"signup_ts":1234567890,"name":"John Doe"}')
print(user.json())
#> {"id":123,"signup_ts":"2009-02-13T23:31:30+00:00","name":"John Doe"}

請注意,orjson 本身就考慮了 datetime 編碼,這使得它比 json.dumps 更快。但這意味着您不能總是使用 Config.json_encoders 自定義編碼。

3.6.5 pickle.dumps(model)

使用與 copy() 相同的管道,pydantic模型支持高效的pickle和unpickle。

import pickle
from pydantic import BaseModel


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


m = FooBarModel(a='hello', b=123)
print(m)
#> a='hello' b=123
data = pickle.dumps(m)
print(data)
"""
b'\x80\x04\x95\x8e\x00\x00\x00\x00\x00\x00\x00\x8c\x17exporting_models_pickle
\x94\x8c\x0bFooBarModel\x94\x93\x94)\x81\x94}\x94(\x8c\x08__dict__\x94}\x94(\
x8c\x01a\x94\x8c\x05hello\x94\x8c\x01b\x94K{u\x8c\x0e__fields_set__\x94\x8f\x
94(h\x07h\t\x90\x8c\x1c__private_attribute_values__\x94}\x94ub.'
"""
m2 = pickle.loads(data)
print(m2)
#> a='hello' b=123

3.6.6 包含和排序的高級用法

dictjsoncopy 方法支持 includeexclude 參數,這些參數可以是集合或字典。這允許嵌套選擇導出哪些字段:

from pydantic import BaseModel, SecretStr


class User(BaseModel):
    id: int
    username: str
    password: SecretStr


class Transaction(BaseModel):
    id: str
    user: User
    value: int


t = Transaction(
    id='1234567890',
    user=User(
        id=42,
        username='JohnDoe',
        password='hashedpassword'
    ),
    value=9876543210,
)

# using a set:
print(t.dict(exclude={'user', 'value'}))
#> {'id': '1234567890'}

# using a dict:
print(t.dict(exclude={'user': {'username', 'password'}, 'value': ...}))
#> {'id': '1234567890', 'user': {'id': 42}}

print(t.dict(include={'id': ..., 'user': {'id'}}))
#> {'id': '1234567890', 'user': {'id': 42}}

省略號 (...) 表示我們要排除或包括整個鍵,就像我們將其包括在集合中一樣。 當然,可以在任何深度級別進行相同的操作。

從子模型或字典的列表或元組中包括或排除字段時,必須格外小心。 在這種情況下,字典和相關方法期望使用整數鍵按元素進行逐項包含或排除。 要從列表或元組的每個成員中排除字段,可以按以下方式使用字典鍵 '_all_':

import datetime
from typing import List

from pydantic import BaseModel, SecretStr


class Country(BaseModel):
    name: str
    phone_code: int


class Address(BaseModel):
    post_code: int
    country: Country


class CardDetails(BaseModel):
    number: SecretStr
    expires: datetime.date


class Hobby(BaseModel):
    name: str
    info: str


class User(BaseModel):
    first_name: str
    second_name: str
    address: Address
    card_details: CardDetails
    hobbies: List[Hobby]


user = User(
    first_name='John',
    second_name='Doe',
    address=Address(
        post_code=123456,
        country=Country(
            name='USA',
            phone_code=1
        )
    ),
    card_details=CardDetails(
        number=4212934504460000,
        expires=datetime.date(2020, 5, 1)
    ),
    hobbies=[
        Hobby(name='Programming', info='Writing code and stuff'),
        Hobby(name='Gaming', info='Hell Yeah!!!'),
    ],
)

exclude_keys = {
    'second_name': ...,
    'address': {'post_code': ..., 'country': {'phone_code'}},
    'card_details': ...,
    # You can exclude fields from specific members of a tuple/list by index:
    'hobbies': {-1: {'info'}},
}

include_keys = {
    'first_name': ...,
    'address': {'country': {'name'}},
    'hobbies': {0: ..., -1: {'name'}},
}

# would be the same as user.dict(exclude=exclude_keys) in this case:
print(user.dict(include=include_keys))
"""
{
    'first_name': 'John',
    'address': {'country': {'name': 'USA'}},
    'hobbies': [
        {
            'name': 'Programming',
            'info': 'Writing code and stuff',
        },
        {'name': 'Gaming'},
    ],
}
"""

# To exclude a field from all members of a nested list or tuple, use "__all__":
print(user.dict(exclude={'hobbies': {'__all__': {'info'}}}))
"""
{
    'first_name': 'John',
    'second_name': 'Doe',
    'address': {
        'post_code': 123456,
        'country': {'name': 'USA', 'phone_code': 1},
    },
    'card_details': {
        'number': SecretStr('**********'),
        'expires': datetime.date(2020, 5, 1),
    },
    'hobbies': [{'name': 'Programming'}, {'name': 'Gaming'}],
}
"""

json 和 copy 方法也是如此。

3.7 Dataclasses

如果您不想使用pydantic的 BaseModel ,您可以在標准 dataclasses (在python 3.7中引入)上獲得相同的數據驗證。

數據類在Python 3.6中使用 dataclasses的backport包 工作。

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.dataclasses.dataclass 是具有驗證的 dataclasses.dataclass 的直接替代。而不是 pydantic.BaseModel (在初始化鈎子的工作方式上略有不同) 的替代。在某些情況下,子類化 BaseModel 是更好的選擇。

有關更多信息和討論,請參閱 samuelcolvin/pydantic#710。

您可以使用所有標准的pydantic字段類型,並且結果數據類將與標准庫 dataclass 裝飾器創建的數據類相同。

可以通過 pydantic_model 訪問底層模型及其模式。 另外,可以通過 dataclasses.field 指定需要 default_factory 的字段。

import dataclasses
from typing import List
from pydantic.dataclasses import dataclass


@dataclass
class User:
    id: int
    name: str = 'John Doe'
    friends: List[int] = dataclasses.field(default_factory=lambda: [0])


user = User(id='42')
print(user.__pydantic_model__.schema())
"""
{
    'title': 'User',
    'type': 'object',
    'properties': {
        'id': {'title': 'Id', 'type': 'integer'},
        'name': {
            'title': 'Name',
            'default': 'John Doe',
            'type': 'string',
        },
        'friends': {
            'title': 'Friends',
            'default': [0],
            'type': 'array',
            'items': {'type': 'integer'},
        },
    },
    'required': ['id'],
}
"""

pydantic.dataclasses.dataclass 的參數與標准裝飾器相同,除了一個額外的關鍵字參數 config,它與 Config 的含義相同。

警告

v1.2之后,必須安裝 Mypy 插件 來對 Pydantic 數據類進行類型檢查。

有關結合驗證器與數據類的更多信息,請參見 [dataclass驗證器](# 3.3.7 Dataclass 驗證器)。

3.7.1 嵌套的數據類

在數據類和普通模型中都支持嵌套數據類。

from pydantic import AnyUrl
from pydantic.dataclasses import dataclass


@dataclass
class NavbarButton:
    href: AnyUrl


@dataclass
class Navbar:
    button: NavbarButton


navbar = Navbar(button=('https://example.com',))
print(navbar)
#> Navbar(button=NavbarButton(href=AnyUrl('https://example.com', scheme='https',
#> host='example.com', tld='com', host_type='domain')))

數據類屬性可以由元組、字典或數據類本身的實例填充。

3.7.2 標准庫的數據類型和pydantic的數據類型

3.7.2.1 將標准庫的數據類型轉換成pydantic的數據類型

可以使用 pydantic.dataclasses.dataclass 來裝飾標准庫的數據類(嵌套或非嵌套),從而使其可以很容易地轉換成 pydantic 數據類。

import dataclasses
from datetime import datetime
from typing import Optional

import pydantic


@dataclasses.dataclass
class Meta:
    modified_date: Optional[datetime]
    seen_count: int


@dataclasses.dataclass
class File(Meta):
    filename: str


File = pydantic.dataclasses.dataclass(File)

file = File(
    filename=b'thefilename',
    modified_date='2020-01-01T00:00',
    seen_count='7',
)
print(file)
#> File(modified_date=datetime.datetime(2020, 1, 1, 0, 0), seen_count=7,
#> filename='thefilename')

try:
    File(
        filename=['not', 'a', 'string'],
        modified_date=None,
        seen_count=3,
    )
except pydantic.ValidationError as e:
    print(e)
    """
    1 validation error for File
    filename
      str type expected (type=type_error.str)
    """

3.7.2.2 從標准庫的 dataclasses 繼承

標准庫數據類(嵌套或非嵌套)也可以繼承,pydantic將自動驗證所有繼承的字段。

import dataclasses

import pydantic


@dataclasses.dataclass
class Z:
    z: int


@dataclasses.dataclass
class Y(Z):
    y: int = 0


@pydantic.dataclasses.dataclass
class X(Y):
    x: int = 0


foo = X(x=b'1', y='2', z='3')
print(foo)
#> X(z=3, y=2, x=1)

try:
    X(z='pika')
except pydantic.ValidationError as e:
    print(e)
    """
    1 validation error for X
    z
      value is not a valid integer (type=type_error.integer)
    """

3.7.2.3 將 BaseModel 與標准庫的數據類型一起使用

請記住,與 BaseModel 混合使用時,標准庫數據類(嵌套或非嵌套)會自動轉換為pydantic數據類!此外,生成的pydantic數據類將具有與原始配置完全相同的配置 (orderfrozen 等)。

import dataclasses
from datetime import datetime
from typing import Optional

from pydantic import BaseModel, ValidationError


@dataclasses.dataclass(frozen=True)
class User:
    name: str


@dataclasses.dataclass
class File:
    filename: str
    last_modification_time: Optional[datetime] = None


class Foo(BaseModel):
    file: File
    user: Optional[User] = None


file = File(
    filename=['not', 'a', 'string'],
    last_modification_time='2020-01-01T00:00',
)  # nothing is validated as expected
print(file)
#> File(filename=['not', 'a', 'string'],
#> last_modification_time='2020-01-01T00:00')

try:
    Foo(file=file)
except ValidationError as e:
    print(e)
    """
    1 validation error for Foo
    file -> filename
      str type expected (type=type_error.str)
    """

foo = Foo(file=File(filename='myfile'), user=User(name='pika'))
try:
    foo.user.name = 'bulbi'
except dataclasses.FrozenInstanceError as e:
    print(e)
    #> cannot assign to field 'name'

3.7.2.4 使用自定義類型

由於標准庫數據類被自動轉換為使用自定義類型添加驗證,因此可能會導致一些意外行為。在這種情況下,您可以簡單地在配置中添加 arbitrary_types_allowed

import dataclasses

import pydantic


class ArbitraryType:
    def __init__(self, value):
        self.value = value

    def __repr__(self):
        return f'ArbitraryType(value={self.value!r})'


@dataclasses.dataclass
class DC:
    a: ArbitraryType
    b: str


# valid as it is a builtin dataclass without validation
my_dc = DC(a=ArbitraryType(value=3), b='qwe')

try:
    class Model(pydantic.BaseModel):
        dc: DC
        other: str

    Model(dc=my_dc, other='other')
except RuntimeError as e:  # invalid as it is now a pydantic dataclass
    print(e)
    """
    no validator found for <class
    'dataclasses_arbitrary_types_allowed.ArbitraryType'>, see
    `arbitrary_types_allowed` in Config
    """


class Model(pydantic.BaseModel):
    dc: DC
    other: str

    class Config:
        arbitrary_types_allowed = True


m = Model(dc=my_dc, other='other')
print(repr(m))
#> Model(dc=DC(a=ArbitraryType(value=3), b='qwe'), other='other')

3.7.3 初始化鈎子

初始化數據類時,可以在 __post_init_post_parse__ 的幫助下在驗證后執行代碼。 該方法與 __post_init__ 不同,后者在驗證之前執行代碼。

from pydantic.dataclasses import dataclass


@dataclass
class Birth:
    year: int
    month: int
    day: int


@dataclass
class User:
    birth: Birth

    def __post_init__(self):
        print(self.birth)
        #> {'year': 1995, 'month': 3, 'day': 2}

    def __post_init_post_parse__(self):
        print(self.birth)
        #> Birth(year=1995, month=3, day=2)


user = User(**{'birth': {'year': 1995, 'month': 3, 'day': 2}})
#> {'year': 1995, 'month': 3, 'day': 2}
#> Birth(year=1995, month=3, day=2)

v1.0版本開始,任何使用 dataclasses.InitVar 注解的字段都將會傳遞給 __post_init____post_init_post_parse__

from dataclasses import InitVar
from pathlib import Path
from typing import Optional

from pydantic.dataclasses import dataclass


@dataclass
class PathData:
    path: Path
    base_path: InitVar[Optional[Path]]

    def __post_init__(self, base_path):
        print(f'Received path={self.path!r}, base_path={base_path!r}')
        #> Received path='world', base_path='/hello'

    def __post_init_post_parse__(self, base_path):
        if base_path is not None:
            self.path = base_path / self.path


path_data = PathData('world', base_path='/hello')
# Received path='world', base_path='/hello'
assert path_data.path == Path('/hello/world')

3.7.3.1 與標准庫數據類的區別

請注意,來自Python 標准庫的 dataclasses.dataclass 僅實現 post_init 方法,因為它不運行驗證步驟。

當用 pydantic.dataclasses.dataclass 代替 dataclasses.dataclass 的使用時,建議將 post_init 方法中執行的代碼移到 post_init_post_parse 方法中,並且只保留需要在驗證之前執行的部分代碼。

3.7.4 JSON 轉儲

Pydantic數據類不具有 .json() 函數。 要將它們轉儲為JSON,您將需要使用 pydantic_encoder,如下所示:

import dataclasses
import json
from typing import List

from pydantic.dataclasses import dataclass
from pydantic.json import pydantic_encoder


@dataclass
class User:
    id: int
    name: str = 'John Doe'
    friends: List[int] = dataclasses.field(default_factory=lambda: [0])


user = User(id='42')
print(json.dumps(user, indent=4, default=pydantic_encoder))
"""
{
    "id": 42,
    "name": "John Doe",
    "friends": [
        0
    ]
}
"""

3.8 驗證裝飾器

validate_arguments 裝飾器允許在調用函數之前使用函數的注解來解析和驗證傳遞給函數的參數。 雖然在后台使用了相同的模型創建和初始化方法,但它提供了一種非常簡單的方法,以最少的樣板將驗證應用於代碼。

Beta 測試中

validate_arguments 裝飾器仍處於測試中,它是在v1.5中臨時添加到pydantic的。在未來的版本中,它可能會有很大的變化,它的接口直到v2才會穩定。在它還處於臨時階段時,來自社區的反饋將非常有用;請在#1205進行評論或者創建一個新問題。

用法示例:

from pydantic import validate_arguments, ValidationError


@validate_arguments
def repeat(s: str, count: int, *, separator: bytes = b'') -> bytes:
    b = s.encode()
    return separator.join(b for _ in range(count))


a = repeat('hello', 3)
print(a)
#> b'hellohellohello'

b = repeat('x', '4', separator=' ')
print(b)
#> b'x x x x'

try:
    c = repeat('hello', 'wrong')
except ValidationError as exc:
    print(exc)
    """
    1 validation error for Repeat
    count
      value is not a valid integer (type=type_error.integer)
    """

3.8.1 參數類型

參數類型是從函數的類型注解進行推斷的,沒有類型注解的參數被認為是 Any 類型的。因為 validate_arguments 內部使用標准的 BaseModel,因此,[字段類型](# 3.2 字段類型) 中列出的所有類型都可以被驗證,包括 pydantic 模型和[自定義數據類型](# 3.2.7 自定義數據類型)。與pydantic的其余部分一樣,類型可以在傳遞給實際函數之前由裝飾器強制執行轉換:

from pathlib import Path
from typing import Pattern, Optional

from pydantic import validate_arguments, DirectoryPath


@validate_arguments
def find_file(path: DirectoryPath, regex: Pattern, max=None) -> Optional[Path]:
    for i, f in enumerate(path.glob('**/*')):
        if max and i > max:
            return
        if f.is_file() and regex.fullmatch(str(f.relative_to(path))):
            return f


print(find_file('/etc/', '^sys.*'))
#> /etc/sysctl.conf
print(find_file('/etc/', '^foobar.*', max=3))
#> None

需要注意的是,通過將它們作為字符串傳遞,path 和 regex 分別被裝飾器轉換為 Path 對象和正則表達式。max 沒有類型注釋,因此裝飾器將其視為 Any 類型。

這樣的類型強制轉換可能非常有用,但也會造成混亂或者不是我們想要的,請參閱[下面](# 3.8.7.2 強制和嚴格)關於 validate_arguments 在這方面的限制的討論。

3.8.2 函數簽名

validate_arguments 裝飾器被設計為使用所有可能的參數配置和下面這些所有可能的組合來處理函數:

  • 有或者沒有默認值的位置參數或關鍵字參數
  • 通過 * (通常為 *args) 定義的可變位置參數
  • 通過 ** (通常為 **kwargs) 定義的可變關鍵字參數
  • 僅限關鍵字參數,即 *, 后面的參數
  • 僅限位置參數,即 , / 前面的參數(Python3.8的新功能)

下面的示例演示了上面所提到的所有參數類型:

from pydantic import validate_arguments


@validate_arguments
def pos_or_kw(a: int, b: int = 2) -> str:
    return f'a={a} b={b}'


print(pos_or_kw(1))
#> a=1 b=2
print(pos_or_kw(a=1))
#> a=1 b=2
print(pos_or_kw(1, 3))
#> a=1 b=3
print(pos_or_kw(a=1, b=3))
#> a=1 b=3


@validate_arguments
def kw_only(*, a: int, b: int = 2) -> str:
    return f'a={a} b={b}'


print(kw_only(a=1))
#> a=1 b=2
print(kw_only(a=1, b=3))
#> a=1 b=3


@validate_arguments
def pos_only(a: int, b: int = 2, /) -> str:  # python 3.8 only
    return f'a={a} b={b}'


print(pos_only(1))
#> a=1 b=2
print(pos_only(1, 2))
#> a=1 b=2


@validate_arguments
def var_args(*args: int) -> str:
    return str(args)


print(var_args(1))
#> (1,)
print(var_args(1, 2))
#> (1, 2)
print(var_args(1, 2, 3))
#> (1, 2, 3)


@validate_arguments
def var_kwargs(**kwargs: int) -> str:
    return str(kwargs)


print(var_kwargs(a=1))
#> {'a': 1}
print(var_kwargs(a=1, b=2))
#> {'a': 1, 'b': 2}


@validate_arguments
def armageddon(
    a: int,
    /,  # python 3.8 only
    b: int,
    c: int = None,
    *d: int,
    e: int,
    f: int = None,
    **g: int,
) -> str:
    return f'a={a} b={b} c={c} d={d} e={e} f={f} g={g}'


print(armageddon(1, 2, e=3))
#> a=1 b=2 c=None d=() e=3 f=None g={}
print(armageddon(1, 2, 3, 4, 5, 6, c=7, e=8, f=9, g=10, spam=11))
#> a=1 b=2 c=7 d=(4, 5, 6) e=8 f=9 g={'spam': 11}

3.8.3 與 mypy 一起使用

validate_arguments 裝飾器應該與mypy配合使用,因為它被定義為返回與裝飾的函數具有相同簽名的函數。 唯一的限制是,由於我們誘使mypy認為裝飾器返回的函數與被裝飾的函數相同; 要訪問[原始函數](# 3.8.4 原始函數)或其他屬性,將需要 type: ignore。

3.8.4 原始函數

被裝飾的原始函數是可訪問的,如果在某些情況下您信任輸入參數並希望以最高性能的方式調用該函數,則這是很有用的(請參見下面的[性能說明](# 3.8.7.3 性能)):

from pydantic import validate_arguments


@validate_arguments
def repeat(s: str, count: int, *, separator: bytes = b'') -> bytes:
    b = s.encode()
    return separator.join(b for _ in range(count))


a = repeat('hello', 3)
print(a)
#> b'hellohellohello'

b = repeat.raw_function('good bye', 2, separator=b', ')
print(b)
#> b'good bye, good bye'

3.8.5 異步函數

validate_arguments 裝飾器也可以在異步函數上使用:

import asyncio
from pydantic import PositiveInt, ValidationError, validate_arguments


@validate_arguments
async def get_user_email(user_id: PositiveInt):
    # `conn` is some fictional connection to a database
    email = await conn.execute('select email from users where id=$1', user_id)
    if email is None:
        raise RuntimeError('user not found')
    else:
        return email


async def main():
    email = await get_user_email(123)
    print(email)
    #> testing@example.com
    try:
        await get_user_email(-4)
    except ValidationError as exc:
        print(exc.errors())
        """
        [
            {
                'loc': ('user_id',),
                'msg': 'ensure this value is greater than 0',
                'type': 'value_error.number.not_gt',
                'ctx': {'limit_value': 0},
            },
        ]
        """


asyncio.run(main())

3.8.6 自定義配置

可以使用配置設置來自定義 validate_arguments 后面的模型,這等同於在普通模型中設置 Config 子類。

警告

@validate_arguments 尚不支持允許配置別名的 Config 的 fields 和 alias_generator 屬性,使用它們會引發錯誤。

使用裝飾器的 config 關鍵字參數設置配置,該參數可以是配置類,也可以是屬性字典,這個屬性字典之后會被轉換成類。

from pydantic import ValidationError, validate_arguments


class Foobar:
    def __init__(self, v: str):
        self.v = v

    def __add__(self, other: 'Foobar') -> str:
        return f'{self} + {other}'

    def __str__(self) -> str:
        return f'Foobar({self.v})'


@validate_arguments(config=dict(arbitrary_types_allowed=True))
def add_foobars(a: Foobar, b: Foobar):
    return a + b


c = add_foobars(Foobar('a'), Foobar('b'))
print(c)
#> Foobar(a) + Foobar(b)

try:
    add_foobars(1, 2)
except ValidationError as e:
    print(e)
    """
    2 validation errors for AddFoobars
    a
      instance of Foobar expected (type=type_error.arbitrary_type;
    expected_arbitrary_type=Foobar)
    b
      instance of Foobar expected (type=type_error.arbitrary_type;
    expected_arbitrary_type=Foobar)
    """

3.8.7 限制

validate_arguments 是臨時發布的,沒有添加任何附加功能(稍后可能會添加),請參閱#1205了解更多有關此功能的討論。

3.8.7.1 驗證異常

當前,如果驗證失敗,則會引發標准的pydantic ValidationError,請參閱[模型錯誤處理](# 3.1.4 錯誤處理)。

這很有用,因為它的 str() 方法提供了所發生錯誤的有用的詳細信息,當將錯誤暴露給最終用戶時,.errors() 和 .json() 之類的方法很有用,但是 ValidationError 繼承自 ValueError 而不是 TypeError,這可能是意外的,因為Python會在參數無效或缺失時引發TypeError。 將來可以通過允許自定義錯誤或在默認情況下引發其他異常,或同時解決兩者來解決此問題。

3.8.7.2 強制和嚴格

pydantic目前傾向於嘗試強制類型轉換,而不是在類型錯誤的情況下引發異常,請參見[模型數據轉換](# 3.1.16 數據轉換),並且 validate_arguments 采用了相同的機制。

有關此問題的討論,請參見#1098 和帶有 “strictness” 標簽的其他問題。 如果pydantic將來獲得 “strict” 模式,validate_arguments 將具有使用此模式的選項,它甚至可能成為裝飾器的默認模式。

3.8.7.3 性能

我們已經盡了最大的努力使pydantic盡可能地具有高性能(請參閱[基准測試](# 4. 基准測試),並且僅在定義函數時才執行一次參數檢查和模型創建,但是與調用原始函數相比,使用 validate_arguments 裝飾器仍然會對性能產生影響。

在許多情況下,這幾乎沒有或沒有明顯的影響,但是請注意,validate_arguments 不是強類型語言中的函數定義的等價物或替代項,並且永遠不會。

3.8.7.4 返回值

函數的返回值沒有根據它的返回類型注解進行驗證,這可以在將來作為一個選項添加。

3.8.7.5 配置和驗證器

不支持自定義的 Config 上的 fields 和 alias_generator ,參見 [上面](# 3.8.6 自定義配置)。

[驗證器](# 3.3 驗證器) 也不支持。

3.8.7.6 模型字段和保留參數

以下名稱不能由參數使用,因為它們可以在內部用於存儲有關函數簽名的信息:

  • v__args
  • v__kwargs
  • v__positional_only
    這些名稱 (以及 args 和 kwargs ) 可能會也可能不會 (取決於函數的簽名) 作為字段出現在可通過 .model 訪問的內部pydantic模型中。因此,目前這個模型並不是特別有用(例如,對於生成一個模式)。

這在將來應該是可以解決的,因為錯誤的提出方式被改變了。

3.9 設置管理

pydantic 最有用的功能之一就是配置管理。如果創建一個繼承自 BaseSettings 的模型,模型初始化器將嘗試從環境變量中讀取未作為關鍵字參數傳遞的任何字段的值。(如果沒有設置匹配的環境變量,仍然使用默認值。)

這使得如下的事情變得很容易:

  • 創建一個明確定義的、具有類型提示的應用程序配置類
  • 自動從環境變量中讀取對配置的修改
  • 在需要的地方手動覆蓋初始化程序中的特定設置(例如在單元測試中)

例如:

import os
from typing import Set

from pydantic import (
    BaseModel,
    BaseSettings,
    PyObject,
    RedisDsn,
    PostgresDsn,
    Field,
)


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


class Settings(BaseSettings):
    auth_key: str
    api_key: str = Field(..., env='my_api_key')

    redis_dsn: RedisDsn = 'redis://user:pass@localhost:6379/1'
    pg_dsn: PostgresDsn = 'postgres://user:pass@localhost:5432/foobar'

    special_function: PyObject = 'math.cos'

    # 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 no prefix, i.e. ""
        fields = {
            'auth_key': {
                'env': 'my_auth_key',
            },
            'redis_dsn': {
                'env': ['service_redis_dsn', 'redis_url']
            }
        }

os.environ['my_api_key'] = 'abc123'
os.environ['my_auth_key'] = '123abc'
print(Settings().dict())
"""
{
    'auth_key': '123abc',
    'api_key': 'abc123',
    'redis_dsn': RedisDsn('redis://user:pass@localhost:6379/1', scheme='redis', user='user', password='pass', host='localhost', host_type='int_domain', port='6379', path='/1'),
    'pg_dsn': PostgresDsn('postgres://user:pass@localhost:5432/foobar', scheme='postgres', user='user', password='pass', host='localhost', host_type='int_domain', port='5432', path='/foobar'),
    'special_function': <built-in function cos>,
    'domains': set(),
    'more_settings': {'foo': 'bar', 'apple': 1},
}
"""

3.9.1 環境變量名稱

以下規則用於確定為給定字段讀取哪些環境變量:

  • 默認情況下,環境變量名是通過連接前綴和字段名構建的:

    • 例如,要覆蓋上面的 special_function,可以使用:

      export my_prefix_special_function='foo.bar'
      
    • 注意 1:默認前綴是空字符串。

    • 注意 2:構建變量名稱時字段別名將被忽略。

  • 自定義環境變量名稱可以使用兩種方法設置:

    • Config.fields['field_name']['env'] (參見上面的 auth_key 和 redis_dsn )
    • Field(..., env=...) (參見上面的 api_key )
  • 當指定了自定義環境變量名稱時,可以提供一個字符串或者一個字符串列表。

    • 當指定一個字符串列表時,順序很重要:第一個發現的值將被使用。
    • 例如,對於上面的 redis_dns,service_redis_dns 比 redis_url 的優先級要高。

警告

由於在查找環境變量來填充設置模型時,v1.0 pydantic不考慮字段別名,所以按照上面的描述使用 env 代替。

為了幫助從別名過渡到 env,當在沒有自定義 env 變量名稱的設置模型上使用別名時,將發出警告。如果您真的打算使用別名,那么可以忽略警告,或者設置 env 來禁止它。

大小寫敏感可以通過 Config 開啟:

from pydantic import BaseSettings


class Settings(BaseSettings):
    redis_host = 'localhost'

    class Config:
        case_sensitive = True

當 case_sensitive 為 True 時,環境變量名必須匹配字段名(可選帶有前綴),因此在本例中只能通過 export redis_host 修改redis_host。如果希望將環境變量命名為全大寫,那么也應該將屬性命名為全大寫。您仍然可以通過 Field(..., env=...) 隨意命名環境變量。

注意

在Windows上,Python的 os 模塊總是將環境變量視為大小寫不敏感的,因此 case_sensitive 配置設置將不起作用——設置將總是被更新為忽略大小寫。

3.9.2 解析環境變量

對於大多數簡單的字段類型(比如 int、float、str 等),環境變量值的解析方式與直接傳遞給初始化器(作為字符串)的方式相同。

復雜類型,如 List、Set、Dict 和子模型,通過將環境變量的值作為JSON編碼的字符串來填充。

3.9.3 Dotenv (.env)支持

注意

dotenv文件解析需要安裝 python-dotenv。這可以通過 pip install python-dotenv 或 pip install pydantic[dotenv] 來完成。

Dotenv文件 (通常命名為 .env ) 是一種常見的模式,它可以方便地以獨立於平台的方式使用環境變量。

dotenv文件遵循與所有環境變量相同的一般原則,看起來如下:

# ignore comment
ENVIRONMENT="production"
REDIS_ADDRESS=localhost:6379
MEANING_OF_LIFE=42
MY_VAR='Hello world'

.env 文件中填充變量后,pydantic支持通過兩種方式加載該文件:

  1. BaseSetting 類中的 Config 上設置 env_file (和 env_file_encoding ,如果你不想使用 OS 的默認編碼的話)。

    class Settings(BaseSettings):
        ...
    
        class Config:
            env_file = '.env'
            env_file_encoding = 'utf-8'
    
  2. 使用 _env_file 關鍵字參數 (和 _env_file_encoding,如果需要) 實例化 BaseSettings 派生類:

    settings = Settings(_env_file='prod.env', _env_file_encoding='utf-8')
    

無論哪種情況,傳遞的參數的值都可以是任何有效路徑或文件名,可以是絕對路徑或相對於當前工作目錄的相對路徑。pydantic將從那里通過加載變量並對其進行驗證來為您處理所有事情。

即使使用dotenv文件,pydantic仍將讀取環境變量以及dotenv文件,環境變量將始終優先於從dotenv文件加載的值。

在實例化時(方法2)通過 _env_file 關鍵字參數傳遞文件路徑將覆蓋 Config 類上設置的值(如果有)。如果以上代碼段結合使用,則將加載 prod.env,而將忽略 .env。

還可以通過將 None 作為實例化關鍵字參數傳遞給 BaseSettings 的派生類的初始化方法來使用關鍵字參數覆蓋來告訴Pydantic根本不加載任何文件 (即使在 Config 類中設置了一個文件),例如 settings = Settings(_env_file = None)。

由於使用 python-dotenv 來解析文件,因此可以使用類似於bash的語義(例如 export) (取決於操作系統和環境) 來允許dotenv文件也與 source 一起使用,請參閱 python-dotenv的文檔 以了解更多詳細信息。

3.9.4 安全支持

在文件中放置機密值是一種常見的模式,可以為應用程序提供敏感的配置。

安全文件遵循與dotenv文件相同的主體,只是它只包含一個值,並且文件名用作鍵。 機密文件如下所示:

/var/run/database_password:

super_secret_database_password

一旦有了安全文件,可以使用兩種方式來加載:

  1. BaseSettings 類的 Config 中將 secrets_dir 設置為安全文件所在的目錄:

    class Settings(BaseSettings):
        ...
        database_password: str
    
        class Config:
            secrets_dir = '/var/run'
    
  2. 使用 _secret_dir 關鍵字參數實例化 BaseSettings 的派生類:

    settings = Settings(_secrets_dir='/var/run')
    

無論哪種情況,傳遞的參數值可以是任意有效的目錄,或者到當前工作目錄的相對或絕對路徑。pydantic將從那里通過加載變量並驗證它們來為您處理一切。

即使使用安全目錄,pydantic 仍然會從 dotenv 文件或環境讀取環境變量,dotenv文件和環境變量總是優先於從安全目錄中讀取的值。

在初始化時通過 _secrets_dir 關鍵字參數傳遞的文件路徑將會覆蓋在 Config 上設置的值(如果有的話)。

3.9.4.1 用例:Docker Secrets

Docker Secrets可用於為在Docker容器中運行的應用程序提供敏感的配置。 要在一個pytantic應用程序中使用這些機密,過程很簡單。 有關在Docker中創建、管理和使用機密的更多信息,請參閱Docker官方文檔。

首先,定義 Settings 類:

class Settings(BaseSettings):
    my_secret_data: str

    class Config:
        secrets_dir = '/run/secrets'

注意

默認情況下,Docker使用 /run/secrets 作為目標掛載點。如果你想使用一個不同的位置,改變相應地 Config.secrets_dir

然后,通過 Docker 命令行接口創建機密:

printf "This is a secret" | docker secret create my_secret_data -

最后,運行 Docker 容器內的應用程序並提供新創建的機密:

docker service create --name pydantic-with-secrets --secret my_secret_data pydantic-app:latest

3.9.5 字段值優先級

如果同一個設置字段有多種方式指定一個值,則選擇的值按以下優先級降序確定:

  1. 傳遞給 Settings 類的初始化器的參數。
  2. 環境變量,例如上面描述的 my_prefix_special_function 。
  3. 從 dotenv( .env 文件) 加載的變量。
  4. 從機密目錄加載的變量。
  5. Settings 模型的默認字段值。

3.10 延遲注解

注意

通過 future 導入和 ForwardRef 的延遲注解都需要python 3.7以上。

延遲注解 (如 PEP563 所述) “just work”。

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


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


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

在內部,pydantic將調用類似於 typing.get_type_hints 的方法來解析注解。

在引用類型尚未定義的情況下,可以使用 ForwardRef (盡管在自引用模型的情況下直接引用類型或通過它的字符串引用是一個更簡單的解決方案)。

在某些情況下,一個 ForwardRef 將不能在模型創建期間被解析。例如,當模型將自身引用為字段類型時,就會發生這種情況。當這種情況發生時,您需要在模型創建后調用 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()

print(Foo())
#> a=123 b=None
print(Foo(b={'a': '321'}))
#> a=123 b=Foo(a=321, b=None)

警告

為了將字符串(類型名稱)解析為注解(類型),pydantic需要一個名稱空間字典來執行查找。 為此,它使用 module.__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():
    # List is defined inside the function so is not in the module's
    # global scope!
    from typing import List

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

    print(Model(a=(1, 2)))

解決這個問題超出了pydantic的要求:要么刪除 future 的導入,要么全局聲明類型。

3.10.1 自引用模型

如果創建模型后調用了 update_forward_refs() 函數,則還支持具有自引用模型的數據結構 (果您忘記了,將會提示一條友好的錯誤消息)。

在模型中,可以使用字符串引用尚未構建的模型:

from pydantic import BaseModel


class Foo(BaseModel):
    a: int = 123
    #: The sibling of `Foo` is referenced by string
    sibling: 'Foo' = None


Foo.update_forward_refs()

print(Foo())
#> a=123 sibling=None
print(Foo(sibling={'a': '321'}))
#> a=123 sibling=Foo(a=321, sibling=None)

從python 3.7開始,您還可以按其類型引用它,前提是您導入了 annotations (請參見上文,具體取決於Python和pydantic版本)。

from __future__ import annotations
from pydantic import BaseModel


class Foo(BaseModel):
    a: int = 123
    #: The sibling of `Foo` is referenced directly by type
    sibling: Foo = None


Foo.update_forward_refs()

print(Foo())
#> a=123 sibling=None
print(Foo(sibling={'a': '321'}))
#> a=123 sibling=Foo(a=321, sibling=None)

3.11 與 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'])
print(m.middle_name)  # not a model field!
Model()  # will raise a validation error for age and list_of_ints

可以通過下面的方式使用 mypy 運行你的代碼:

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

如果你在上面的示例代碼上調用 mypy,將會看到 mypy 發現了屬性訪問錯誤:

13: error: "Model" has no attribute "middle_name"

3.11.1 嚴格模式

為了讓您的代碼在使用 --strict-optional 選項時能夠通過,您需要為所有字段使用 Optional[] 或 Optional[] 的別名,並將字段默認值設置為 None。(這是mypy的標准。)

Pydantic提供了一些有用的可選或聯合類型:

  • NoneStr,即 Optional[str]
  • NoneBytes,即 Optional[bytes]
  • StrBytes,即 Union[str, bytes]
  • NoneStrBytes,即 Optional[StrBytes]

這些還不夠,你當然可以自己定義。

3.12 與 devtools 一起使用

注意

python-devtools 由 Pydantic 的主要開發者開發。

python-devtools( pip install devtools )提供了許多在python開發期間有用的工具,包括 debug() 。這是 print() 的一個替代品,它能提供更易閱讀的格式化輸出,並顯式打印語句位於哪個文件的哪個行上,以及打印了什么值。

pydantic通過在大多數公共類上實現 pretty 方法與 devtools 集成。

特別是在檢查模型時,debug() 很有用:

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

from devtools import debug


class Address(BaseModel):
    street: str
    country: str
    lat: float
    lng: float


class User(BaseModel):
    id: int
    name: str
    signup_ts: datetime
    friends: List[int]
    address: Address


user = User(
    id='123',
    name='John Doe',
    signup_ts='2019-06-01 12:22',
    friends=[1234, 4567, 7890],
    address=dict(street='Testing', country='uk', lat=51.5, lng=0),
)
debug(user)
print('\nshould be much easier read than:\n')
print('user:', user)

將會在終端打印如下輸出:

docs/examples/devtools_main.py:31 <module>
    user: User(
        id=123,
        name='John Doe',
        signup_ts=datetime.datetime(2019, 6, 1, 12, 22),
        friends=[
            1234,
            4567,
            7890,
        ],
        address=Address(
            street='Testing',
            country='uk',
            lat=51.5,
            lng=0.0,
        ),
    ) (User)

should be much easier read than:

user: id=123 name='John Doe' signup_ts=datetime.datetime(2019, 6, 1, 12, 22) friends=[1234, 4567, 7890] address=Address(street='Testing', country='uk', lat=51.5, lng=0.0)

4. 基准測試

以下是將pydantic與其他驗證庫進行比較的原始基准測試結果。

pydantic 1.1 43.6μs
attrs + cattrs 19.3.0 1.4x slower 59.9μs
valideer 0.4.2 1.4x slower 61.8μs
marshmallow 3.2.2 2.5x slower 107.6μs
trafaret 2.0.0 3.4x slower 148.7μs
django-rest-framework 3.10.3 12.6x slower 551.2μs
cerberus 1.3.2 26.3x slower 1146.3μs

pydantic 1.1 43.6μs
attrs + cattrs 19.3.0 1.4x slower 59.9μs
valideer 0.4.2 1.4x slower 61.8μs
marshmallow 3.2.2 2.5x slower 107.6μs
trafaret 2.0.0 3.4x slower 148.7μs
django-rest-framework 3.10.3 12.6x slower 551.2μs
cerberus 1.3.2 26.3x slower 1146.3μs

有關測試用例的更多詳細信息,請參見基准代碼。 隨意建議更多軟件包以進行基准測試或改進現有軟件包。

基准測試是使用Python 3.7.4運行的,上面列出的軟件包版本是通過pypi在Ubuntu 18.04上安裝的。

5.Mypy 插件

由於個人不使用 mypy,故略過。

詳情請參考英文文檔。

6.Pycharm 插件

盡管pydantic可以與任何現成的IDE很好地兼容,但JetBrains插件存儲庫中的PyCharm可以使用提供改進的pydantic集成的PyCharm插件。 您可以從插件市場免費安裝插件(PyCharm的Preferences->Plugin->Marketplace->搜索 “pydantic”)。

該插件當前支持以下功能:

  • 對 pydantic.BaseModel.init 進行:
    • 審查
    • 自動補全
    • 類型檢查
  • 對 pydantic.BaseModel 的字段進行:
    • 重構重命名字段會更新 init 調用,並影響子類和超類;
      重構重命名 init 關鍵字參數會更新字段名稱,並影響子類和超類;

可以在官方插件頁面和Github存儲庫中找到更多信息。

7.代碼生成

datamodel-code-generator 項目是一個庫和命令行實用程序,可從幾乎任何數據源生成pydantic模型,包括:

  • OpenAPI 3 (YAML/JSON)
  • JSON 模式
  • JSON/YAML 數據 (將會轉變為 JSON 模式)

當您發現自己有任何JSON可轉換數據但沒有pydantic模型時,此工具將允許您根據需要生成類型安全的模型層次結構。

7.1 安裝

pip install datamodel-code-generator

7.2 示例

在這種情況下,datamodel-code-generator 從一個 JSON 模式文件創建 pydantic 模型:

Person.json:

{
  "$id": "person.json",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Person",
  "type": "object",
  "properties": {
    "first_name": {
      "type": "string",
      "description": "The person's first name."
    },
    "last_name": {
      "type": "string",
      "description": "The person's last name."
    },
    "age": {
      "description": "Age in years.",
      "type": "integer",
      "minimum": 0
    },
    "pets": {
      "type": "array",
      "items": [
        {
          "$ref": "#/definitions/Pet"
        }
      ]
    },
    "comment": {
      "type": "null"
    }
  },
  "required": [
      "first_name",
      "last_name"
  ],
  "definitions": {
    "Pet": {
      "properties": {
        "name": {
          "type": "string"
        },
        "age": {
          "type": "integer"
        }
      }
    }
  }
}

model.py:

# generated by datamodel-codegen:
#   filename:  person.json
#   timestamp: 2020-05-19T15:07:31+00:00
from __future__ import annotations
from typing import Any, List, Optional
from pydantic import BaseModel, Field, conint


class Pet(BaseModel):
    name: Optional[str] = None
    age: Optional[int] = None


class Person(BaseModel):
    first_name: str = Field(..., description="The person's first name.")
    last_name: str = Field(..., description="The person's last name.")
    age: Optional[conint(ge=0)] = Field(None, description='Age in years.')
    pets: Optional[List[Pet]] = None
    comment: Optional[Any] = None

更多信息,請參考 官方文檔

原文鏈接


免責聲明!

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



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