Python dataclass 与类型注解
AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。
类型注解让编辑器和 CI 帮你找错;@dataclass 让「装数据的类」少写几十行样板代码。
环境准备
python -m pip install mypy==1.13.0
mypy --version
dataclasses 是标准库,Python 3.7+ 自带。
类型注解语法
name: str = "小明"
count: int = 0
def greet(name: str) -> str:
return f"你好,{name}"
class Config:
retries: int
hosts: list[str]
容器与联合类型(3.10+ 推荐内置泛型):
from collections.abc import Sequence
def total(values: Sequence[int]) -> int: ...
mapping: dict[str, int]
maybe: int | None = None # 等价于 Optional[int]
pair: tuple[int, str]
handler: Callable[[str], None]
需要复用时定义别名:
type UserId = int # 3.12+ 的 type 语句
JSONValue = dict[str, "JSONValue"] | list["JSONValue"] | str | int | float | bool | None
注解不影响运行,Python 解释器不检查它们:
x: int = "abc" # 运行时不报错,mypy 会报错
用 mypy 检查
mypy src/
mypy --strict src/ # 更严格:要求所有函数都有注解
典型输出:
src/app.py:12: error: Argument 1 to "greet" has incompatible type "int"; expected "str"
常见配置(pyproject.toml):
[tool.mypy]
python_version = "3.13"
strict = true
编辑器里装 Python 扩展(Pylance / pyright)能实时看到同类提示。
dataclass:装数据的类
手写:
class Point:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
def __repr__(self): ...
def __eq__(self, other): ...
用 dataclass:
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
p = Point(1, 2)
p # Point(x=1, y=2),自动生成 __repr__
Point(1, 2) == Point(1, 2) # True,自动生成 __eq__
它自动生成 __init__、__repr__、__eq__,字段顺序就是注解顺序。
默认值与 default_factory
from dataclasses import dataclass, field
@dataclass
class Team:
name: str
members: list[str] = field(default_factory=list) # 每个实例一个新列表
created: str = ""
可变默认值必须用 default_factory,直接写 members: list[str] = [] 会触发 ValueError: mutable default ...(dataclass 会拦住这个坑)。
其他常用参数
@dataclass(order=True) # 生成 __lt__ 等,可排序
class Version:
major: int
minor: int
@dataclass(frozen=True) # 不可变,可哈希,可当字典键
class Settings:
host: str
port: int = 8000
@dataclass(slots=True) # 3.10+,省内存、属性写错会报错
class Row:
id: int
@dataclass
class User:
name: str
slug: str = field(init=False) # 不在 __init__ 里,自己算
def __post_init__(self) -> None:
self.slug = "-".join(self.name.lower().split())
frozen=True 的类实例不能改属性,适合配置对象;需要「改一个字段」时用 dataclasses.replace(obj, port=9000)。
转换
from dataclasses import asdict, astuple, replace
asdict(team) # {'name': ..., 'members': [...]}
astuple(point) # (1, 2)
replace(point, x=9) # Point(x=9, y=2)
dataclass vs NamedTuple
| 需求 | 选择 |
|---|---|
| 需要可哈希、当字典键、可解包 | NamedTuple |
| 需要可变、默认值、方法、继承 | @dataclass |
| 配置对象(不可变) | @dataclass(frozen=True) |
| 只是两三个字段的中间结果 | 普通 tuple |
常见错误与注意事项
1. 以为注解会校验运行时的值
def add(a: int, b: int) -> int: ...
add("1", "2") # 运行时照样执行,不会报错
校验要靠 pydantic 这类库,或在函数里显式检查。
2. dataclass 里写可变默认值
用 field(default_factory=list)。
3. 字段顺序错误
有默认值的字段必须写在没有默认值的字段后面,和普通函数参数规则一致。
4. 用 Any 掩盖问题
Any 让检查失效,只在确实「任意类型」时用;临时绕过写 # type: ignore[arg-type] 并注明原因。
5. 冻结类里放列表
frozen=True 只阻止替换属性,列表内容仍可改:
s = Settings(host="a") # 字段是字符串,没问题
# 若字段是 list,仍可 s.items.append(...)
需要完全不可变就用元组或 frozenset。
小结
- 注解是给人和工具的说明,用 mypy / pyright 在提交前检查。
@dataclass自动生成初始化、repr、比较,可变默认值用field(default_factory=...)。frozen=True得到不可变对象,slots=True省内存。- 需要可哈希与解包用
NamedTuple,需要方法与可变性用 dataclass。