Python 从入门到精通
第 19 / 36 篇
从环境安装和基础语法出发,逐步学习工程实践、自动化、数据处理与 Web 开发。
-
01
Python 怎么装、怎么用 -
02
变量、数字和字符串 -
03
列表:把一组数据放在一起 -
04
元组、集合和字典 -
05
条件判断:让程序做选择 -
06
循环:重复的事交给程序 -
07
函数:把代码整理成可复用的块 -
08
模块与包:拆分你的程序 -
09
输入、输出与字符串格式化 -
10
文件读写:保存程序的数据 -
11
异常处理:程序出错时怎么办 -
12
基础阶段练习:命令行记账本 -
13
类和对象:面向对象入门 -
14
继承、组合与特殊方法 -
15
迭代器与生成器 -
16
列表推导式与生成器表达式 -
17
装饰器:给函数增加能力 -
18
上下文管理器与 with -
19
类型标注与 dataclass正在阅读 -
20
正则表达式:从文本中找规律 -
21
日期、时间与时区 -
22
日志与调试 -
23
虚拟环境与依赖管理 -
24
测试:让修改不再提心吊胆 -
25
网络请求:用 Python 调用 API -
26
网页解析与合规采集 -
27
操作 Excel、CSV 与批量文件 -
28
SQLite:给程序加一个数据库 -
29
数据分析入门:NumPy 与 Pandas -
30
画图:把数据变得直观 -
31
Flask 入门:做一个小网站 -
32
异步编程:同时处理多项任务 -
33
线程、进程与并发选择 -
34
性能分析与优化 -
35
项目结构、配置与发布 -
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
练习
- 给记账本的每条记录定义 dataclass:类别、金额、备注、日期,并写一个打印函数
- 给一个函数加类型标注:接收字符串列表,返回每个字符串长度的列表
- 思考:类型标注不影响运行,那它到底有什么用?什么情况下”标注 + mypy”能帮你省下几个小时?