Clean Code(3): 在Python中使用dataclass/pydantic,而不是Dict/JSON做类型
在AI的加持下,可擦除的类型有优势,例如 Python 的 dataclass/dataclass_json, 以及TypeScript 对于 JS 的外置类型。这是因为:
- 编程语言的类型写给人/AI/编译器三种角色看的。人用来理解结构,AI用来理解上下文,编译器用来卡通过。
- 编译器检查太严格,会失去一丢丢灵活性,例如像C++那样严格要求类型声明正确,代码可能完成了 90%,还有10%死活过不去,例如一个函数的返回值类型,没写出来就编译不过。
- AI主要是通过类型知道应该如何改代码,但是不幸的是,如果AI 看到了一个C++ 的 auto x=...,局部不知道这个是啥类型是什么,那么如果要用这个类型做一个新的函数的返回值,就难以写对。写不对,编译器就不给过。
- 但是对于可擦除类型来说,看不到的这部分,可以不声明类型,就能过。当然损失的是这里的类型不够安全,但是对于使用动态语言的场景来说也够了。至少AI可以把能加的都加上。
那么回到 Python,Python 的list/dict 太灵活以至于很容易用 dict 替代了很多其他语言里会严格定义类型来用的做法,但是 dict 带来的是项目的难以持续可维护。这就是灵活但是容易写出难以维护的代码。解决办法也是有的,推荐用 dataclass/dataclass_json 装饰器组合来修饰类,或者用更全面的 pydantic 库来组装类。
Python的dict 及其容易被直接用做数据类型,这是它灵活性的根源,但是也是它的代码难以应对大规模可维护代码的问题根源。最佳的做法是:dict仅仅用在@dataclass的字段。并且只能是 Dict[str, OtherType],这里 OtherType最好不是另外一个dict,而是另外一个类型。
在早期阶段或快速开发时,用 dict 做数据结构的确非常方便,但在 项目复杂度提高 时,它带来的问题也会逐渐暴露出来,比如:
- 字段不确定或拼写错误不报错 → 潜在的运行时 bug。
- 缺乏 IDE 补全和类型提示 → 降低开发效率。
- 难以统一修改和文档化 → 难以协作维护。
Good case
from dataclasses import dataclass
from typing import Dict
@dataclass
class FeatureConfig:
enabled: bool
version: str
@dataclass
class AppConfig:
features: Dict[str, FeatureConfig]
这样不仅保留了 dict[str, X] 的灵活性,还提供了:
- 结构清晰(每个值都是明确的数据模型)
- 静态检查(mypy、pylint 可以检查字段是否存在)
- IDE 支持好(可用 auto-complete)
- 易于单元测试(每一层都可 mock 或 assert)
Bad case
# 初始配置数据结构
config = {
"upload": {
"enabled": True,
"version": "v2"
},
"download": {
"enabled": False,
"version": "v1"
}
}
def add_feature(config, name, enabled=False, version="v1"):
config[name] = {
"enabled": enabled,
"version": version
}
def enable_all_features(config):
for feature in config.values():
feature["enabled"] = True
def print_feature_status(config):
for name, feature in config.items():
status = "enabled" if feature.get("enabled") else "disabled"
print(f"{name} is {status}, version: {feature.get('version', 'unknown')}")
问题爆发点:
字段名不能重构或重命名,一改动全崩。
- 没有 IDE 补全,写起来靠记忆和文档。
- 单元测试覆盖困难,很难 mock 某一项数据。
- 类型错误滥用(比如 version 是 int 而不是 str)无从发现。
重构
from dataclasses import dataclass
from typing import Dict
@dataclass
class FeatureConfig:
enabled: bool = False
version: str = "v1"
@dataclass
class AppConfig:
features: Dict[str, FeatureConfig]
# 重构后的数据
config = AppConfig(features={
"upload": FeatureConfig(enabled=True, version="v2"),
"download": FeatureConfig()
})
# 类型安全的操作
def enable_all_features(config: AppConfig):
for feature in config.features.values():
feature.enabled = True
def print_feature_status(config: AppConfig):
for name, feature in config.features.items():
status = "enabled" if feature.enabled else "disabled"
print(f"{name} is {status}, version: {feature.version}")
重构后:
- IDE 可以 自动补全字段名。
- mypy 可以发现所有字段拼写或类型错误。
- unittest 可以单独 mock 某个 FeatureConfig。
- 自动文档化、测试友好、代码整洁。
实际上,这个示例还不是太复杂,实际的随着需求变更的代码,dict会带来深层的代码质量滑坡和维护地狱。
除了使用 dataclass, 你还可以使用 pydantic,对于一个全新的项目, 建议一开始就有用 pydantic.
下一篇: Clean Code(4): 玩转 Python 的 @dataclass 与 @dataclass_json:从结构建模到 JSON 魔法
--end--

浙公网安备 33010602011771号