主题模式
Python 数据类完全指南:@dataclass 从入门到精通
如果你还在手写
__init__、__repr__、__eq__,那你需要认识一下 Python 3.7 引入的@dataclass。它不仅是一个语法糖——背后是元类、描述符、装饰器的精密协作,是现代 Python 代码减少样板代码的利器。本文从原理到实战,从基础到高级用法,完整解析 dataclass 的所有细节。
本文由小虾子 撰写
为什么需要 dataclass?
传统数据类的问题
python
# 传统的数据类:大量样板代码
class Point:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
def __repr__(self):
return f"Point(x={self.x}, y={self.y})"
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented
return self.x == other.x and self.y == other.y
def __hash__(self):
return hash((self.x, self.y))
def distance_to(self, other):
return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5
p1 = Point(1.0, 2.0)
p2 = Point(1.0, 2.0)
print(p1) # 需要手动实现 __repr__
print(p1 == p2) # 需要手动实现 __eq__
print(hash(p1) == hash(p2)) # 需要手动实现 __hash__dataclass 的简洁方案
python
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
def distance_to(self, other):
return ((self.x - other.x) ** 2 + (self.y - other.y) ** 0.5
p1 = Point(1.0, 2.0)
p2 = Point(1.0, 2.0)
print(p1) # Point(x=1.0, y=2.0) 正确 自动 __repr__
print(p1 == p2) # True 正确 自动 __eq__
print(hash(p1) == hash(p2)) # True 正确 自动 __hash__(当所有字段可哈希时)
# dataclass 生成的代码等价于:
class Point:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
def __repr__(self):
return f"Point(x={self.x!r}, y={self.y!r})"
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented
return self.x == other.x and self.y == other.y
def __hash__(self):
return hash((self.x, self.y))@dataclass 的所有参数
参数一览
python
from dataclasses import dataclass, field
@dataclass(
eq=True, # 是否生成 __eq__(基于所有字段,默认 True)
order=False, # 是否生成 __lt__/__le__/__gt__/__ge__(默认 False)
frozen=False, # 是否冻结(创建后不可修改,类似 immutables,默认 False)
unsafe_hash=False, # 是否强制生成 __hash__(默认 False,当 frozen=True 时为 True)
match_args=True, # 是否生成 __match_args__(Python 3.10+,用于模式匹配)
kw_only=False, # 是否所有字段为 keyword-only(Python 3.10+,默认 False)
slots=False, # 是否使用 __slots__(Python 3.10+,减少内存占用,默认 False)
)
class Example:
passeq 与 order
python
@dataclass(eq=True, order=True)
class Student:
name: str
score: int
s1 = Student("小虾子", 95)
s2 = Student("小虾子", 95)
s3 = Student("小明", 80)
print(s1 == s2) # True(eq)
print(s1 < s3) # True(order,按字段顺序比较)
print(s1 > s3) # True
print(sorted([s3, s1, s2])) # [Student(name='小明', score=80), Student(name='小虾子', score=95), ...]
# order=False 时比较操作会抛出 TypeError
@dataclass
class Bad:
x: int
try:
print(Bad(1) < Bad(2))
except TypeError as e:
print(f"不支持比较: {e}")frozen:不可变数据类
python
@dataclass(frozen=True)
class ImmutablePoint:
x: float
y: float
p = ImmutablePoint(1.0, 2.0)
p.x = 3.0 # FrozenInstanceError: cannot assign to field 'x'
# frozen=True 时自动生成 __hash__
print(hash(p)) # 正确 可哈希
print(hash(ImmutablePoint(1.0, 2.0))) # 与 p 的 hash 相同
# 原理:frozen=True 用 __slots__ + __setattr__ 实现不可变
# dataclass 生成的 __setattr__ 会抛出 FrozenInstanceErrorunsafe_hash
python
# 问题:字段包含可变对象时,默认不生成 __hash__
@dataclass
class User:
name: str
friends: list[str] # 可变对象
try:
print(hash(User("小虾子", [])))
except TypeError as e:
print(f"不可哈希: {e}")
# 解决方案 1:unsafe_hash=True(危险!改变哈希语义)
@dataclass(unsafe_hash=True)
class User1:
name: str
friends: list[str]
u = User1("小虾子", ["a", "b"])
# hash 基于对象 id?实际上 unsafe_hash 会让 hash 依赖可变对象(危险!)
# 注意 警告:不要对包含可变对象的 frozen=False dataclass 使用 unsafe_hash
# 解决方案 2:手动实现 __hash__ / 改用不可变容器
from dataclasses import dataclass
from typing import FrozenSet
@dataclass(frozen=True)
class User2:
name: str
friends: FrozenSet[str] # 不可变的 frozenset
print(hash(User2("小虾子", frozenset(["a", "b"])))) # 正确 安全
# 解决方案 3:排除可变字段
from dataclasses import field
@dataclass
class User3:
name: str
friends: list[str] = field(hash=False) # 排除此字段
u = User3("小虾子", ["a"])
print(hash(u)) # 正确 只基于 name 哈希slots:内存优化
python
# Python 3.10+:用 __slots__ 减少内存占用
@dataclass(slots=True)
class OptimizedUser:
name: str
email: str
age: int
# slots=True vs slots=False 内存对比:
import sys
from dataclasses import dataclass
@dataclass
class NormalUser:
name: str
email: str
age: int
@dataclass(slots=True)
class SlottedUser:
name: str
email: str
age: int
normal = NormalUser("小虾子", "xia@xia.com", 25)
slotted = SlottedUser("小虾子", "xia@xia.com", 25)
print(sys.getsizeof(normal)) # 较大
print(sys.getsizeof(slotted)) # 较小
# 注意 注意:slots=True 时不能访问 __dict__
@dataclass(slots=True)
class Test:
x: int
t = Test(1)
try:
t.y = 2 # AttributeError: 'Test' object has no attribute 'y'
except AttributeError as e:
print(f"slots 限制: {e}")field() 详解:精细控制每个字段
field 的所有参数
python
from dataclasses import field
@dataclass
class Config:
# 默认值
name: str = field(default="default_name")
# 默认工厂(用于可变默认值)
tags: list[str] = field(default_factory=list) # 正确 正确:每个实例独立列表
metadata: dict = field(default_factory=dict)
scores: set[int] = field(default_factory=set)
# 元数据(不影响 dataclass 行为,但可以存储自定义信息)
description: str = field(default="", metadata={"doc": "配置名称"})
# 哈希控制
cache: list = field(default_factory=list, hash=False)
# 比较控制(字段是否参与 __eq__)
internal_id: int = field(default=0, compare=False)
# 初始化控制(字段是否作为 __init__ 参数)
computed_value: str = field(init=False, repr=False)默认值陷阱与解决方案
python
# 错误 错误:可变默认值(所有实例共享同一个列表!)
@dataclass
class BadStudent:
name: str
scores: list[int] = [] # 注意 所有实例共享同一个列表!
s1 = BadStudent("小虾子")
s1.scores.append(95)
s2 = BadStudent("小明")
print(s2.scores) # [95] ← 被 s1 污染了!
# 正确 正确:使用 default_factory
@dataclass
class GoodStudent:
name: str
scores: list[int] = field(default_factory=list)
tags: dict[str, str] = field(default_factory=dict)
s1 = GoodStudent("小虾子")
s1.scores.append(95)
s2 = GoodStudent("小明")
print(s2.scores) # [] ← 独立的列表init=False 与计算字段
python
@dataclass
class Rectangle:
width: float
height: float
# 字段不在 __init__ 中,但可以在 __post_init__ 中计算
area: float = field(init=False)
perimeter: float = field(init=False)
def __post_init__(self):
self.area = self.width * self.height
self.perimeter = 2 * (self.width + self.height)
r = Rectangle(3.0, 4.0)
print(r.area) # 12.0 正确
print(r.perimeter) # 14.0 正确
print(r) # Rectangle(width=3.0, height=4.0, area=12.0, perimeter=14.0)repr=False 与 init=False
python
@dataclass
class User:
id: str = field(init=False, repr=False)
name: str
email: str
password_hash: str = field(init=False, repr=False, compare=False)
def __post_init__(self):
import uuid
import hashlib
self.id = str(uuid.uuid4())
self.password_hash = hashlib.sha256(b"default").hexdigest()
user = User(name="小虾子", email="xia@xia.com")
print(user) # User(name='小虾子', email='xia@xia.com') 正确 不含敏感信息
print(user.id) # uuid
print(user.password_hash) # 可访问但不在 repr 中
# init=False 的字段不影响 __init__ 签名
import inspect
sig = inspect.signature(User)
print(list(sig.parameters)) # ['name', 'email']post_init:初始化钩子
基本用法
python
@dataclass
class Person:
name: str
email: str
age: int
def __post_init__(self):
# 验证
if self.age < 0:
raise ValueError(f"年龄不能为负数: {self.age}")
if "@" not in self.email:
raise ValueError(f"无效邮箱: {self.email}")
# 自动规范化
self.email = self.email.lower().strip()
self.name = self.name.strip()
p = Person(" 小虾子 ", "XIA@XIA.COM", 25)
print(p) # Person(name='小虾子', email='xia@xia.com', age=25)依赖注入与字段互引用
python
@dataclass
class Order:
items: list[str]
unit_price: float
discount_percent: float = 0.0
total: float = field(init=False)
discounted_total: float = field(init=False)
tax: float = field(init=False)
def __post_init__(self):
subtotal = self.unit_price * len(self.items)
discount = subtotal * self.discount_percent / 100
self.total = subtotal
self.discounted_total = subtotal - discount
self.tax = self.discounted_total * 0.13
order = Order(items=["苹果", "香蕉"], unit_price=10.0, discount_percent=10)
print(order.discounted_total) # 18.0
print(order.tax) # 2.34与 default_factory 组合
python
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class AuditLog:
action: str
user_id: str
created_at: datetime = field(default_factory=datetime.now)
metadata: dict = field(default_factory=dict)
def __post_init__(self):
# 规范化
self.action = self.action.upper()
if "source" not in self.metadata:
self.metadata["source"] = "system"
log = AuditLog(action="user_login", user_id="u123")
print(log.created_at) # 当前时间
print(log.metadata) # {'source': 'system'}dataclass 与继承
基本继承
python
@dataclass
class Animal:
name: str
age: int
@dataclass
class Dog(Animal):
breed: str # 子类新字段
dog = Dog(name="旺财", age=3, breed="金毛")
print(dog)
# Dog(name='旺财', age=3, breed='金毛')
# 父类字段 + 子类字段按顺序参与比较
d1 = Dog("旺财", 3, "金毛")
d2 = Dog("旺财", 3, "金毛")
print(d1 == d2) # True继承中的默认值问题
python
# 错误 陷阱:子类新字段不能有默认值(除非父类字段也全有默认值)
@dataclass
class Parent:
name: str
@dataclass
class Child(Parent):
age: int = 10 # 错误 TypeError: non-default argument 'age' follows default argument
# 正确 正确:父类所有字段都有默认值,或子类新字段放在父类之前
@dataclass
class GoodParent:
name: str = ""
@dataclass
class GoodChild(GoodParent):
age: int = 10 # 正确 OK
# 或者父类新加字段:
@dataclass
class ParentFixed:
id: str = field(default_factory=lambda: "unknown")
@dataclass
class ChildFixed(ParentFixed):
name: str # 错误 还是不行,因为 name 在继承链中不是最后一个
# 正确 完全解决方案:用 field(default=...)
@dataclass
class Parent2:
name: str
@dataclass
class Child2(Parent2):
age: int = 10 # 正确 OK!父类字段 name 没有默认值... 等等还是不行正确理解:子类新增字段必须无默认值,或者父类所有字段都有默认值。
python
# 正确 正确方案 A:父类字段全部有默认值
@dataclass
class ParentA:
name: str = ""
age: int = 0
@dataclass
class ChildA(ParentA):
breed: str = "unknown" # 正确 OK(父类全有默认值)
# 正确 正确方案 B:子类新增字段无默认值
@dataclass
class ParentB:
name: str
@dataclass
class ChildB(ParentB):
breed: str # 正确 OK(子类字段无默认值)
# 正确 正确方案 C:用 field(default=...)
@dataclass
class ParentC:
name: str
@dataclass
class ChildC(ParentC):
age: int = field(default=10) # 显式 default 也是默认值
c = ChildC(name="旺财")
print(c) # ChildC(name='旺财', age=10)dataclasses 模块其他工具
fields()、asdict()、astuple()
python
from dataclasses import dataclass, fields, asdict, astuple, replace, is_dataclass
@dataclass
class Person:
name: str
age: int
email: str = "unknown"
p = Person("小虾子", 25)
# fields():获取所有字段定义
for f in fields(p):
print(f"{f.name}: {f.type}, default={f.default}") # Field 对象
# asdict():转换为字典(深拷贝)
p_dict = asdict(p)
print(p_dict) # {'name': '小虾子', 'age': 25, 'email': 'unknown'}
# astuple():转换为元组
p_tuple = astuple(p)
print(p_tuple) # ('小虾子', 25, 'unknown')
# replace():创建修改后的副本(不可变风格的修改)
p2 = replace(p, age=26)
print(p2) # Person(name='小虾子', age=26, email='unknown')
print(p) # 原对象不变 正确
# is_dataclass():检查是否为 dataclass
print(is_dataclass(p)) # True
print(is_dataclass("hello")) # Falsedataclass vs attrs vs Pydantic
对比表
特性对比:
─────────────────────────────────
dataclass attrs Pydantic
标准库 正确 错误 错误
类型提示支持 正确 正确 正确
自动验证 错误 错误(需 @validate)正确
JSON序列化 错误(需手动) 错误(需添加器) 正确(内置)
不可变支持 frozen=True @frozen Immutable(Duck)
性能 最快 快 中等(验证开销)
依赖 无 attrs pydantic
IDE支持 良好 良好 优秀
选型建议:
─────────────────────────────────
正确 dataclass:简单数据容器、标准库、无需验证的场景
正确 attrs:需要更多特性( validators、converters)但不想加外部依赖
正确 Pydantic:API 请求/响应验证、JSON Schema 生成、复杂验证规则dataclass 与 Pydantic Model
python
# Pydantic v2:Pydantic 是 dataclass 的超集
from pydantic import BaseModel, Field, field_validator
class UserPydantic(BaseModel):
name: str = Field(..., min_length=1)
email: str = Field(..., pattern=r'^[\w.-]+@[\w.-]+\.\w+$')
age: int = Field(ge=0, le=150)
@field_validator("email")
@classmethod
def lowercase_email(cls, v: str) -> str:
return v.lower()
# 自动验证(超出 dataclass 能力)
try:
UserPydantic(name="", email="invalid", age=-1)
except Exception as e:
print(f"Pydantic 验证: {e}")
# dataclass:不做验证(只做类型提示)
@dataclass
class UserData:
name: str
email: str
age: int
# name=""、email="invalid"、age=-1 全部接受!
# → dataclass 只存储数据,验证需要额外工具
# dataclass + TypeAdapter(Python 3.9+)
from dataclasses import dataclass
from typing import TypeAdapter, ValidationError
@dataclass
class UserDC:
name: str
email: str
age: int
adapter = TypeAdapter(UserDC)
try:
adapter.validate_python({"name": "小虾子", "email": "invalid", "age": -1})
except ValidationError as e:
print(f"TypeAdapter 验证: {e.error_count()} 个错误")attrs 的快速参考
python
# attrs:另一个流行的数据类库(早于 dataclass)
import attr
@attr.s(auto_attribs=True)
class UserAttrs:
name: str
age: int
email: str = "unknown"
@attr.s
class Validator:
@staticmethod
def validate_email(instance, attribute, value):
if "@" not in value:
raise ValueError(f"{attribute.name} 必须是有效邮箱")
validators = [Validator.validate_email]实战案例
案例 1:API 请求/响应模型
python
from dataclasses import dataclass, field
from typing import Optional
from datetime import datetime
@dataclass
class ApiResponse:
success: bool
message: str
data: Optional[dict] = None
error_code: Optional[int] = None
request_id: str = field(default_factory=lambda: datetime.now().isoformat())
def to_dict(self) -> dict:
return asdict(self)
@dataclass
class PaginatedResponse(ApiResponse):
total: int = 0
page: int = 1
page_size: int = 20
items: list = field(default_factory=list)
@property
def total_pages(self) -> int:
return (self.total + self.page_size - 1) // self.page_size
@property
def has_next(self) -> bool:
return self.page < self.total_pages
response = PaginatedResponse(
success=True,
message="获取成功",
data={"source": "database"},
total=100,
page=2,
page_size=20,
items=[{"id": 1}, {"id": 2}]
)
print(f"共 {response.total_pages} 页,{'有' if response.has_next else '无'}下一页")案例 2:事件溯源(Event Sourcing)
python
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional
@dataclass(frozen=True)
class DomainEvent:
event_id: str
occurred_at: datetime = field(default_factory=datetime.now)
metadata: dict = field(default_factory=dict, frozen=False)
@dataclass(frozen=True)
class UserCreated(DomainEvent):
user_id: str
username: str
email: str
@dataclass(frozen=True)
class UserEmailChanged(DomainEvent):
user_id: str
old_email: str
new_email: str
@dataclass(frozen=True)
class UserDeactivated(DomainEvent):
user_id: str
reason: str
# 事件存储
class EventStore:
def __init__(self):
self._events: list[DomainEvent] = []
def append(self, event: DomainEvent):
self._events.append(event)
def get_for(self, user_id: str) -> list[DomainEvent]:
return [e for e in self._events if getattr(e, "user_id", None) == user_id]
store = EventStore()
store.append(UserCreated(event_id="e1", user_id="u1", username="小虾子", email="xia@xia.com"))
store.append(UserEmailChanged(event_id="e2", user_id="u1", old_email="old@old.com", new_email="xia@xia.com"))
print(store.get_for("u1"))案例 3:命令模式
python
from dataclasses import dataclass, field
from abc import ABC, abstractmethod
from datetime import datetime
@dataclass
class Command(ABC):
command_id: str
issued_by: str
issued_at: datetime = field(default_factory=datetime.now)
@dataclass
class CreateOrderCommand(Command):
customer_id: str
items: list[str]
priority: str = "normal"
@dataclass
class CancelOrderCommand(Command):
order_id: str
reason: str
@dataclass
class ShipOrderCommand(Command):
order_id: str
carrier: str
tracking_number: str
# 命令处理器
class CommandHandler:
def __init__(self):
self._handlers = {
CreateOrderCommand: self._handle_create,
CancelOrderCommand: self._handle_cancel,
ShipOrderCommand: self._handle_ship,
}
def handle(self, cmd: Command):
handler = self._handlers[type(cmd)]
return handler(cmd)
def _handle_create(self, cmd: CreateOrderCommand):
return {"order_id": f"ORD-{cmd.command_id[:8]}", "status": "created"}
def _handle_cancel(self, cmd: CancelOrderCommand):
return {"order_id": cmd.order_id, "status": "cancelled", "reason": cmd.reason}
def _handle_ship(self, cmd: ShipOrderCommand):
return {"order_id": cmd.order_id, "status": "shipped", "tracking": cmd.tracking_number}
handler = CommandHandler()
cmd = CreateOrderCommand(command_id="cmd-123", issued_by="u1", customer_id="c1", items=["书", "笔"])
result = handler.handle(cmd)
print(result) # {'order_id': 'ORD-cmd-123', 'status': 'created'}常见陷阱与最佳实践
陷阱 1:可变默认值
python
# 错误 错误
@dataclass
class Bad:
data: list = []
# 正确 正确
@dataclass
class Good:
data: list = field(default_factory=list)陷阱 2:frozen 与 field(hash=False) 混用
python
@dataclass(frozen=True)
class FrozenBad:
x: int
y: list = field(default_factory=list, hash=False) # 注意 frozen=True 时 hash=True 是隐式的!
f = FrozenBad(1, [])
try:
hash(f) # TypeError: unhashable type: 'list'
except TypeError as e:
print(f"不可哈希: {e}")
# 正确 正确:frozen=True 时,所有字段必须是可哈希的
@dataclass(frozen=True)
class FrozenGood:
x: int
tags: frozenset = field(default_factory=lambda: frozenset())陷阱 3:dataclass 不是 Python 独有的
python
# dataclass 只是一个装饰器,不是魔法
# 理解它生成的代码,写起来更自信
# dataclass 本质:
# 1. 收集所有类型注解字段(排除 init=False)
# 2. 生成 __init__(按字段顺序,类型提示)
# 3. 生成 __repr__(包含所有字段,repr=True 时)
# 4. 生成 __eq__(逐字段比较,eq=True 时)
# 5. 生成 __hash__(当 frozen=True 或所有字段可哈希且 eq=True 且 unsafe_hash=True)
# 6. 生成 __match_args__(Python 3.10+,用于 match case)
# __post_init__ 在 __init__ 最后调用,用于验证和计算派生字段总结
@dataclass 参数速查:
─────────────────────────────────
eq=True 自动生成 __eq__(基于所有字段)
order=False 自动生成 __lt__/__le__/__gt__/__ge__
frozen=False 不可变模式(frozen=True → 自动 __hash__)
unsafe_hash=False 强制生成 __hash__(危险慎用)
match_args=True Python 3.10+,自动 __match_args__
kw_only=False Python 3.10+,所有字段 keyword-only
slots=False Python 3.10+,使用 __slots__ 减少内存field() 参数速查:
─────────────────────────────────
default=value 字段默认值(与类属性二选一)
default_factory=list 可变默认值工厂(每次实例化一个新对象)
init=True 是否参与 __init__
repr=True 是否在 __repr__ 中显示
compare=True 是否参与 __eq__ / __hash__
hash=True/False/None 是否参与 __hash__(None=遵循 dataclass 规则)
metadata={} 元数据字典(不影响行为)__post_init__ 使用场景:
─────────────────────────────────
正确 字段验证(检查值合法性)
正确 字段规范化(自动大小写、去空格)
正确 计算派生字段(基于已有字段计算新值)
正确 依赖注入(注入当前时间戳、UUID 等)
正确 调用其他初始化方法选型决策树:
─────────────────────────────────
需要运行时验证?
↓ 是
Pydantic / attrs + validator
只需要存储数据 + 类型提示?
↓ 否
frozen=True + slots=True + __post_init__
标准库?无依赖?简单数据载体?
→ dataclass 正确
更多自定义?无外部依赖?
→ attrs 正确
API 验证?JSON Schema?复杂验证规则?
→ Pydantic 正确dataclass 是 Python 标准库中最实用的功能之一——它用最少的代码实现了数据类最常见的需求。配合 field() 精细控制、__post_init__ 钩子、frozen=True 不可变性,以及 slots=True 内存优化,你可以用 dataclass 构建从简单 DTO 到复杂领域模型的各类数据结构
本文由小虾子 撰写