← Back to 文字

类、数据类与类型注解

本文来自《AI 应用开发课程》月份 1 课程文档,已整理为网站文章版本。

学习目标

学完本节后,你应当能够:

  • 理解什么时候用函数,什么时候用类。
  • 理解数据类的用途。
  • 读懂并书写基础类型注解。
  • 为后续 Pydantic 模型学习打下基础。

前置知识

  • 已完成 01-Python最小入门.md
  • 已完成 02-函数-模块-包.md

1. 为什么这节重要

月份 1 后面会频繁出现下面这些结构:

  • ChatMessage
  • ChatRequest
  • ChatResponse
  • ToolDefinition

它们本质上都是“有结构的数据”。如果你不会用类或数据类,就很难把这些对象组织清楚。

2. 类的最小示例

class ChatMessage:
    def __init__(self, role: str, content: str) -> None:
        self.role = role
        self.content = content

    def summary(self) -> str:
        return f"[{self.role}] {self.content}"

使用:

message = ChatMessage(role="user", content="你好")
print(message.summary())

适合用类的场景:

  • 数据和行为需要放在一起
  • 对象需要长期复用
  • 结构比普通字典更稳定

3. 数据类

如果一个对象主要是“装数据”,Python 提供了更简洁的写法:dataclass

from dataclasses import dataclass


@dataclass
class ChatMessage:
    role: str
    content: str

这比手写 __init__ 更简洁。

你可以把数据类理解为:

  • 比字典更清晰
  • 比完整类更轻量
  • 很适合表达“结构化数据”

4. 类型注解基础

基础写法

name: str = "leo"
age: int = 18
score: float = 99.5

函数类型

def add(x: int, y: int) -> int:
    return x + y

列表和字典

messages: list[str] = ["hello", "hi"]
profile: dict[str, str] = {"name": "leo"}

可选值

from typing import Optional

nickname: Optional[str] = None

5. 为什么动态语言还要写类型注解

原因不是“为了让 Python 变成静态语言”,而是为了:

  • 增强可读性
  • 让编辑器和检查工具更早发现错误
  • 降低多人协作成本
  • 让复杂接口更容易理解

月份 1 所有关键代码都应带基本类型注解。

6. 从字典升级到结构化对象

错误示例:

message = {"role": "user", "content": "hello"}

这不是不能用,而是问题很多:

  • 字段名容易拼错
  • 没有清晰约束
  • 没有类型提示

更好的版本:

from dataclasses import dataclass


@dataclass
class ChatMessage:
    role: str
    content: str

这也是后面 Pydantic 模型出现前的过渡训练。

7. 实操任务

完成以下练习:

  1. 用类定义一个 UserProfile,包含 namerole
  2. 用数据类定义一个 ChatMessage
  3. 给 3 个函数补全参数和返回值类型注解。

8. 自测题

  1. 字典、类、数据类的差异是什么?
  2. 为什么 ChatMessage 这种结构不建议长期用裸字典表示?
  3. 为什么类型注解对后续 FastAPI 和 Pydantic 很关键?

9. 作业与验收

作业:

  • 定义 ChatMessageToolCallResult 两个数据类。

验收标准:

  • 字段命名清晰
  • 都有类型注解
  • 能在一个主程序里实例化并打印

10. 常见错误

  • list[str] 写成 list(str)
  • 忘记写返回值类型
  • 用类存数据但没有明确字段

11. 延伸阅读

  • Python 官方文档中的 dataclasses
  • Real Python 的 type hints 教程

12. 本章与前文关系

上一章解决了“代码怎么拆模块”,这一章解决“数据怎么表达”。这一步很重要,因为月份 1 后面所有核心对象都不是随便一段字符串,而是结构化数据:

  • ChatMessage
  • ChatRequest
  • ChatResponse
  • ToolDefinition
  • ToolCallResult

如果你不会用类、数据类和类型注解表达结构,后面几乎每章都会感觉别扭。

13. 本章在研发助手项目中的位置

研发助手项目中的很多关键对象,都需要从“裸字典”升级为“明确结构的对象”。比如:

  • 用户传来的消息
  • 模型返回的结构化结果
  • 工具定义与工具结果
  • API 的请求和响应

你现在学的,不是面向抽象语法题,而是面向真实项目对象建模。

14. 裸字典、普通类、数据类、Pydantic 模型的关系

这是月份 1 非常关键的一条认知主线。

裸字典

优点:

  • 上手快
  • 写起来少

缺点:

  • 字段名容易拼错
  • 没有边界
  • 类型不清晰

普通类

优点:

  • 可以把数据和行为放在一起
  • 更适合表达一个“对象”

缺点:

  • 如果只是装数据,会显得稍重

数据类

优点:

  • 比普通类更轻量
  • 很适合表达“主要是存数据的对象”

缺点:

  • 校验能力有限

Pydantic 模型

优点:

  • 有数据校验
  • 错误信息清晰
  • 和 FastAPI 天然配合

缺点:

  • 比纯 Python 对象多一层框架依赖

月份 1 的学习路线其实就是:

裸字典 -> 普通类 / 数据类 -> Pydantic 模型

15. 错误示例 vs 正确示例

错误示例:长期用裸字典表达消息

message = {"rol": "user", "content": "hello"}

这里字段名 role 拼错成了 rol,程序未必会在第一时间发现。

正确示例:使用结构化对象

from dataclasses import dataclass


@dataclass
class ChatMessage:
    role: str
    content: str

这种写法至少让“消息应该有哪些字段”变得清晰了。

16. 完整文件级示例:dataclass_demo.py

"""使用普通类和数据类表达聊天消息。"""

from dataclasses import dataclass


class LegacyChatMessage:
    """用普通类表达消息。

    这种写法更接近你在很多面向对象语言中的直觉。
    """

    def __init__(self, role: str, content: str) -> None:
        self.role = role
        self.content = content

    def summary(self) -> str:
        return f"[{self.role}] {self.content}"


@dataclass
class ChatMessage:
    """使用数据类表达消息。

    当对象主要是“装数据”时,数据类通常比手写 __init__ 更清晰。
    """

    role: str
    content: str

    def summary(self) -> str:
        return f"[{self.role}] {self.content}"


def main() -> None:
    old_message = LegacyChatMessage(role="user", content="请总结这个 diff")
    new_message = ChatMessage(role="assistant", content="这是摘要结果")

    print(old_message.summary())
    print(new_message.summary())


if __name__ == "__main__":
    main()

17. 逐段解释这份完整示例

为什么保留 LegacyChatMessage

因为你可能来自 Java、C#、Go 等语言,用普通类会更容易建立迁移直觉。课程先让你看到“传统写法”,再让你看到“Python 更自然的写法”。

为什么 ChatMessage 更适合用数据类

因为它目前主要承担“数据容器”的角色,数据类能自动生成初始化逻辑,让代码更短、更清晰。

为什么仍然可以保留方法

很多人误以为数据类只能装字段。其实只要有必要,它仍然可以有方法,比如这里的 summary()

18. 类型注解的工程意义

类型注解不是为了把 Python 变成 Java,而是为了让你在复杂系统中少犯低级错误。

以月份 1 为例,类型注解至少能帮助你做到:

  • 快速理解函数输入输出
  • 让编辑器给出更好的提示
  • 在重构时更少踩坑
  • 为后面 Pydantic 和 FastAPI 做铺垫

19. 一个更接近后续项目的增强版示例

下面是更贴近月份 1 后续风格的表达:

from dataclasses import dataclass
from typing import Literal


ChatRole = Literal["system", "user", "assistant", "tool"]


@dataclass
class ChatMessage:
    role: ChatRole
    content: str

这里多出来的关键点是 Literal

  • 它不是任意字符串
  • 它在告诉读者:角色只能是这几个值

这就是“类型注解从可读性走向约束表达”的开始。

20. 从最小实现到工程实现的递进

你可以把本章理解成三段升级:

第一步:字典先能装数据

适合最初上手,但边界弱。

第二步:数据类让结构更清晰

适合建立对象表达习惯。

第三步:Pydantic 让结构同时具备“清晰 + 校验”

适合进入 FastAPI、API schema、结构化输出等阶段。

下一模块的 Pydantic 文档,会承接这里继续往下走。

21. 调试与排错:本章最常见问题

问题一:类型注解写法不熟

例如把:

list[str]

写成:

list(str)

问题二:把数据类当成 Pydantic

数据类会帮你自动生成 __init__,但它不会像 Pydantic 那样做强校验。

问题三:把对象结构设计得过早过重

月份 1 不需要一开始就设计很复杂的类层级。目标是先把消息、请求、结果这些核心结构表达清楚。

22. 本章完成后你应该具备的能力

完成本章后,你应当做到:

  1. 能解释字典、普通类、数据类的区别。
  2. 能为一个简单对象写出数据类。
  3. 能读懂常见类型注解。
  4. 能理解为什么月份 1 后面要升级到 Pydantic。

23. 如果你卡在这里,先回看哪几章

24. 从本章过渡到下一章的桥接说明

接下来进入 04-异常-文件-JSON.md

这很自然:既然你已经学会如何表达结构化对象,下一步就该学习这些对象如何被保存、加载和在失败时正确处理。后面的 API 调用、结构化输出、日志记录,本质都离不开异常处理和 JSON。

Fin