Python dataclass 默认值完整工作流:从可变默认值到 default_factory
来源:17golang原创
时间:2026-06-16 17:06:49 228浏览 收藏
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=...) 生成新对象,再用两个实例做隔离验证。这个流程稳定、清晰,也方便后续维护。
-
387 收藏
-
447 收藏
-
360 收藏
-
271 收藏
-
142 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习