一、基础概念
@dataclass 是 Python3.7+ 标准库 dataclasses 提供的类装饰器,核心用途是简化纯数据承载类开发。自动生成 __init__、__repr__、__eq__、__hash__、__lt__ 等冗余魔法方法,剔除样板代码,让数据类代码更简洁、规范、易维护。
适用场景:仅存储数据、无复杂业务逻辑的类,如参数模型、接口DTO、配置类、结构化数据实体等。
本质:
基于类型注解的元编程
核心流程:
扫描解析类的字段注解 → 动态生成各类魔法方法 → 绑定至原类,完成类的自动增强
from dataclasses import dataclass
二、基础用法
在没有 @dataclass 之前,定义一个简单的数据类需要编写大量重复的样板代码。
from dataclasses import dataclass
# 使用前
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self):
return f"Person(name={self.name}, age={self.age})"
def __str__(self):
return f"Person(name={self.name}, age={self.age})"
def __eq__(self, other):
if not isinstance(other, Person):
return False
return self.name == other.name and self.age == other.age
# 使用后
@dataclass
class Person2:
name: str
age: int
# 实例化调用
p1 = Person("Alice", 25)
p2 = Person("Alice1", 25)
p3 = Person2("Alice", 25)
p4 = Person2("Alice1", 25)
print(p1)
print(p1 == p2)
print(p3)
print(p3 == p4)
三、核心参数
@dataclass 装饰器支持多种可选参数,用于控制生成方法的行为及类的特性。
init(bool,默认 True):是否生成__init__方法,默认为True,若为False,则需显式定义__init__方法repr(bool,默认 True):是否生成__repr__方法,生成的字符串包含类名及所有字段名和对应值(以字段定义顺序排列)。可通过 field(repr=False) 排除个别字段。eq(bool,默认 True):是否生成__eq__方法,将实例按字段值进行比较。关闭则使用默认 is 判断。如果已定义__eq__,则忽略此参数。order(bool,默认 False):是否生成排序比较方法 lt、le、gt、ge,按字段顺序比较;要求 eq=True,否则抛出 ValueError。如果类中已定义任一比较方法,则抛出 TypeError。unsafe_hash(bool,默认 False):控制 hash() 方法的生成。默认情况下,如果同时 eq=True 且 frozen=False,则会将 hash 设为 None(实例不可哈希);如果同时 eq=True 且 frozen=True,则生成与字段相关的 hash。设为 True 时,会强制生成 hash(),即使实例可变,这通常只在特殊情况下使用。frozen(bool,默认 False):如果为 True,则生成的类会“冻结”实例,使字段赋值变为只读(在尝试修改字段时抛出异常),模拟不可变对象。注意这对性能有小幅影响:init 不能使用普通赋值,而是通过 object.setattr() 初始化。如果类中已定义 setattr 或 delattr,则抛出 TypeError。match_args(bool,默认 True,自 3.10 起):控制是否生成 match_args 属性(用于结构化模式匹配)。为 True 时,match_args 包含所有非仅关键字字段名的元组(按定义顺序);若字段中已有定义则不会覆盖(3.10+)。kw_only(bool,默认 False,自 3.10 起):将所有字段标记为仅关键字字段,即在生成的 init() 中,这些字段只能通过关键字参数传入。仅限关键字字段不会包含在 match_args 中。slots(bool,默认 False,自 3.10 起):生成 slots 并返回一个新的类而非原始类;这样实例会使用槽来存储属性(节省内存)。如果类中已定义 slots,则抛出 TypeError。注意:使用 slots=True 时,普通的无参 super() 在 init() 中可能报错,需要使用带参 super();此外,如果基类定义了槽位,任何相同名称的字段不会重复在子类槽位中。weakref_slot(bool,默认 False,自 3.11 起):如果为 True(必须同时 slots=True),则在 slots 中增加 “weakref” 槽位,使实例可以被弱引用
# 三种写法:
@dataclass
class C
@dataclass()
class C
@dataclass(
init=True,
repr=True,
eq=True,
order=False,
unsafe_hash=False,
frozen=False,
kw_only=False,
match_args=True,
slots=False,
weakref_slot=False
)
class C
@dataclass(kw_only=True)
class Config:
host: str
port: int = 8080
cfg = Config(host="localhost") # 必须关键字传参
@dataclass(frozen=True)
class Config:
host: str
port: int
cfg = Config("localhost", 8080)
cfg.port = 9090 # 报错:FrozenInstanceError
@dataclass(order=True)
class Player:
score: int
name: str
players = [Player(20, "Alice"), Player(15, "Bob")]
players.sort()
print(players) # 按 score 排序
@dataclass(init=False)
class User:
id: int
name: str
def __init__(self, id):
self.id = id
self.name = "默认"
# 使用默认值
@dataclass
class User:
id: int = 1001
name: str = "匿名"
四、字段控制 field()
用于解决单个字段的个性化需求配置,比如可变类型默认值、隐藏字段、关闭比较等
from dataclasses import dataclass, field
field(
default=..., # 默认值
default_factory=...,# 一个无参数的可调用对象,用于动态生成字段的默认值,不能和default共用,处理列表、字典等可变默认值的唯一正确方式
init=True, # 控制该字段是否包含在 __init__ 参数
repr=True, # 控制该字段是否包含在 __repr__ 的输出中
compare=True, # 控制该字段是否参与 __eq__ 和其他比较方法
hash=None, # 是否参与哈希计算
metadata={}, # 自定义元数据,用于第三方库解析
)
4.1、可变对象默认值
default_factory=list 确保每个实例的 items 字段初始为一个独立的空列表,避免多个实例共享同一个列表
from dataclasses import dataclass, field
# 可变对象默认值
# 错误展示
@dataclass
class Demo:
my_list: list = [] # Mutable default '[]' is not allowed. Use 'default_factory'
# 正确展示
@dataclass
class User:
# 使用 default_factory 为可变对象默认值创建新实例
my_list: list = field(default_factory=list)
my_dict: dict = field(default_factory=dict)
user1 = User()
user1.my_list.append("Alice")
print(user1.my_list) # ['Alice']
print(User().my_list) # [],互不干扰
4.2、field其他参数案例
@dataclass
class User:
# 非默认字段必须出现在默认字段之前
inst_id: int
# 给 id 添加默认值,不参与repr打印,不参与比较
id: int = field(default=1001, repr=False, compare=False)
# 不参与初始化
name: str = field(init=False)
五、生命周期钩子:__post_init__
当自动生成的 __init__ 执行完成后,会自动触发__post_init__ 方法,用于处理字段二次计算、数据校验、数据格式 化等后置逻辑,是开发高频钩子
from dataclasses import dataclass, field
@dataclass
class Rectangle:
width: float
height: float
area: float = field(init=False)
# 初始化 后置钩子
def __post_init__(self):
self.area = self.width * self.height
@dataclass
class User:
id: int
age: int
def __post_init__(self):
# 校验
if self.age < 0:
raise ValueError("年龄不能负数")
# 派生属性
self.is_adult = self.age >= 18
六、通用 DTO 模板
6.1、简单实用案例
from dataclasses import dataclass, field, asdict, replace
from typing import List
@dataclass(frozen=False, order=True)
class Student:
sid: int
name: str
scores: List[int] = field(default_factory=list, repr=False)
avg: float = field(init=False)
def __post_init__(self):
if self.scores:
self.avg = sum(self.scores) / len(self.scores)
else:
self.avg = 0.0
s1 = Student(101, "小明", [90, 95, 88])
print(s1)
print(s1.avg)
print(asdict(s1))
s2 = replace(s1, name="大明")
print(s2)
6.2、通用模板
Optional[T] = 该字段可以是 T 类型,也可以是 None
等价于 Optional[T]
detail: T | None = None
# DTO 模板
from dataclasses import dataclass, field, asdict, replace, fields
from typing import Any, List, Dict, Optional
import json
# 嵌套子DTO示例
@dataclass(frozen=True, slots=True)
class AddressDTO:
province: str
city: str
detail: Optional[str] = None
# 主业务DTO模板
@dataclass(
frozen=False, # 设为True则全局只读不可修改
order=True, # 支持对象大小比较
kw_only=True, # 实例化仅允许关键字传参,避免参数顺序混乱
slots=True # 3.11+ 节省内存,大量对象推荐开启
)
class UserDTO:
# 必填基础字段
user_id: int
username: str
age: int
# 普通默认值
email: str = ""
# 可变容器必须用 default_factory,禁止直接=[]/{}
tags: List[str] = field(default_factory=list, repr=True)
extra_info: Dict[str, Any] = field(default_factory=dict, repr=False) # repr=False打印隐藏
# 嵌套DTO对象
address: Optional[AddressDTO] = None
# 衍生字段,不参与构造函数传参
is_adult: bool = field(init=False)
def __post_init__(self) -> None:
"""初始化后置钩子:参数校验、派生字段计算"""
# 1. 参数合法性校验
if self.age < 0:
raise ValueError(f"年龄不能为负数,当前age={self.age}")
if len(self.username.strip()) == 0:
raise ValueError("用户名不能为空")
# 2. 计算衍生属性
self.is_adult = self.age >= 18
def to_dict(self) -> Dict[str, Any]:
"""转字典,兼容嵌套dataclass"""
return asdict(self)
def to_json(self, indent: int = 2) -> str:
"""直接输出JSON字符串"""
return json.dumps(self.to_dict(), ensure_ascii=False, indent=indent)
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "UserDTO":
"""字典反序列化为DTO对象"""
# 嵌套对象单独解析
if data.get("address"):
data["address"] = AddressDTO(**data["address"])
if 'is_adult' in data: # 兼容is_adult不进行初始化field(init=False)的操作
del data['is_adult']
return cls(**data)
def copy(self, **kwargs) -> "UserDTO":
"""复制对象并修改指定字段,返回新实例"""
return replace(self, **kwargs)
@classmethod
def get_field_names(cls) -> List[str]:
"""获取所有字段名列表"""
return [f.name for f in fields(cls)]
# 调用
# 1. 创建嵌套地址对象
addr = AddressDTO(province="江苏", city="南京", detail="鼓楼区")
# 2. 实例化(kw_only=True 只能关键字传参)
user = UserDTO(
user_id=10001,
username="zhangsan",
age=22,
tags=["admin", "dev"],
extra_info={"role": "backend"},
address=addr
)
# 3. 打印对象(extra_info已隐藏)
print(user)
# 4. 读取衍生字段
print("是否成年:", user.is_adult)
# 5. 转字典 / JSON
print(user.to_dict())
print(user.to_json())
# 6. 字典反向构建DTO
data = user.to_dict()
user2 = UserDTO.from_dict(data)
# 7. 拷贝修改字段生成新对象
user3 = user.copy(username="lisi", age=25)
print(user3)
# 8. 获取所有字段名
print(UserDTO.get_field_names())
# 9. 冻结只读模式演示(复制一份设为frozen=True)
@dataclass(frozen=True, kw_only=True)
class UserReadDTO(UserDTO):
pass
read_user = UserReadDTO(user_id=999, username="readonly", age=30)
# read_user.user_id = 111 # 报错FrozenInstanceError,禁止修改
七、四大内置工具函数
7.1、asdict() 实例转字典
将数据类实例递归转为字典,常用于接口序列化、数据存储
# 极简底层实现
def asdict(obj):
return {k:v for k,v in obj.__dict__.items()}
# 用法
from dataclasses import dataclass, asdict
@dataclass
class User:
name: str
age: int
print(asdict(User("张三", 18))) # {'name': '张三', 'age': 18}
7.2、astuple() 实例转元组
将数据类实例按字段顺序转为元组,用于有序数据比对、解包赋值
# 极简底层实现
def astuple(obj):
return tuple(obj.__dict__.values())
# 用法
from dataclasses import dataclass, astuple
@dataclass
class User:
name: str
age: int
print(astuple(User("张三", 18))) # ('张三', 18)
7.3、replace() 拷贝并生成新实例
浅拷贝原实例,支持批量修改指定字段,兼容 frozen 不可变类,不会修改原实例
# 极简底层实现
def replace(obj, **kwargs):
new_kwargs = obj.__dict__.copy()
new_kwargs.update(kwargs)
return obj.__class__(**new_kwargs)
# 用法
from dataclasses import dataclass, replace
@dataclass(frozen=True)
class User:
name: str
age: int
old = User("张三", 18)
new = replace(old, age=20)
print(new) # User(name='张三', age=20)
7.4、fields() 获取字段元信息
获取数据类所有字段的配置信息,用于反射、动态解析数据类结构
# 用法
from dataclasses import dataclass, fields
@dataclass
class User:
name: str
age: int
print(fields(User)) # 获取所有字段配置、类型、默认值等元信息
八、高级用法与扩展
默认工厂:对于可变类型的默认值(如列表、字典等),应该使用 default_factory。官方文档指出,如果将可变类型直接作为默认参数,dataclass 会在检测到时抛出 TypeError 来避免错误。正确做法是使用 field(default_factory=…),这样每次创建实例时都会调用工厂函数生成新对象,避免不同实例间的数据污染。仅初始化变量 (InitVar):dataclasses.InitVar 可用于声明仅在 init 时使用的临时参数。这些 InitVar 字段不会成为类的实际字段,也不会出现在 fields() 返回值中,而是作为参数传递给 post_init。这常用于在初始化时用某个值计算或设置其它字段,但不将其保留。
from dataclasses import dataclass, field, InitVar
@dataclass
class C:
x: int
data_source: InitVar[str] = None
def __post_init__(self, data_source):
if self.x is None and data_source:
self.x = load_default_from(data_source)
继承与重写:子类可以继承父类数据类,并在子类中添加字段或重新定义字段。dataclass 会自动管理字段顺序(父类字段排在前,子类字段排在后)在子类中定义与父类同名的字段会覆盖父类字段的类型或默认值
@dataclass
class Base:
id: int
@dataclass
class User(Base):
name: str
u = User(1, "李四")
print(u) # User(id=1, name='李四')
比较 NamedTuple、TypedDict 等相比:- namedtuple 创建的是元组子类,不可变(需用 _replace 更新),而 dataclass 创建的是普通类实例,默认可变。也可以设置 frozen=True 实现不可变。
- 与 TypedDict 相比,dataclass 更像电子表格,有严格的列定义和可选的不变性,并自带方法。一般建议:如果需要灵活的字典结构,使用 TypedDict;如果需要具有行为的正规类结构,则使用 dataclass。
九、高频避坑指南 + 适用场景
核心避坑点:
- 可变类型(list/dict/set)禁止直接赋值默认值,必须使用 default_factory,否则所有实例共享同一数据
- frozen=True 仅为浅不可变,嵌套的可变对象(列表、字典)仍可修改内部数据
- 开启 order=True 时,严格按照字段定义顺序逐字段比对大小
- 无类型注解的类变量,不会被识别为 dataclass 数据字段。
3309

被折叠的 条评论
为什么被折叠?



