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}
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐