Python dataclass 默认值完整工作流:从可变默认值到 default_factory
Python 里的 dataclass 很适合写配置对象、任务参数、接口返回结构。它能少写很多样板代码,但默认值这一块如果处理得不稳,很容易把多个实例之间的数据搞混。
这篇文章按完整工作流来整理:什么时候可以直接写默认值,什么时候必须用 field(default_factory=...),如何给 list、dict、set 这类字段建模,最后用一个小测试确认每个实例都有自己的独立对象。
摘要
dataclass 默认值的核心规则很简单:不可变值可以直接写,可变值不要直接放在字段定义上。对于列表、字典、集合这类字段,使用 field(default_factory=list)、field(default_factory=dict) 或自定义函数,让每次创建实例时都生成新的对象。这样可以避免实例之间共享同一份可变数据。
适合人群
- 用 Python 写配置类、请求参数类、任务对象、数据传输对象的开发者。
- 遇到列表或字典字段在多个对象之间互相影响的读者。
- 想把 dataclass 默认值规则整理成团队代码规范的人。
- 目标和边界:这篇文章解决什么问题
- 全流程总览:从可变默认值到 default_factory
- 阶段 1:先区分不可变值和可变值
- 阶段 2:用 field(default_factory=...) 生成独立对象
- 阶段 3:给复杂默认值写工厂函数
- 我的推荐流程
- 容易踩坑
- 速查表
目标和边界:这篇文章解决什么问题
我们先把边界定清楚。本文解决的是 dataclass 字段默认值怎么写更稳,尤其是 list、dict、set 这类可变字段。它不展开讲 dataclass 的全部参数,也不讨论 ORM、Pydantic 这类更完整的数据建模框架。
最终要得到的是一个可复用判断流程:
- 字段默认值是数字、字符串、布尔值、
None:通常可以直接写。 - 字段默认值是列表、字典、集合或自定义对象:优先使用
default_factory。 - 字段默认值需要运行一段初始化逻辑:写一个专门的工厂函数。
- 写完后创建两个实例,确认它们的可变字段不是同一个对象。
全流程总览:从可变默认值到 default_factory
先看整体路径。很多默认值问题不是语法不会写,而是没有在建模阶段区分“值本身能不能被修改”。列表、字典、集合都能被原地修改,所以它们不能当作普通常量看待。

这张图要表达的核心是:如果多个实例拿到同一个列表,一个实例追加数据,另一个实例也会看到变化。正确做法是让 dataclass 在每次创建实例时都调用工厂,生成新的列表或字典。
阶段 1:先区分不可变值和可变值
这一阶段的目标是决定字段默认值能不能直接写。先看一个适合直接写的例子。
from dataclasses import dataclass
@dataclass
class RetryConfig:
max_times: int = 3
timeout: float = 2.5
enabled: bool = True
remark: str = "default"
这些字段的默认值都是不可变值。创建多个实例时,它们不会因为某个实例修改字段而共享内部状态。
检查点很明确:如果字段默认值不需要被原地修改,直接写通常没问题。如果字段后续要 append、update、add,就要进入下一阶段。
阶段 2:用 field(default_factory=...) 生成独立对象
现在看可变字段。任务对象通常会带标签、扩展参数、运行记录。它们很适合用列表和字典表达,但默认值不能直接写成空列表或空字典。
from dataclasses import dataclass, field
@dataclass
class TaskConfig:
name: str
tags: list[str] = field(default_factory=list)
options: dict[str, str] = field(default_factory=dict)
warnings: set[str] = field(default_factory=set)
这里的 default_factory=list 不是提前准备一个列表,而是在每次创建 TaskConfig 时调用 list()。所以每个实例都有自己的 tags。
a = TaskConfig(name="daily")
b = TaskConfig(name="weekly")
a.tags.append("report")
a.options["level"] = "normal"
print(a.tags)
print(b.tags)
print(a.options)
print(b.options)
输出应该是:
['report']
[]
{'level': 'normal'}
{}
检查点是:修改 a 的列表或字典,不会影响 b。这说明默认值已经按实例隔离。
阶段 3:给复杂默认值写工厂函数
有些默认值不是空容器,而是带初始内容的结构。比如任务默认要有一个来源字段和一个版本号。这时不要把字典直接写在字段上,可以写一个工厂函数。
from dataclasses import dataclass, field
def make_default_options() -> dict[str, str]:
return {
"source": "system",
"version": "v1",
}
@dataclass
class JobConfig:
name: str
options: dict[str, str] = field(default_factory=make_default_options)
这个函数每次都会返回一个新字典。读代码的人也能看出来:默认配置不是一个共享对象,而是按实例生成。
如果默认值依赖外部配置,建议把读取动作放在更上层完成,再显式传入 dataclass。字段工厂适合轻量、稳定、无副作用的初始化。
我的推荐流程
日常写 dataclass 时,可以按下面的流程走,避免每次都临时判断。

| 阶段 | 目标 | 关键动作 | 检查点 |
|---|---|---|---|
| 字段设计 | 确认字段用途 | 标注类型,判断是否会原地修改 | 字段类型清楚,默认值含义明确 |
| 默认值选择 | 避免共享可变对象 | 不可变值直接写,可变值用 field |
列表、字典、集合没有直接放在字段上 |
| 复杂初始化 | 让默认值可读可维护 | 把默认结构放进工厂函数 | 工厂函数每次返回新对象 |
| 结果验证 | 确认实例隔离 | 创建两个实例,修改其中一个 | 另一个实例不受影响 |
一个最小测试可以这样写:
def test_task_config_defaults_are_isolated():
a = TaskConfig(name="a")
b = TaskConfig(name="b")
a.tags.append("hot")
a.options["owner"] = "ops"
a.warnings.add("slow")
assert b.tags == []
assert b.options == {}
assert b.warnings == set()
assert a.tags is not b.tags
assert a.options is not b.options
这类测试很小,但能直接证明默认值没有在实例之间串数据。团队里写通用配置类时,建议把它放进单元测试。
容易踩坑
1. 看到空列表就顺手写在字段上
空列表看起来像一个普通默认值,但它是可变对象。只要后续会修改,就应该用 default_factory。
2. 工厂函数返回了同一个全局对象
工厂函数要返回新对象,不要返回提前定义好的全局列表或全局字典。否则问题只是从字段定义挪到了函数内部。
shared_tags: list[str] = []
def bad_tags() -> list[str]:
return shared_tags
更稳的写法是:
def make_tags() -> list[str]:
return []
3. 把业务配置读取放进字段工厂
字段工厂最好保持轻量。如果创建对象时会读取文件、访问网络或依赖复杂环境,排查问题会变难。可以在外层准备好配置,再传给 dataclass。
4. 只看运行没报错,不验证实例隔离
默认值问题的危险点在于短期内不一定报错。创建两个实例、修改其中一个,是最直接的验证方式。
速查表
| 字段默认值 | 推荐写法 | 原因 |
|---|---|---|
0、1.0、True |
直接写 | 不可变,语义清楚 |
"text" |
直接写 | 字符串不可变 |
[] |
field(default_factory=list) |
每个实例要有独立列表 |
{} |
field(default_factory=dict) |
避免多个实例共享字典 |
set() |
field(default_factory=set) |
避免集合状态互相影响 |
| 带初始内容的结构 | 自定义工厂函数 | 保持可读,并确保返回新对象 |
总结一句话:写 dataclass 默认值时,先问这个字段会不会被原地修改。不会修改的简单值可以直接写;会修改的容器或对象,用 field(default_factory=...) 生成新对象,再用两个实例做隔离验证。这个流程稳定、清晰,也方便后续维护。
前端轮询接口越打越多怎么办:从重复定时器到清理机制一步步排查
- 上一篇
- 前端轮询接口越打越多怎么办:从重复定时器到清理机制一步步排查
- 下一篇
- 7-Zip 怎么给压缩包设置密码:AES-256 加密压缩教程
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- ljg-skills
- ljg-skills 是李继刚开源的 AI 技能与提示词集合,面向大模型使用者整理了一批可复用的 prompt、角色设定和任务技能模板,适合用于学习提示词设计、搭建个人 AI 工作流和沉淀团队常用智能体能力。
- 88次使用
-
- MELO音乐
- MELO音乐是一站式AI视频与音乐制作助手,对标suno, udio的高品质体验。提供伴奏生成、原创写词、无损导出、哼唱识曲、混音变声等全套音频与短视频编辑工具。无论是流行Kpop、电音说唱、民谣古风、摇滚儿歌还是商用轻音乐,MELO为你免费谱曲,轻松做同款!
- 109次使用
-
- UniScribe
- UniScribe 是一款 AI 音视频转文字与内容整理工具,支持上传音频、视频文件或粘贴 YouTube 链接,自动生成转写文本、摘要、思维导图和关键问题,并支持多格式导出,适合会议记录、课程学习、访谈整理和内容创作复盘。
- 101次使用
-
- 剧云
- 剧云是专业中文剧本创作平台,安全稳定运行十余年,集成AI编剧、剧本医生审核、人物小传、剧情关系图、大纲编写、多人协作、Word导入导出、版权管控功能,数据安全防护,轻松高效创作剧本。
- 245次使用
-
- 万象有声
- 万象有声,一个专为有声创作者打造的新一代智能有声内容创作平台。平台提供专业的智能拆章、智能画本编辑、AI配音、AI生成音效、后期制作、智能对轨、智能审听等有声创作全流程工具,可以帮助创作者高效、低成本创作出引人入胜的有声作品。立即体验,让有声书制作更简单!
- 249次使用
-
- HTTP 的 response 中的响应体和头部是分开发送的吗?
- 2023-01-28 387浏览
-
- B站等视频网站的弹幕用的是 websocket 还是轮询?
- 2023-02-16 447浏览
-
- Linux 下有什么命令行工具以时序显示 CPU 占用率?
- 2023-01-13 360浏览
-
- 测试开发知识图谱
- 2023-02-24 271浏览
-
- 如何不停机升级机器的配置
- 2023-02-16 142浏览

