Python 之 dataclasses 和 pydantic 数据解析和校验
dataclasses 是一个标准库,这个模块提供了一个装饰器和一些函数,用于自动为用户自定义的类添加生成的 特殊方法 。例如常见的 __init__();pydantic 则是 Python 中最流行的数据验证和设置管理库之一,利用 Python 类型提示 进行运行时数据验证和解析。包括 FastApi 等大多框架都在使用。
dataclasses
dataclass 装饰会自动给类添加如下方法:
-
__init__:初始化方法 -
__repr__:字符串表示 -
__eq__:相等比较 -
__hash__(如果设置frozen=True) -
__post_init__:初始化后处理
初始化赋值
from dataclasses import dataclass
@dataclass
class InventoryItem:
"""Class for keeping track of an item in inventory."""
name: str
unit_price: float
quantity_on_hand: int = 0
def total_cost(self) -> float:
return self.unit_price * self.quantity_on_hand
item = InventoryItem("Apple", 5.6, 5)
print(item) # InventoryItem(name='Apple', unit_price=5.6, quantity_on_hand=5)
print(item.total_cost()) # 28.0
使用 dataclass 装饰以上类以后,相当于自动添加了如下的初始化方法。
def __init__(self, name: str, unit_price: float, quantity_on_hand: int = 0):
self.name = name
self.unit_price = unit_price
self.quantity_on_hand = quantity_on_hand
当然,如果类里边手动添加了相应的初始化方法的话,上面通过 dataclass 自动添加的方法就会失效(其他 __eq__() 类似)。
from dataclasses import dataclass
@dataclass
class InventoryItem:
"""Class for keeping track of an item in inventory."""
name: str
unit_price: float
quantity_on_hand: int = 0
def __init__(self, name: str, unit_price: float, quantity_on_hand):
self.name = name
self.unit_price = unit_price
self.quantity_on_hand = quantity_on_hand + 5
def total_cost(self) -> float:
return self.unit_price * self.quantity_on_hand
item = InventoryItem("Apple", 5.6, 5)
print(item) # InventoryItem(name='Apple', unit_price=5.6, quantity_on_hand=10)
print(item.total_cost()) # 56.0
初始化后处理
dataclass 自动生成的初始化方法基本上只做赋值的用途,如果除了基本赋值以后,还要做其他额外操作。则需要使用初始化后处理。__post_init__ 是 dataclass 提供的一个特殊方法,它在 __init__ 方法执行完毕后自动被调用,主要用于执行初始化后的额外处理逻辑。
比如下面自动给数量 +5
from dataclasses import dataclass
@dataclass
class InventoryItem:
"""Class for keeping track of an item in inventory."""
name: str
unit_price: float
quantity_on_hand: int = 0
def __post_init__(self):
self.quantity_on_hand += 5
def total_cost(self) -> float:
return self.unit_price * self.quantity_on_hand
item = InventoryItem("Apple", 5.6, 5)
print(item) # InventoryItem(name='Apple', unit_price=5.6, quantity_on_hand=10)
print(item.total_cost()) # 56.0
数据类继承
from dataclasses import dataclass
from typing import Any
@dataclass
class Base:
x: Any = 15.0
y: int = 0
@dataclass
class C(Base):
z: int = 10
x: int = 15
c = C()
print(c) # C(x=15, y=0, z=10)
print(type(C.x)) # <class 'int'>
类变量
由于数据类不直接使用 self 关键字,所以可能不容易区分类变量和实例变量。一般来说,数据类中,没有注解直接赋值的是类变量。
from dataclasses import dataclass
from typing import ClassVar
@dataclass
class MyClass:
# 实例变量(有类型注解)
instance_var1: int
instance_var2: str = "default" # 有默认值的实例变量
# 类变量(没有类型注解)
class_var1 = "shared by all instances"
myclass1 = MyClass(1)
myclass2 = MyClass(2)
MyClass.class_var1 = "Looking"
print(myclass1.class_var1) # Looking
print(myclass2.class_var1) # Looking
在有注解的情况下,如果使用了 ClassVar 声明为了类变量,则也属于类变量。
from dataclasses import dataclass
from typing import ClassVar
@dataclass
class MyClass:
# 实例变量(有类型注解)
instance_var1: int
instance_var2: str = "default" # 有默认值的实例变量
# 注意:如果用了 ClassVar 类型注解,也是类变量
class_var3: ClassVar[int] = 100 # 明确标记为类变量
myclass1 = MyClass(1)
myclass2 = MyClass(2)
MyClass.class_var3 = 50
print(myclass1.class_var3) # 50
print(myclass2.class_var3) # 50
默认工厂函数
dataclass 不允许实例变量使用可变默认值。所有实例会共享同一个列表对象,导致意外修改。
from dataclasses import dataclass
from typing import List
@dataclass
class InventoryItem:
names: List[str] = []
unit_price: float = 2
quantity_on_hand: int = 0
item = InventoryItem(["Apple"], 5.6, 5)
print(item)
# ValueError: mutable default <class 'list'> for field names is not allowed: use default_factory
一般可使用默认工厂函数来替换可变默认值。
from dataclasses import dataclass, field
from typing import List
@dataclass
class InventoryItem:
names: List[str] = field(default_factory=list)
unit_price: float = 2
quantity_on_hand: int = 0
item = InventoryItem(["Apple"], 5.6, 5)
print(item)
# InventoryItem(names=['Apple'], unit_price=5.6, quantity_on_hand=5)
变量顺序
根据默认的变量初始顺序,有默认值的参数必须在无默认值的参数后边。
from dataclasses import dataclass
@dataclass
class InventoryItem:
name: str
quantity_on_hand: int = 0
unit_price: float
item = InventoryItem("Apple", 5.6, 5)
print(item)
TypeError: non-default argument 'unit_price' follows default argument
字典和元组形式
可以使用 asdict 和 astuple 输出数据类实例的字典和元素形式。
from dataclasses import dataclass, asdict, astuple
@dataclass
class InventoryItem:
"""Class for keeping track of an item in inventory."""
name: str
unit_price: float
quantity_on_hand: int = 0
item = InventoryItem("Apple", 5.6, 5)
print(asdict(item))
# {'name': 'Apple', 'unit_price': 5.6, 'quantity_on_hand': 5}
print(astuple(item))
# ('Apple', 5.6, 5)
pydantic
api 文档可参照:https://pydantic.com.cn/api/base_model/
安装
类型提示是基于标准库的,pydantic 则是需要进行安装的。
pip install pydantic
解析赋值
将参数解析赋值到类实例变量里边去。这个也是最基本的功能(不过,pydantic 似乎不支持使用位置参数传参来初始化——必须使用关键字参数)。
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
item = Item(**{
"name": "Looking",
"description": "An optional description",
"price": 2.3,
"tax": 0.25
})
print(item)
# name='Looking' description='An optional description' price=2.3 tax=0.25
当然,如果只是需要解析赋值的话,使用标准库 dataclasses 的 dataclass 也是可以做到的。
from dataclasses import dataclass
@dataclass
class Item:
name: str
description: str
price: float
tax: float | None = None
item = Item(**{
"name": "Looking",
"description": "An optional description",
"price": 2.3,
"tax": 0.25
})
print(item)
# Item(name='Looking', description='An optional description', price=2.3, tax=0.25)
类型提示
量如果没有添加类型提示,会报错未定义。
from pydantic import BaseModel
class InventoryItem(BaseModel):
name: str
unit_price
quantity_on_hand: int = 0
item = InventoryItem(name="Apple", unit_price=5.6, quantity_on_hand=5)
print(item)
NameError: name 'unit_price' is not defined
默认值
在类型提示的同时,还可以设置默认值;这样,当输入没有对应字段数据的时候,会使用默认值。
from pydantic import BaseModel
class Item(BaseModel):
name: str
unit_price: float
quantity_on_hand: int = 5
item = Item(name="Apple", unit_price=5.6)
print(item) # name='Apple' unit_price=5.6 quantity_on_hand=5
类型转换
pydantic 会自动将输入转换成字段所声明的类型。
from pydantic import BaseModel
class Item(BaseModel):
name: str
unit_price: float
quantity_on_hand: int = 0
item = Item(name="Apple", unit_price="5.6", quantity_on_hand=5)
print(item) # name='Apple' unit_price=5.6 quantity_on_hand=5
当然,如果出现转换失败的情况,也仍然会报错。
pydantic_core._pydantic_core.ValidationError: 1 validation error for Item
unit_price
Input should be a valid number, unable to parse string as a number [type=float_parsing, input_value='test5.6', input_type=str]
For further information visit https://errors.pydantic.dev/2.11/v/float_parsing
严格模式
默认是宽松模式,也就是输入的数据类型与声明的即使不一样,但是如果可以进行转换的话,就会自动转换成声明的类型。如果要求输入的类型必须与声明的类型保持一致,则需要使用 strict 来声明严格模式。
from pydantic import BaseModel, ConfigDict
class Item(BaseModel):
name: str
unit_price: float
quantity_on_hand: int = 0
model_config = ConfigDict(strict=True)
item = Item(name="Apple", unit_price="5.6", quantity_on_hand=5)
print(item)
pydantic_core._pydantic_core.ValidationError: 1 validation error for Item
unit_price
Input should be a valid number [type=float_type, input_value='5.6', input_type=str]
可选字段
常规的提示声明默认字段是必选字段,必须字段如果没有赋值的话,会导致报错。
pydantic_core._pydantic_core.ValidationError: 1 validation error for Item
unit_price
Field required [type=missing, input_value={'name': 'Apple'}, input_type=dict]
For further information visit https://errors.pydantic.dev/2.11/v/missing
我们可以使用类似 Optional[float] = None 或 float | None = None 的方式来声明可选字段。
from pydantic import BaseModel
from typing import Optional
class Item(BaseModel):
name: str
unit_price: Optional[float] = None
quantity_on_hand: int = 5
item = Item(name="Apple")
print(item)
# name='Apple' unit_price=None quantity_on_hand=5
嵌套模型
from typing import List
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str
email: str
is_active: bool = True
class Address(BaseModel):
street: str
city: str
zip_code: str
class Company(BaseModel):
name: str
address: Address # 嵌套模型
employees: List[User] # 模型列表
company = Company(
name="Tech Corp",
address={"street": "123 Main St", "city": "SF", "zip_code": "94105"},
employees=[{"id": 1, "name": "Bob", "email": "bob@example.com"}]
)
print(company)
# name='Tech Corp' address=Address(street='123 Main St', city='SF', zip_code='94105') employees=[User(id=1, name='Bob', email='bob@example.com', is_active=True)]
数据校验
比如要求输入的字段必须在指定的范围。
from pydantic import BaseModel, Field
class Item(BaseModel):
name: str
unit_price: float = Field(gt=3, lt=10)
quantity_on_hand: int = 5
item = Item(name="Apple", unit_price=2)
print(item)
如果不满足给定的要求,则进行报错。
pydantic_core._pydantic_core.ValidationError: 1 validation error for Item
unit_price
Input should be greater than 3 [type=greater_than, input_value=2, input_type=int]
For further information visit https://errors.pydantic.dev/2.11/v/greater_than
高级校验
我们还可以从 pydantic 引入 EmailStr, HttpUrl 等高级校验器。
from pydantic import BaseModel, EmailStr, HttpUrl, conint, condecimal
from enum import Enum
class Status(str, Enum):
ACTIVE = "active"
INACTIVE = "inactive"
class UserProfile(BaseModel):
email: EmailStr # 自动验证邮箱格式
website: HttpUrl # 自动验证 URL 格式
age: conint(ge=0, le=150) # 约束整数
score: condecimal(max_digits=5, decimal_places=2) # 约束小数及最大位数
status: Status # 枚举类型
user = UserProfile(email="123@qq.com", website="http://test.com", age=20, score=123.45, status="active")
print(user)
# email='123@qq.com' website=HttpUrl('http://test.com/') age=20 score=Decimal('123.45') status=<Status.ACTIVE: 'active'>
自定义校验
有时候现有的校验方式可能不满足要求,这个时候可以针对字段自定义复杂的校验处理逻辑。
from typing import List
from pydantic import BaseModel, field_validator, model_validator
class Order(BaseModel):
items: List[str]
total_price: float
discount: float = 0.0
@field_validator('discount')
def discount_valid(cls, v):
if v < 0 or v > 1:
raise ValueError('discount must be between 0 and 1')
return v
@model_validator(mode='after')
def check_total(self):
if self.total_price < 0:
raise ValueError('total price cannot be negative')
return self
order = Order(items=["Apple", "Banana"], total_price=-2, discount=.1)
print(order)
pydantic_core._pydantic_core.ValidationError: 1 validation error for Order
Value error, total price cannot be negative [type=value_error, input_value={'items': ['Apple', 'Bana...e': -2, 'discount': 0.1}, input_type=dict]
文件加载和校验
我们也可以从配置文件加载数据并使用 pydantic 进行校验并赋值。
pip install pydantic-settings
比如有配置文件 .env
DB_HOST=localhost
DB_PORT=2345
DB_USER=root
DB_PASSWORD=secret
API_KEY=your_api_key_here
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
DB_HOST: str
DB_PORT: int
DB_USER: str
DB_PASSWORD: str
API_KEY: str
model_config = SettingsConfigDict(env_file=".env")
settings = Settings()
print(settings)
# DB_HOST='localhost' DB_PORT=2345 DB_USER='root' DB_PASSWORD='secret' API_KEY='your_api_key_here'
错误捕获
我们可以使用 ValidationError 异常来捕获校验方面的错误,还可以以 json 的形式输出报错提示。
from pydantic import BaseModel, Field, ValidationError
class Item(BaseModel):
name: str
unit_price: float = Field(gt=3, lt=10)
quantity_on_hand: int = 5
try:
item = Item(name="Apple", unit_price=2)
print(item)
except ValidationError as e:
print(e.json(indent=2))
[
{
"type": "greater_than",
"loc": [
"unit_price"
],
"msg": "Input should be greater than 3",
"input": 2,
"ctx": {
"gt": 3.0
},
"url": "https://errors.pydantic.dev/2.11/v/greater_than"
}
]
模型信息
使用 model_dump 可以返回模型的字典形式。
from pydantic import BaseModel
class InventoryItem(BaseModel):
name: str
unit_price: float = 5.6
quantity_on_hand: int = 0
item = InventoryItem(name="Apple", unit_price=5.6, quantity_on_hand=5)
print(item.model_dump())
# {'name': 'Apple', 'unit_price': 5.6, 'quantity_on_hand': 5}更多推荐



所有评论(0)