Week 2 学习计划|Python 函数、抽象与模块化
Week 2 从函数进阶出发,逐步学习模块、异常、数据建模、类型检查与工程化重构,最终完成一个具备模块化、日志、配置、类型检查、异常体系和插件式架构雏形的 Python 项目。
Week 2 核心主题:函数、抽象与模块化
本周目标不是单纯继续学习 Python 语法,而是开始从:
「会写 Python 代码」→「能够组织 Python 代码」→「能够设计一个小型项目」
逐步过渡。本周会把 Week 1 的简单数据结构与 Key-Value Store 项目继续向工程化方向推进。
一、Week 2 总览
| Day | 主题 | 对应目标 |
|---|---|---|
| Day 1 | 函数进阶:参数、作用域、闭包 | L1 |
| Day 2 | 模块与包:import 机制 | L1 |
| Day 3 | 异常处理 | L1 |
| Day 4 | dataclass 与 type hints | L1 |
| Day 5 | 项目重构(一):目录拆分、logging、配置 | L2 |
| Day 6 | 项目重构(二):类型检查、异常整合 | L2 |
| Day 7 | 插件式架构设计 + 周复盘 | L3 |
二、每日学习节奏
每天控制在 1.5–2 小时,固定节奏:
学习 45min → 实验 45min → 验收 15min
核心原则:理论不是最终目标,必须通过实验把概念变成自己的代码能力。因此如果当天理论已经理解,但实验没有真正写出来,则当天仍然不算完全完成。
Day 1|函数进阶:参数、作用域、闭包
时间:1.5–2h
学习 45min
阅读:
重点:默认参数、关键字参数、任意参数列表、Lambda、LEGB。
掌握:
- positional / keyword / default 参数
*args/**kwargs- 可变默认参数陷阱
- LEGB 作用域
global/nonlocal- 闭包:函数记住定义时的环境
- 函数是一等对象:可赋值、可传参、可返回
- lambda
sorted(key=...)map()/filter()
实验 45min
创建 function_demo.py,完成:
1. 三种参数传递方式
分别写出 positional、keyword、default 参数示例。
2. 可变默认参数陷阱
def append_to(item, target=[]):
target.append(item)
return target
print(append_to(1))
print(append_to(2))
print(append_to(3, []))
写出三次输出并解释原因。然后使用 target=None 进行修复。
3. log_call
def log_call(func, *args, **kwargs):
...
要求:打印函数名称、打印位置参数、打印关键字参数、调用原函数、返回原函数结果。
4. 闭包计数器
实现:
def make_counter():
...
要求:
counter = make_counter()
print(counter())
print(counter())
print(counter())
得到 1、2、3。同时验证:
a = make_counter()
b = make_counter()
两个计数器是否相互独立。
5. Lambda + sorted
构造 (name, age, score) 形式的数据,使用 lambda 实现多字段排序,例如先按年龄,再按成绩。
6. global
在函数内部使用 global 修改全局变量。观察:修改前、函数内部修改、修改后的全局变量,理解全局状态带来的副作用。
验收 15min
不用资料解释:
- 默认参数什么时候求值?
- 为什么 list 默认值危险?
*args和**kwargs有什么区别?- 闭包捕获的是值还是变量?
- "函数是对象"意味着哪三件事?
Day 2|模块与包:import 机制
时间:1.5–2h
学习 45min
阅读:Python Tutorial:Modules、Python Tutorial:Packages、sys.path、sys.modules。
掌握:
- module / package / subpackage
import x、from x import y、from x import y as z__name__ == "__main__"- re-export
- 绝对导入 / 相对导入
sys.modules/sys.path- import 缓存
- 循环导入
理解 import 流程:
检查 sys.modules → 没有则按 sys.path 查找 → 加载模块 → 执行模块代码 → 放入 sys.modules。
实验 45min
创建包结构:
mypkg/
├── __init__.py
├── core.py
└── utils/
├── __init__.py
└── text.py
完成:
1. 三种 import
分别实验 import mypkg.core、from mypkg import core、from mypkg.core import xxx,并理解它们绑定的对象有什么不同。
2. re-export
在 mypkg/__init__.py 中进行 re-export,验证 from mypkg import xxx 是否能够直接使用。
3. 模块运行方式
分别运行 python core.py 以及 python -m mypkg.core,观察 __name__ 有什么不同。理解:python -m 是按照模块路径运行模块,而不是简单按照文件路径执行文件。
4. sys.path
运行:
import sys
print(sys.path)
理解 Python 到哪里寻找模块。
5. 循环导入
人为制造 a.py → import b、b.py → import a,观察错误,然后修复。
重点实验:import 绑定关系
# counter.py
count = 0
def inc():
global count
count += 1
# main.py
import counter
from counter import count
counter.inc()
print(counter.count, count)
预测输出。重点解释:为什么两个 count 可能不一致?最终理解:from x import y 绑定的是当前时刻从模块中取得的对象引用,而不是之后动态读取 x.y。
验收 15min
回答:
- import 同一个模块两次,模块代码执行几次?为什么?
python file.py和python -m pkg.mod有什么区别?- 循环导入为什么发生?
- 最常见的解决思路是什么?
Day 3|异常处理:健壮性基础
时间:1.5–2h
学习 45min
阅读:Python Tutorial:Errors and Exceptions、内置异常层级、raise 文档。
重点掌握:try / except / else / finally、raise、异常链 raise ... from ...、自定义异常、EAFP、LBYL。
重点认识异常层级:
Exception
├── ValueError
├── TypeError
├── OSError
└── ...
以及:异常不是单纯"报错",而是一种控制异常情况的机制。
实验 45min
创建 exception_demo.py,完成:
1. parse_int
实现 parse_int(s),分别处理 ValueError、TypeError、None。
2. try / except / else / finally
全部使用,通过 print(...) 观察实际执行顺序。
3. 自定义异常
建立:
AppError
├── ConfigError
└── DataError
4. EAFP 改写
将 Week 1 项目中一段"先检查 → 再执行"的 LBYL 代码改写成"直接执行 → 捕获异常"的 EAFP 风格,比较两种写法的可读性。
5. 异常链
try:
int("abc")
except ValueError as e:
raise RuntimeError("配置解析失败") from e
观察 traceback 中 The above exception was the direct cause...,理解:新异常表达当前层面的业务语义,原异常保留底层原因。
6. finally + return
def test():
try:
return 1
finally:
return 2
观察最终返回值。
验收 15min
回答:
except Exception和裸except:有什么区别?- 为什么裸
except:危险? else什么时候执行?finally什么时候执行?- 什么情况下值得自定义异常?
- EAFP 是什么?相比 LBYL 有什么优势和风险?
Day 4|dataclass 与类型标注
时间:1.5–2h
学习 45min
阅读:dataclasses 官方文档、typing 官方文档、mypy Getting Started。
重点:dataclass、@dataclass、field、default_factory、frozen=True。
类型标注:变量标注、参数标注、返回值标注、Optional、Union、list[T]、dict[K, V]、Callable、Any。
理解:mypy 是静态类型检查工具,不负责在运行时执行类型检查。
实验 45min
创建 typed_demo.py。
1. Record
@dataclass
class Record:
...
字段:id、name、tags、created_at。其中 tags 必须使用 default_factory。
2. frozen=True
实验 item.name = "new",观察错误,理解不可变 dataclass。
3. 数据校验
增加 price >= 0 等数据校验。
4. 类型标注
为几个函数补全参数类型、返回值类型,至少包含一个 Optional[T]。
5. mypy
安装并运行 mypy。故意制造至少 3 个类型错误,然后逐个修复。
重点实验:dataclass 可变默认值
先尝试:
@dataclass
class Item:
tags: list[str] = []
观察错误,然后修复:
@dataclass
class Item:
tags: list[str] = field(default_factory=list)
思考:dataclass 和 Day 1 的普通函数默认参数为什么会出现类似问题?最终理解:可变对象默认值需要在实例创建时生成,而不是共享一个对象。
验收 15min
回答:
@dataclass自动生成了哪些常用方法?list[str]为什么不能直接作为 dataclass 默认值?正确写法是什么?Optional[int]是什么意思?- 类型标注会不会自动阻止错误运行?
- mypy 在什么阶段发挥作用?
Day 5|L2 重构(一):目录拆分 + logging + 配置
时间:1.5–2h
这一日开始正式进入工程化。前四天主要学习语言和工具,从 Day 5 开始,把这些能力真正用于 Week 1 项目。
学习 45min
阅读:logging 官方文档、Logging HOWTO。
掌握:Logger、Handler、Formatter、日志级别、logging.getLogger(__name__)、basicConfig。
同时学习基本分层思想:storage → core → cli,以及 config 负责配置。
目标目录
创建:
project/
├── app/
│ ├── __init__.py
│ ├── core/
│ ├── storage/
│ ├── cli.py
│ └── config.py
├── tests/
└── config.json
实验 45min
1. 目录拆分
将 Week 1 项目按照职责拆分。例如:
- storage:负责数据保存、读取
- core:负责业务逻辑
- cli:负责用户交互 / 程序入口
- config:负责配置
2. 配置与代码分离
创建 config.json,使用 Day 4 的 dataclass AppConfig,实现 load_config(),完成 JSON → dict → AppConfig。
3. logging
将项目中的 print(...) 逐渐替换成 logger.info(...)、logger.warning(...)、logger.error(...)。使用 logging.getLogger(__name__),统一日志格式:时间、模块、日志级别、消息。
4. 测试
保持原有测试能够运行,并新增一个 storage 测试,验证目录重构没有改变原有行为。
重点实验:basicConfig
分别在 storage、cli 调用 logging.basicConfig(...),观察行为。最终总结:basicConfig() 应该由应用入口统一配置,而不是由底层库模块随意配置。
验收 15min
回答:
- 四层结构分别负责什么?
- 为什么推荐
getLogger(__name__)? - 哪些东西应该放配置文件?
- 哪些东西不应该放配置文件?
- 什么信号说明"拆对了"?什么信号说明"越拆越乱"?
Day 6|L2 重构(二):类型检查 + 异常整合
时间:1.5–2h
学习 45min
阅读:mypy Getting Started、mypy cheat sheet、pytest.raises。
掌握:类型检查、Optional 未判空、返回类型错误、Any 扩散、分层异常、异常翻译、异常测试。
实验 45min
继续 Day 5 的项目。
1. 完整类型标注
给 storage、core 中的公开函数增加完整类型标注。运行 mypy ...,目标是通过。记录:一共发现多少错误、修复了多少错误、其中有哪些是真实问题。
2. 建立异常体系
创建 app/exceptions.py,结构:
AppError
├── StorageError
├── ConfigError
└── ValidationError
3. Storage 层异常翻译
def load_records(path: str) -> list[Record]:
try:
...
except (OSError, json.JSONDecodeError) as e:
raise StorageError(f"无法读取 {path}") from e
理解:底层异常(OSError / JSONDecodeError)→ StorageError,上层业务只需要理解 StorageError。
4. CLI 统一处理
CLI 层:捕获 AppError → 输出用户友好的信息 → 设置正确退出码。
Core 层:尽量不充满 try/except。
5. 异常测试
使用 pytest.raises(...),至少增加 2 个异常路径测试。
6. pyproject.toml
统一配置 pytest、mypy,让项目工具配置集中管理。
验收 15min
回答:
- 为什么 core 层不应该写满 try/except?
- 为什么要在 storage 层翻译异常?
raise ... from e有什么价值?- mypy 帮你发现了哪些真实问题?
- 每一层应该"抛什么、捕什么"?
最终能够用一句话描述 storage、core、cli 的异常责任。
Day 7|L3 插件式架构设计 + 周复盘
时间:1.5–2h
这一日不追求大量代码,核心目标:开始从"写代码"进入"设计代码"。
学习 45min
阅读:abc 官方文档、ABC、@abstractmethod、typing.Protocol、PEP 544、开闭原则 OCP 短文。
掌握抽象:抽象基类可以定义一个实现必须遵守的契约。
接口与实现,例如:
Reader
├── CSVReader
├── JSONReader
└── ExcelReader
Processor 不应该关心你到底是 CSV、JSON 还是 Excel,它只需要知道 Reader 接口。
依赖注入:Processor(reader) 而不是 Processor() 内部写死 CSVReader(),这样可以降低耦合。
实验 45min
产出:architecture.md、readers.py。
1. 定义 Reader 接口
class Reader(ABC):
@abstractmethod
def read(self, path: str) -> list[Record]:
...
@abstractmethod
def can_handle(self, path: str) -> bool:
...
2. 三种 Reader
创建 CSVReader、JSONReader、ExcelReader,暂时只写空壳,方法内部 raise NotImplementedError。验证:Reader 不能直接实例化(Reader() 应该报错),子类漏实现抽象方法也应该无法实例化。
3. Processor
定义 Processor(reader),让 Processor 通过构造函数接收 Reader。
class Processor:
def __init__(self, reader):
self.reader = reader
def process(self, path):
records = self.reader.read(path)
...
Processor 不关心具体 Reader。
4. 绘制结构图
CSVReader ─┐
JSONReader ├──→ Reader 接口 ──→ Processor ──→ 输出
ExcelReader┘
核心思想:具体实现 → 统一接口 → 业务逻辑。
5. architecture.md
回答:
- 问题一:如果新增
XMLReader,完整步骤是什么?明确列出哪些文件需要修改,以及哪些文件完全不需要修改? - 问题二:为什么
read() -> list[Record]是一个关键设计? - 问题三:异常约定——Reader 抛什么?Processor 捕不捕?写出你的设计。
- 问题四:如果
ExcelReader需要 sheet 参数,参数应该怎么设计?比较构造参数、read() 参数、可选配置对象,最终选择一种,并写出理由。
重点设计题
思考:如果把 read() 的返回值从统一的 list[Record] 改成各 Reader 返回各自格式的 dict,会发生什么?
分析:
Reader 返回不同结构
↓
Processor 必须认识各种结构
↓
Processor 出现大量 if / isinstance
↓
新增 Reader 需要修改 Processor
↓
系统耦合增加
最终回答:这说明统一接口和统一数据模型有什么价值?
Day 7 验收
不用资料回答:
- 为什么新增 Reader 不需要修改 Processor?
- ABC 和 Protocol 的核心区别是什么?
- 你的 Reader 接口最难设计的地方是什么?
- 为什么
list[Record]比"各自返回 dict"更重要? - 什么是依赖注入?
- 本周最有收获的概念是什么?
- 本周项目最大的改进是什么?
- 什么问题留到 Week 3?
三、Week 2 最终项目结构
如果本周全部完成,项目最终大致应该形成:
project/
├── app/
│ ├── __init__.py
│ ├── core/
│ │ ├── __init__.py
│ │ └── ...
│ ├── storage/
│ │ ├── __init__.py
│ │ └── ...
│ ├── cli.py
│ ├── config.py
│ └── exceptions.py
├── tests/
│ └── ...
├── architecture.md
├── config.json
├── pyproject.toml
└── ...
并具备:模块化 → 类型标注 → mypy → 异常体系 → logging → 配置分离 → 测试 → 接口抽象 → 插件式架构雏形。
四、本周能力目标
L1|语言能力
能够独立理解和使用:函数参数、*args、**kwargs、作用域、闭包、import、module、package、exception、dataclass、type hints。
L2|工程能力
能够:合理拆分目录、管理模块依赖、使用 logging、分离配置、建立异常层级、使用 mypy、编写异常测试、使用 pytest、对已有项目进行重构。
L3|设计能力
开始理解:抽象、接口、依赖注入、开闭原则、插件式架构、数据模型统一、降低模块耦合。
五、本周最终验收标准
本周不是以"看完教程"为完成标准。真正的完成标准是:
Python 语言层
- 能解释函数参数绑定
- 能正确使用
*args/**kwargs - 能解释可变默认参数陷阱
- 能解释 LEGB
- 能使用
global/nonlocal - 能写闭包
- 能把函数作为参数传递
模块层
- 理解 module / package
- 理解 import 机制
- 理解
sys.modules - 理解
sys.path - 理解
__main__ - 能定位循环导入问题
异常层
- 能正确使用 try / except / else / finally
- 能使用 raise
- 能使用
raise ... from ... - 能设计简单异常层级
- 理解 EAFP / LBYL
数据模型层
- 能使用 dataclass
- 能使用
default_factory - 能写类型标注
- 能使用 Optional / Union
- 能运行 mypy
工程层
- 项目能够模块化拆分
- 使用 logging
- 配置与代码分离
- 建立统一异常体系
- 类型检查通过
- 异常测试通过
架构层
- 能定义简单抽象接口
- 理解 ABC
- 知道 Protocol 的基本思想
- 理解依赖注入
- 能解释插件式架构为什么降低耦合
六、本周产出清单
function_demo.pymypkg/exception_demo.pytyped_demo.py- Week 1 项目完成目录拆分
- logging 接入
- 配置文件接入
exceptions.py- mypy 检查通过
- 异常测试
pyproject.tomlarchitecture.md- Reader 接口设计
- CSV / JSON / Excel Reader 骨架
七、Week 2 的核心认知变化
Week 1 主要解决:"Python 中的数据是什么?数据结构怎么工作?"
Week 2 开始解决:"代码应该如何组织?"
进一步:
Week 1
数据结构
↓
函数
↓
模块
↓
异常
↓
数据模型
↓
工程重构
↓
抽象接口
↓
插件式架构
最终希望形成一个重要的工程意识:好的代码不只是"能运行",还应该容易理解、容易修改、容易测试,并且能够在需求变化时控制修改范围。
Week 2 的最终目标,就是从"我能写一个功能"逐渐过渡到"我能把功能组织成一个可维护的小型 Python 项目"。
来源与延伸阅读
本周涉及的主要官方文档与规范: