当前位置:首页 > 文章列表 > 文章 > python教程 > Python dataclasses.field 怎样声明不可共享的默认值

Python dataclasses.field 怎样声明不可共享的默认值

来源:17golang原创 2026-11-02 11:59:51 0浏览 收藏

Python dataclasses.field 要为每个实例创建独立的列表、字典或集合,应使用 field(default_factory=list)、field(default_factory=dict)等零参数工厂。这里传入的是可调用对象,默认值真正需要时才调用它。工厂必须返回新对象;这个参数不会禁止调用方显式共享对象,也不会自动深拷贝嵌套数据。

官方文档:https://docs.python.org/3/library/dataclasses.html。设计背景见 https://peps.python.org/pep-0557/。下面的示例使用 Python 3.9 起支持的内置容器泛型语法;代码用于解释规则,文中不提供本机运行记录。

我会先区分默认对象与默认工厂

给一个“导入任务”建模时,我更愿意先问:每次创建任务,错误列表归谁所有?如果答案是各个任务自己维护,那么默认配置就应该保存创建方法,而不是一份已经存在的列表。这是我选择字段写法时的判断方式,不依赖任务名称或者列表最初是否为空。

from dataclasses import dataclass, field

# 反例:类定义处理时会因可变列表默认值而抛出 ValueError
@dataclass
class BadImportJob:
    errors: list[str] = field(default=[])

这段反例应单独看待,不要与后面的正常示例拼成一个脚本继续运行。把 []包进 field(default=...)没有改变对象归属:它仍是一份现成的列表。dataclasses 对常见危险默认值会主动报错,而不是悄悄为每个实例复制它。

规则有一个兼容变化:Python 3.11 起,用“默认对象不可哈希”作为拒绝条件;更早的规则针对 list、dict、set。不可哈希只是可变性的近似判断,不能把“装饰器接受了这个对象”理解为它必然不可变,更不能当成每实例独立的保证。PEP 557 解释了设计背景,现行行为仍应以当前库文档为准。

用零参数工厂创建独立字段

from dataclasses import dataclass, field

@dataclass
class ImportJob:
    source: str
    errors: list[str] = field(default_factory=list)
    counters: dict[str, int] = field(default_factory=dict)

# 两次省略字段参数,分别需要自己的默认容器
first = ImportJob("north.csv")
second = ImportJob("south.csv")
first.errors.append("缺少编号")
first.counters["invalid"] = 1

# 这些断言表达预期行为,可由读者在自己的环境执行
assert first.errors is not second.errors
assert first.counters is not second.counters
assert second.errors == []
assert second.counters == {}

errors和 counters分别使用 list、dict 工厂,保存可追加错误和可更新计数。source没有默认值,放在有默认值的字段前面,避免生成初始化方法时出现参数顺序错误。

我通常优先选内置构造器,原因是它直接表达“每次创建一个空容器”,读代码的人不必进入另一个函数确认行为。需要非空默认内容时,再写一个命名函数或简短 lambda。default_factory=list()则把列表本身传了进去;列表不可调用,不能满足工厂契约。default与 default_factory也不能同时指定。

ImportJob 字段与两个实例各自容器的关系结构图
静态说明图:字段声明中的工厂关系与实例容器归属分别分组;连线表示结构归属,不是执行时间线。

嵌套容器和显式参数仍要单独处理

让我更在意的边界是:工厂被调用,不代表它真的创造了新对象。比如 lambda: shared_errors虽然可以零参数调用,却始终返回那份共享列表。修复要落在工厂函数的返回值上,而不是仅仅让字段声明出现 default_factory 这个单词。

from dataclasses import dataclass, field

def new_sections() -> dict[str, list[str]]:
    # 外层字典和内层列表都在调用时重新创建
    return {"accepted": [], "rejected": []}

@dataclass
class ImportReport:
    sections: dict[str, list[str]] = field(default_factory=new_sections)

left = ImportReport()
right = ImportReport()
left.sections["accepted"].append("item-7")

# 不只判断外层身份,也判断实际需要隔离的内层对象
assert left.sections is not right.sections
assert left.sections["accepted"] is not right.sections["accepted"]
assert right.sections["accepted"] == []

这个工厂为每次调用创建完整的嵌套结构。如果改成对共享模板执行 dict(template),只会新建外层字典,模板里面的列表仍可能被复用。对于业务形状明确的默认值,我倾向直接重新构建需要的层级;如果确实要从复杂模板复制,则应按对象语义选择复制方式。连接、锁、文件句柄等资源不能简单视作普通数据深拷贝。

# 以下代码承接前面的 ImportJob 定义
provided: list[str] = []
one = ImportJob("one.csv", errors=provided)
two = ImportJob("two.csv", errors=provided)

# 显式传入同一个对象时,默认工厂不会接管或复制它
assert one.errors is two.errors
one.errors.append("共同记录")
assert provided == ["共同记录"]

如果接口要求“即使显式传入列表,也必须由当前实例独占外层容器”,可以在 __post_init__中用 self.errors = list(self.errors)建立一份浅拷贝。这个选择改变了输入所有权契约,应该写清楚;元素如果本身可变,浅拷贝仍会共享元素。默认工厂解决省略参数时的创建问题,输入复制解决显式传参的所有权问题,两处职责不同。

ImportReport 外层字典、内层列表与显式输入的结构关系图
静态说明图:区分嵌套默认结构与显式输入共享。判断隔离时需要查看实际会被修改的对象层级。

哪些兼容细节会改变采用方式

字段不允许构造传入时怎么写?可以设置 field(default_factory=dict, init=False)。在 dataclasses 生成的初始化方法中,这类字段仍会通过工厂取得初值。如果自己定义了 __init__,就需要自己负责相应初始化;不要假定未生成的方法仍会替你调用工厂。

frozen=True 能代替工厂吗?不能。它约束实例字段赋值,不会把字段引用的列表自动转成不可变对象。若业务确实需要不可变内容,应选择满足要求的不可变数据结构;tuple 的内部元素也需要逐一考虑,不能只看最外层类型。

为什么两个空容器判断相等,却仍然是独立的?==比较内容,is判断对象身份。判断这类默认值隔离问题时,应同时关注身份和修改后的影响;不要只用两个空列表相等就断言它们被共享。

需求建议声明或处理需要保留的边界
每实例独立空列表field(default_factory=list)显式传参仍按引用接收
每实例独立空字典field(default_factory=dict)不等于线程安全字典
非空嵌套默认结构零参数函数逐层新建不复用模板中的可变子对象
标量或合适的不可变默认值直接赋默认值考虑内部引用是否仍可变
显式输入也要复制定义初始化所有权策略浅拷贝与深层隔离不同

我会把 default_factory 当成默认对象的创建约定:省略参数时,调用一个零参数函数拿到所需对象。对于每个实例独立维护的可变状态,工厂应真正新建相关容器;对于调用方有意共享的输入,则通过接口约定明确保留或复制。这种区分比单纯消除 ValueError 更有助于后续维护。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Python sqlite3.blobopen 怎样读写 SQLite 的大二进制字段Python sqlite3.blobopen 怎样读写 SQLite 的大二进制字段
上一篇
Python sqlite3.blobopen 怎样读写 SQLite 的大二进制字段
Python typing.TypeIs 怎样表达更精确的类型收窄
下一篇
Python typing.TypeIs 怎样表达更精确的类型收窄
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    843次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    843次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    925次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    900次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    729次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码