Clean Code(3): 在Python中使用dataclass/pydantic,而不是Dict/JSON做类型

在AI的加持下,可擦除的类型有优势,例如 Python 的 dataclass/dataclass_json, 以及TypeScript 对于 JS 的外置类型。这是因为:

  1. 编程语言的类型写给人/AI/编译器三种角色看的。人用来理解结构,AI用来理解上下文,编译器用来卡通过。
  2. 编译器检查太严格,会失去一丢丢灵活性,例如像C++那样严格要求类型声明正确,代码可能完成了 90%,还有10%死活过不去,例如一个函数的返回值类型,没写出来就编译不过。
  3. AI主要是通过类型知道应该如何改代码,但是不幸的是,如果AI 看到了一个C++ 的 auto x=...,局部不知道这个是啥类型是什么,那么如果要用这个类型做一个新的函数的返回值,就难以写对。写不对,编译器就不给过。
  4. 但是对于可擦除类型来说,看不到的这部分,可以不声明类型,就能过。当然损失的是这里的类型不够安全,但是对于使用动态语言的场景来说也够了。至少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--

posted @ 2025-06-12 22:28  ffl  阅读(68)  评论(0)    收藏  举报