Python dataclass 与类型注解

AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。

Python 3.13 · mypy 1.13

类型注解让编辑器和 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。

下一步