一、dataclasses --- 不止是「自动生成 init」
📘 知识点简介
dataclasses 是 Python 3.7+ 引入的标准库装饰器,绝大多数人只用了它最浅层的能力------省掉几行 __init__ 样板代码。实际上,它还隐藏了 冻结实例、字段校验、后置处理、性能优化(slots) 等一系列实用特性。
🔧 核心功能速览
| 参数 / 函数 | 作用 |
|---|---|
@dataclass(frozen=True) |
实例不可变,类似 NamedTuple 但更强大 |
@dataclass(slots=True) |
Python 3.10+,减少内存占用约 30-50% |
field(default=..., repr=False) |
精细控制每个字段的行为 |
__post_init__ |
初始化后自动调用的钩子,适合校验与派生字段 |
field(default_factory=list) |
避免可变默认参数的巨坑 |
💻 实战代码
python
from dataclasses import dataclass, field
from typing import Optional
from datetime import datetime
# ── 基础防坑:用 default_factory 而非 mutable default ──
@dataclass
class ShoppingCart:
items: list = field(default_factory=list) # ✅ 正确
# items: list = [] # ❌ 所有实例共享同一个列表,经典踩坑
def add(self, item: str):
self.items.append(item)
# ── frozen + slots + post_init 组合拳 ──
@dataclass(frozen=True, slots=True)
class Order:
order_id: str
items: tuple = field(default_factory=tuple)
created_at: Optional[datetime] = None
_total: float = field(init=False, repr=False, default=0.0)
def __post_init__(self):
# ❗在 frozen=True 下不能直接 self.xxx = ...
# 需要用 object.__setattr__
total = sum(getattr(item, 'price', 0) for item in self.items)
object.__setattr__(self, '_total', total)
@property
def total(self) -> float:
return self._total
# ── 运行验证 ──
cart = ShoppingCart()
cart.add("鼠标")
cart.add("键盘")
order = Order(
order_id="ORD-20260624-001",
items=[{"name": "鼠标", "price": 99}, {"name": "键盘", "price": 299}],
created_at=datetime.now()
)
print(f"购物车: {cart.items}") # ['鼠标', '键盘']
print(f"订单总价: ¥{order.total}") # ¥398.0
print(f"不可变尝试: ", end="")
try:
order.order_id = "hacked" # ❌ frozen 禁止修改
except AttributeError as e:
print(f"拒绝修改 → {e}")
# slots 验证:没有 __dict__
print(f"slots 内存优化: {'__dict__' not in dir(order)}") # True
⚠️ 避坑指南
- frozen 下赋值用
object.__setattr__,不要手写self.field = value,否则__post_init__内也会抛错。 slots=True只在 Python 3.10+ 可用 ,且会失去__dict__,无法动态添加属性,序列化时需留意(dataclasses.asdict()不受影响)。- 字段顺序决定
__init__参数顺序 ,有默认值的字段必须放在无默认值字段之后,否则报TypeError。
二、functools.singledispatch --- 告别 if-else 类型分发
📘 知识点简介
Python 没有原生函数重载。过去我们这样写:
python
def process(data):
if isinstance(data, str):
...
elif isinstance(data, list):
...
elif isinstance(data, dict):
...
代码又臭又长,扩展时需要修改原函数。functools.singledispatch 让我们像写重载一样优雅地按 第一个参数类型 分派函数。
🔧 核心函数
python
from functools import singledispatch
@singledispatch装饰基础函数(默认兜底行为)@func.register(type)注册针对特定类型的实现func.dispatch(type)手动查询某个类型对应哪个实现func.registry查看所有已注册的分发
💻 实战代码
python
from functools import singledispatch
from typing import Any
import json
@singledispatch
def serialize(data: Any) -> str:
"""兜底:尝试 JSON 序列化"""
return json.dumps(data, ensure_ascii=False)
@serialize.register(str)
def _(data: str) -> str:
# 字符串直接返回,不需额外引号
return data
@serialize.register(bytes)
def _(data: bytes) -> str:
return data.hex()
@serialize.register(list)
@serialize.register(tuple)
def _(data) -> str:
items = ", ".join(serialize(item) for item in data)
return f"[{items}]"
@serialize.register(dict)
def _(data: dict) -> str:
pairs = ", ".join(f"{k}: {serialize(v)}" for k, v in data.items())
return f"{{{pairs}}}"
@serialize.register(type(None))
def _(data: None) -> str:
return "∅"
# ── 运行验证 ──
test_data = [
"hello",
None,
b"\x01\x02\x03",
[1, "two", None],
{"name": "Python", "version": 3.12}
]
for item in test_data:
print(f" {type(item).__name__:>8s} → {serialize(item)}")
# 输出:
# str → hello
# NoneType → ∅
# bytes → 010203
# list → [1, two, ∅]
# dict → {name: Python, version: 3.12}
# ── 进阶:继承关系自动匹配 ──
from collections import OrderedDict
print(serialize(OrderedDict([("a", 1), ("b", 2)])))
# dict 的注册会匹配 OrderedDict(继承链)
⚠️ 避坑指南
- 只匹配第一个参数的类型 ,这是
singledispatch 的含义。多参数重载需用第三方库(如multipledispatch)。 - 子类型会自动降级到父类型 :如果
OrderedDict没有专门注册,会走dict的实现;如果仍没有,走兜底函数。合理利用这一点可以减少冗余注册。 - 注册的函数名无所谓 (惯例用
_),因为通过dispatch(type)查找,不会直接调用。 - 类型注解只是装饰 :
register接受的是运行时传入的类型对象,注解-> str只起文档作用。
三、组合拳:dataclass + singledispatch 实现类型安全的序列化器
将两个知识点结合,做一个实用的 自定义序列化器:
python
from dataclasses import dataclass, field, asdict
from functools import singledispatch
from datetime import datetime
from typing import Any
# ── 用 singledispatch 处理不同类型的序列化 ──
@singledispatch
def to_dict(obj: Any) -> dict:
return asdict(obj) if hasattr(obj, '__dataclass_fields__') else dict(obj)
@to_dict.register(datetime)
def _(dt: datetime) -> str:
return dt.isoformat()
# ── dataclass 享受自动递归序列化 ──
@dataclass
class Address:
city: str
street: str
@dataclass
class User:
name: str
age: int
address: Address
created_at: datetime = field(default_factory=datetime.now)
user = User("张三", 28, Address("北京", "朝阳区"))
print(to_dict(user))
# {'name': '张三', 'age': 28, 'address': {'city': '北京', 'street': '朝阳区'}, 'created_at': '2026-06-24T09:30:00'}
📌 总结
| 技巧 | 一句话记住 |
|---|---|
dataclass(frozen=True) |
不可变数据模型,比 NamedTuple 功能更丰富 |
dataclass(slots=True) |
Python 3.10+ 高性能数据类,内存减半 |
__post_init__ + object.__setattr__ |
frozen 下的初始化后处理方案 |
singledispatch |
优雅替代 if isinstance,开闭原则的最佳实践 |
register 继承匹配 |
子类型自动降级到父类型,减少冗余代码 |
今天的两个技巧都来自 标准库 ,零依赖即可大幅提升代码质量。建议在你的下一个项目里试试 @dataclass(slots=True) 和 singledispatch 的组合,代码会变得更简洁、更 Pythonic。