dataclass 类使用笔记 Focused Reading 0 Categories / 0 Tags / 1.8k Words
Python Note

dataclass 类使用笔记

Python dataclass 的定义、校验、默认值与序列化

2026.08.18 1.8k Words

dataclass

dataclasses 是 Python 标准库,用来定义“主要负责保存数据”的类。它会根据类型注解自动生成构造函数、调试友好的字符串表示和比较方法,让数据对象少写一些样板代码。

1
2
3
4
5
6
7
8
9
10
from dataclasses import dataclass

@dataclass
class User:
id: int
name: str
active: bool = True

user = User(1, "Ame")
print(user) # User(id=1, name='Ame', active=True)

@dataclass 只处理带有类型注解的字段。类型注解既是文档,也决定了哪些类属性会被当作数据字段。

基础使用

1. 定义字段与默认值

字段必须先写无默认值,再写有默认值的字段,否则生成 __init__ 时会违反 Python 参数规则。

1
2
3
4
5
6
7
8
9
from dataclasses import dataclass

@dataclass
class Article:
title: str
author: str
published: bool = False

article = Article("dataclass 入门", "SukiAme")

实例会自动获得:

  • __init__:按字段顺序接收参数。
  • __repr__:输出 Article(title=..., author=..., published=...)
  • __eq__:同类型实例按字段值比较。

2. 可变默认值要用 default_factory

不要把列表、字典或集合直接写成默认值。否则多个实例可能共享同一个对象;使用 field(default_factory=...) 才会为每个实例创建一份独立值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from dataclasses import dataclass, field

@dataclass
class Project:
name: str
tags: list[str] = field(default_factory=list)
settings: dict[str, str] = field(default_factory=dict)

first = Project("blog")
second = Project("api")
first.tags.append("python")

print(first.tags) # ['python']
print(second.tags) # []

3. field() 控制字段行为

1
2
3
4
5
6
7
8
from dataclasses import dataclass, field

@dataclass
class Task:
title: str
secret: str = field(repr=False)
created_at: float = field(default=0.0, init=False)
labels: list[str] = field(default_factory=list, compare=False)

常用参数:

参数 作用
default 提供一个固定默认值。
default_factory 调用工厂函数生成默认值,适合列表、字典等可变对象。
init=False 不把字段放进自动生成的 __init__
repr=False 不在 repr(instance) 中显示字段,适合密码等敏感值。
compare=False 比较实例时忽略该字段。

初始化与校验

1. 用 __post_init__ 处理派生值

__post_init__ 会在自动生成的 __init__ 结束后执行,适合校验参数或根据输入计算字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from dataclasses import dataclass, field

@dataclass
class Rectangle:
width: float
height: float
area: float = field(init=False)

def __post_init__(self) -> None:
if self.width <= 0 or self.height <= 0:
raise ValueError("width and height must be positive")
self.area = self.width * self.height

rectangle = Rectangle(3, 4)
print(rectangle.area) # 12

2. 使用 InitVar 接收一次性参数

InitVar 会出现在构造函数中,也会传给 __post_init__,但不会成为实例字段。它适合密码、原始配置等只在初始化阶段需要的值。

1
2
3
4
5
6
7
8
9
10
11
from dataclasses import InitVar, dataclass, field

@dataclass
class DatabaseConfig:
host: str
port: int
url: str = field(init=False)
scheme: InitVar[str] = "postgresql"

def __post_init__(self, scheme: str) -> None:
self.url = f"{scheme}://{self.host}:{self.port}"

3. ClassVar 不会成为实例字段

类级别的常量应标注为 ClassVar,这样 dataclass 不会把它加入构造函数、比较和 fields() 结果。

1
2
3
4
5
6
7
from dataclasses import dataclass
from typing import ClassVar

@dataclass
class Response:
status: int
default_status: ClassVar[int] = 200

不可变对象与高级选项

1. frozen=True 创建不可变数据对象

冻结后不能直接修改字段,适合配置、坐标、字典键等值对象。需要修改时,应创建新实例,而不是绕过限制强行赋值。

1
2
3
4
5
6
7
8
9
from dataclasses import dataclass

@dataclass(frozen=True)
class Point:
x: float
y: float

origin = Point(0, 0)
# origin.x = 1 # FrozenInstanceError

冻结类通常更适合参与哈希和集合操作,但只有在字段本身也可哈希时才适合作为字典键。

2. 关键字参数与 slots

1
2
3
4
5
6
7
8
9
from dataclasses import dataclass

@dataclass(kw_only=True, slots=True)
class HttpRequest:
url: str
timeout: float = 10.0
retry: int = 2

request = HttpRequest(url="https://example.com", timeout=3.0)
  • kw_only=True 要求字段使用关键字传入,参数含义更清楚,也能减少后续新增字段造成的调用破坏。
  • slots=True 使用 __slots__ 存储字段,通常能减少对象开销,同时阻止随意添加未声明的属性。

这两个选项需要较新的 Python 版本;如果项目需要兼容旧版本,应先确认运行环境。

3. 继承时注意字段顺序

父类已有默认字段时,子类再添加无默认字段可能触发 TypeError: non-default argument follows default argument。常见解决方式是让新字段成为关键字参数,或重新设计基类的字段顺序。

转换、复制与检查

1
2
3
4
5
6
7
8
from dataclasses import asdict, astuple, fields, is_dataclass, replace

point = Point(3, 4)
print(asdict(point)) # {'x': 3, 'y': 4}
print(astuple(point)) # (3, 4)
print(is_dataclass(point)) # True
print([item.name for item in fields(Point)])
print(replace(point, x=10)) # Point(x=10, y=4)
  • asdict() 递归转换嵌套 dataclass,并会复制部分容器;适合生成普通字典,但不等同于完整 JSON 序列化。
  • astuple() 以元组形式递归转换。
  • fields() 返回声明过的字段信息,可用于通用校验或表格渲染。
  • replace() 创建一个新实例,并用指定值覆盖字段;冻结对象也应通过它更新。
  • is_dataclass() 可判断类或实例是否由 @dataclass 定义。

转成 JSON

标准库没有为所有类型自动提供 JSON 编码器。普通字段可以先用 asdict() 转换,再交给 json.dumps()datetimePath 等特殊类型仍需自定义转换。

1
2
3
4
5
6
7
8
9
10
import json
from dataclasses import asdict, dataclass

@dataclass
class User:
id: int
name: str

payload = json.dumps(asdict(User(1, "Ame")), ensure_ascii=False)
print(payload) # {"id": 1, "name": "Ame"}

一个更完整的实践例子

下面用一个页面配置对象串起默认值、校验、派生字段和序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from dataclasses import asdict, dataclass, field

@dataclass
class PageConfig:
title: str
path: str
tags: list[str] = field(default_factory=list)
published: bool = False
slug: str = field(init=False)

def __post_init__(self) -> None:
self.title = self.title.strip()
self.path = self.path.strip().strip("/")
if not self.title or not self.path:
raise ValueError("title and path cannot be empty")
self.slug = self.path.lower().replace(" ", "-")

config = PageConfig(" 操作系统 ", "notes/os", ["linux", "cpu"])
print(config.slug) # notes/os
print(asdict(config))

使用建议

  • 数据类只表达数据和轻量的领域行为;复杂业务流程应放在服务函数或独立类中。
  • 任何可变默认值都优先使用 default_factory
  • 对外部输入在 __post_init__ 中尽早校验,让无效对象无法继续传播。
  • 配置和不可变值对象可考虑 frozen=True;需要明确调用语义时可考虑 kw_only=True
  • asdict() 适合简单转换,不要把它当作所有类型的通用序列化方案。