在日常项目开发里,我们习惯直接用字典承载业务数据,到处都是 data["字段名"] 的取值写法。随着项目迭代、字段慢慢变多,就会暴露出一些问题:拼写错键名不会提前预警、入参结构模糊没法看明白、缺少默认配置只能手动兜底赋值。而 Python 标准库里的 dataclasses,可以用短短几行代码就能自动生成构造方法、打印、等值对比逻辑,同时原生支持类型注解、字段默认值,从而规避字典带来的各类隐性 BUG。
在正式开始前,我们看看日常的写法中的弊端
# 字典无类型标注、无IDE提示、键写错静默报错task = { "id": "task-01", "name": "Generate chapter", "status": "pending", "retries": 0,}task["statsu"] # 拼写错误,运行时才抛出KeyError中断程序task.get("statsu") # 静默返回None,业务逻辑悄悄出错,极难排查# 函数入参只标注dict,使用者完全不清楚字典内部有哪些字段def process(task: dict) -> dict: task["status"] = "done" return task
而dataclass可以很好的处理这些问题,只需要导入@dataclass装饰器标注类,python就会自动帮我们生成__init__、__repr__、__eq__等常用方法。
from dataclasses import dataclass@dataclassclass Task: id: str name: str status: str = "pending" # 字段默认值 retries: int = 0task = Task(id="task-01", name="Generate chapter")print(task.status) # IDE自动补全属性名称,编码阶段就能提示# task.statsu → 属性拼写错误,编辑器立刻标红、运行直接抛AttributeError
对比字典的静默报错,dataclass 在编码或运行初期就能拦截拼写失误,大幅降低线上故障概率。实际开发中字段场景各不相同,有的字段不能打印输出敏感信息、有的需要后置动态计算、列表字典这类可变类型不能直接配置默认值,field 就是用来解决这些个性化需求的。
很多新手会直接给 list、dict 设置字面量默认值,这会导致所有实例共用同一个容器对象,修改一处全部实例同步变化,属于高频踩坑点,规范写法是通过 default_factory 动态创建容器。
from dataclasses import dataclass, fieldfrom typing import Optional# 错误写法:所有实例共用同一个列表@dataclassclass BadDemo: tags: list = []# 标准写法@dataclassclass PipelineTask: id: str name: str code: str status: str = "pending" retries: int = 0 error: Optional[str] = None tags: list[str] = field(default_factory=list) meta: dict = field(default_factory=dict)task = PipelineTask("t01", "Print hello", "print('hi')")task.tags.append("validated")# dataclass自动按字段值做等值比对,而非对比对象内存地址t1 = PipelineTask("t01", "x", "y")t2 = PipelineTask("t01", "x", "y")print(t1 == t2) # True
除了默认值配置,field 还支持 repr、init、compare 等参数,灵活控制字段在打印、实例化、等值判断中的表现,搭配 __post_init__ 还能在实例创建后自动执行自定义逻辑。
from dataclasses import dataclass, fieldfrom datetime import datetime@dataclassclass Article: title: str body: str api_token: str = field(default="", repr=False) # repr打印时隐藏密钥,避免日志泄露 word_count: int = field(default=0, init=False) # 实例化入参不需要传递该字段 created_at: datetime = field(default_factory=datetime.now, compare=False) # 等值对比忽略创建时间 def __post_init__(self): # __init__执行完成后自动运行,用于派生字段赋值、入参校验 self.word_count = len(self.body.split())art = Article("My Ebook", "Hello world this is a test")print(art.word_count) # 6print(repr(art)) # api_token 不会出现在打印字符串中
本次我们就先到这里,下次我们会继续对_post_init 进行扩展。