登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  python教程

Python dataclasses.field 默认工厂为什么不能直接写成列表

来源:17golang原创

时间:2026-09-10 10:02:11 495浏览 收藏

dataclasses 里给列表字段设置默认值时,最容易写错的是把 default_factory=list 写成 default_factory=[]。两者只差一对括号,含义却完全不同:前者传入“以后可以被调用的工厂”,后者传入“现在已经创建好的列表”。正确写法是:

from dataclasses import dataclass, field

@dataclass
class Basket:
    # 传入 list 类型;每次构造 Basket 时由它创建一个新列表。
    items: list[str] = field(default_factory=list)
要点速览
  • default_factory 要的是零参数可调用对象,list 可以,[] 不可以。
  • 可变容器应使用 field(default_factory=list)dictset,不要把实例写进 default
  • 用两个实例检查内容和 is 身份,能快速确认列表没有跨对象共享。

为什么 default_factory=[] 不是列表默认值

field()default_factory 参数定义的是“需要默认值时调用的零参数函数”。list 本身是一个可调用类型,调用 list() 会得到新列表;[] 则已经是列表对象,不是函数。于是下面的写法把参数类型弄反了:

from dataclasses import dataclass, field

@dataclass
class WrongBasket:
    # 错误:[] 是列表实例,不是可以被调用的零参数工厂。
    items: list[str] = field(default_factory=[])

# 构造实例时,生成的 __init__ 会尝试执行 [](),因此会报不可调用错误。

另一种常见误写是 items: list[str] = []。这不是“每次构造自动复制一份列表”的语法。数据类仍然遵循普通 Python 类的默认值语义;当前文档还明确说明,不可哈希的默认对象会被拒绝,以拦住许多可变默认值错误。即使某个自定义可变对象没有立刻触发保护,也不应该把它当成实例级默认容器。

Python dataclasses.field 中 default_factory、list 工厂与列表实例的参数边界关系图
图1:把 list 类型作为工厂,把 [] 作为实例,理解 default_factory 的参数边界。

把列表类型交给工厂,而不是提前创建列表

修复时只需要把方括号移除。field(default_factory=list) 表示:这个字段没有调用方传入值时,初始化逻辑调用一次 list()。工厂不接收 self,也不应该依赖另一个字段的值。

from dataclasses import dataclass, field

@dataclass
class Basket:
    # list 是零参数可调用对象;每个 Basket 都会获得独立容器。
    items: list[str] = field(default_factory=list)

@dataclass
class Settings:
    # 字典和集合也使用各自的类型作为工厂。
    labels: dict[str, str] = field(default_factory=dict)
    flags: set[str] = field(default_factory=set)

如果默认列表需要固定的初始内容,可以传入零参数的 lambda。它每次返回新对象,而不是把同一个列表挂到所有实例上:

from dataclasses import dataclass, field

@dataclass
class QueueConfig:
    # 工厂不接收构造参数;返回值是每个实例自己的新列表。
    stages: list[str] = field(default_factory=lambda: ["queued", "ready"])

不过,固定空列表优先写成 default_factory=list,可读性更好,也更直接地表达“调用列表类型创建容器”。

用两个实例指标确认列表彼此隔离

排查这类问题时,不要只看对象打印出来的内容。建议同时看三个指标:初始内容是否为空、向对象 A 追加元素后对象 B 是否仍为空、两个 items 是否不是同一对象。这个小检查能把“值看起来一样”和“身份真的独立”区分开。

first = Basket()
second = Basket()

# 先检查两个新实例都从空列表开始。
assert first.items == []
assert second.items == []

# 只修改对象 A;对象 B 不应被连带修改。
first.items.append("python")
assert first.items == ["python"]
assert second.items == []

# is 检查身份,确认不是同一个列表对象。
assert first.items is not second.items

可以把结果整理成一张速查表:

写法传给字段的东西适用判断
default_factory=list零参数可调用类型空列表,推荐
default_factory=lambda: [...]零参数函数每次创建带初始内容的新列表
default_factory=[]已创建的列表实例实例化时不可调用,错误
default=[]共享候选对象可变默认值风险,不应使用
Python dataclass 两个实例分别持有 items 列表并通过 is 检查隔离关系的技术图
图2:观察对象 A、对象 B 与各自 items 的独立关系,检查 append 是否越过实例边界。

有依赖关系时不要硬塞进零参数工厂

default_factory 适合“只要一个新对象”的初始化。如果列表内容必须根据构造参数决定,工厂拿不到 self,此时可以先给空列表,再在 __post_init__ 中补齐;或者直接编写类方法构造器,把依赖关系写在更明确的位置。

from dataclasses import dataclass, field

@dataclass
class Plan:
    name: str
    tasks: list[str] = field(default_factory=list)

    def __post_init__(self) -> None:
        # 只有调用方未提供任务时,才根据 name 生成实例自己的初始内容。
        if not self.tasks:
            self.tasks.append(f"prepare:{self.name}")

这里的边界是:工厂负责创建容器,__post_init__ 负责读取实例字段并决定内容。若调用方显式传入任务,应该再根据业务规则决定是否保留空列表判断,避免把“用户明确传入空列表”和“没有提供任务”混为一谈。

常见问题

default_factory=listdefault_factory=lambda: [] 有区别吗?

对空列表这个场景,结果都能做到每个实例新建列表;list 更短、更直接。需要固定初始元素或更复杂的无参创建逻辑时,才使用 lambda 或命名工厂。

为什么元组可以直接写成 default=()

元组通常作为不可变默认值使用,多个实例共享同一个不可变对象不会产生 append 一类的状态串改。只要容器内容需要修改,就回到对应的 default_factory

工厂可以写成带参数的函数吗?

不能直接这样写。数据类会以零参数调用工厂;需要外部参数时,用闭包捕获固定值、在 __post_init__ 中初始化,或提供显式构造方法。

记住一句话:default 放值,default_factory 放“如何创建值”的可调用对象。对列表、字典和集合,优先把类型本身交给工厂,再用两个实例的内容与 is 检查确认隔离。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>