系列文章

Python 从入门到精通

第 19 / 36 篇

从环境安装和基础语法出发,逐步学习工程实践、自动化、数据处理与 Web 开发。

  1. 01
    Python 怎么装、怎么用
  2. 02
    变量、数字和字符串
  3. 03
    列表:把一组数据放在一起
  4. 04
    元组、集合和字典
  5. 05
    条件判断:让程序做选择
  6. 06
    循环:重复的事交给程序
  7. 07
    函数:把代码整理成可复用的块
  8. 08
    模块与包:拆分你的程序
  9. 09
    输入、输出与字符串格式化
  10. 10
    文件读写:保存程序的数据
  11. 11
    异常处理:程序出错时怎么办
  12. 12
    基础阶段练习:命令行记账本
  13. 13
    类和对象:面向对象入门
  14. 14
    继承、组合与特殊方法
  15. 15
    迭代器与生成器
  16. 16
    列表推导式与生成器表达式
  17. 17
    装饰器:给函数增加能力
  18. 18
    上下文管理器与 with
  19. 19
    类型标注与 dataclass正在阅读
  20. 20
    正则表达式:从文本中找规律
  21. 21
    日期、时间与时区
  22. 22
    日志与调试
  23. 23
    虚拟环境与依赖管理
  24. 24
    测试:让修改不再提心吊胆
  25. 25
    网络请求:用 Python 调用 API
  26. 26
    网页解析与合规采集
  27. 27
    操作 Excel、CSV 与批量文件
  28. 28
    SQLite:给程序加一个数据库
  29. 29
    数据分析入门:NumPy 与 Pandas
  30. 30
    画图:把数据变得直观
  31. 31
    Flask 入门:做一个小网站
  32. 32
    异步编程:同时处理多项任务
  33. 33
    线程、进程与并发选择
  34. 34
    性能分析与优化
  35. 35
    项目结构、配置与发布
  36. 36
    综合项目:从需求到上线

查看整个系列 →

Python 是动态类型语言,变量不需要声明类型,写起来很爽。但项目一大就发现问题:函数接收什么类型、返回什么类型,全靠猜,改代码时心里没底。这一篇介绍两个提升”心里有底”的利器:类型标注(type hints)dataclass

类型标注:给代码加说明书

Python 3.5+ 支持在函数签名里标注类型,3.10+ 语法更简洁:

def greet(name: str) -> str:
    return f"你好,{name}"

def add(a: int, b: int) -> int:
    return a + b

print(add(1, 2))        # 3
print(add("1", "2"))    # "12" —— 注意:标注只是说明,不会强制检查!

要点:

  • 参数: 类型 标注参数,-> 类型 标注返回值
  • 标注不影响运行——传错类型照样跑,它主要是给人看 + 给工具检查
  • 好处:代码自文档化、编辑器自动补全提示、配合 mypy 静态检查

内置类型和 typing 模块

简单类型直接用内置的:int float str bool list dict set tuple。但更常见的是”列表里装什么”,用 typing 模块(Python 3.9+ 也可直接用内置泛型):

# Python 3.9+ 推荐写法(内置泛型)
def total(scores: list[int]) -> int:
    return sum(scores)

def find(name: str, data: dict[str, int]) -> int | None:
    # dict[str, int]:键是 str,值是 int
    # int | None:可能是 int 也可能是 None
    return data.get(name)

# 旧写法(typing 模块,老代码常见,也兼容)
from typing import List, Dict, Optional, Union

def old_style(names: List[str]) -> Optional[int]:
    return len(names) if names else None

常用组合:

from typing import Optional, Union, Any

x: Optional[int] = None      # 等价 int | None
y: Union[int, str] = "a"     # 等价 int | str
z: Any = "随便什么"         # 任何类型(少用,等于没标注)

def process(items: list[tuple[str, int]]) -> dict[str, int]:
    """接收 [(名字, 分数), ...],返回 {名字: 分数}"""
    return dict(items)

mypy:让类型检查真正生效

标注本身不检查,但可以用 mypy 工具做静态检查(装一次,随时用):

pip install mypy
mypy my_program.py

写错类型它会报:

def add(a: int, b: int) -> int:
    return a + b

add("1", 2)    # mypy 报错:Argument 1 has incompatible type "str"; expected "int"

mypy 能在运行前揪出一批低级错误,大项目标配。小脚本不强制,但养成标注习惯,受益终身。

dataclass:写数据类的偷懒神器

之前写类,最烦的是样板代码——__init__ 里一个一个 self.xxx = xxx:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Point(x={self.x}, y={self.y})"

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

@dataclass,这些全部自动生成:

from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

p1 = Point(3, 4)
p2 = Point(3, 4)
print(p1)            # Point(x=3, y=4)(自动 __repr__)
print(p1 == p2)      # True(自动 __eq__,按字段比较)
print(p1.x)          # 3

就这一小段,自动帮你写了 __init__、__repr__、__eq__。字段声明即属性:x: int 就是”有个 int 类型的字段 x”。

dataclass 进阶

from dataclasses import dataclass, field

@dataclass
class Student:
    name: str
    scores: list[int] = field(default_factory=list)   # 可变默认值要用 factory!
    grade: str = "未分班"                               # 普通默认值
    active: bool = True

s = Student("小明")
s.scores.append(92)     # 每个实例自己的列表(不会共享)
print(s)                # Student(name='小明', scores=[92], grade='未分班', active=True)

# 冻结:不可变对象
@dataclass(frozen=True)
class Config:
    host: str
    port: int

c = Config("localhost", 8080)
# c.port = 9090         # 报错!frozen 不能改

# 排序支持
@dataclass(order=True)
class Score:
    value: int
    name: str

print(sorted([Score(88, "b"), Score(92, "a")]))   # 按 value 排

重点记住:可变默认值(列表、字典)必须用 field(default_factory=list),直接写 = [] 会掉进”所有实例共享同一个列表”的经典坑(函数默认参数那篇讲过同一个问题)。

什么时候用 dataclass

适合:纯数据容器(点、学生、配置、API 响应),没有复杂逻辑的类。

不适合:有继承体系、需要精细控制 __init__ 逻辑的类。

一句话:只装数据用 dataclass,有行为逻辑用普通 class

新手坑

坑 1:字段顺序。有默认值的字段必须排在没默认值的后面(和函数参数规则一样)。

坑 2:可变默认值忘 factory。scores: list = [] 所有实例共享,改一个全变。

坑 3:以为标注会检查类型。标注不拦运行,传错类型照样跑,要检查得靠 mypy 或运行时校验。

小结

  • 类型标注:参数: 类型-> 类型,不强制但提升可读性,配合 mypy 静态检查
  • 复杂类型:list[int]dict[str, int]int | None(Optional)
  • @dataclass 自动生成 __init__/__repr__/__eq__,纯数据类的偷懒神器
  • 可变默认值用 field(default_factory=list);frozen=True 变不可变
  • 数据容器用 dataclass,行为逻辑用普通 class

练习

  1. 给记账本的每条记录定义 dataclass:类别、金额、备注、日期,并写一个打印函数
  2. 给一个函数加类型标注:接收字符串列表,返回每个字符串长度的列表
  3. 思考:类型标注不影响运行,那它到底有什么用?什么情况下”标注 + mypy”能帮你省下几个小时?