文章
学习
从 Python 到 Data/AI 一年计划计划第 4 / 5 篇 对应成果

Week 2 学习计划|Python 函数、抽象与模块化

Week 2 从函数进阶出发,逐步学习模块、异常、数据建模、类型检查与工程化重构,最终完成一个具备模块化、日志、配置、类型检查、异常体系和插件式架构雏形的 Python 项目。

PythonWeek2函数模块异常dataclass类型标注重构模块化架构

Week 2 核心主题:函数、抽象与模块化

本周目标不是单纯继续学习 Python 语法,而是开始从:

「会写 Python 代码」→「能够组织 Python 代码」→「能够设计一个小型项目」

逐步过渡。本周会把 Week 1 的简单数据结构与 Key-Value Store 项目继续向工程化方向推进。

一、Week 2 总览

Day主题对应目标
Day 1函数进阶:参数、作用域、闭包L1
Day 2模块与包:import 机制L1
Day 3异常处理L1
Day 4dataclass 与 type hintsL1
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:ModulesPython Tutorial:Packagessys.pathsys.modules

掌握:

  • module / package / subpackage
  • import xfrom x import yfrom 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.corefrom mypkg import corefrom 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 bb.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.pypython -m pkg.mod 有什么区别?
  • 循环导入为什么发生?
  • 最常见的解决思路是什么?

Day 3|异常处理:健壮性基础

时间:1.5–2h

学习 45min

阅读:Python Tutorial:Errors and Exceptions内置异常层级raise 文档

重点掌握:try / except / else / finallyraise、异常链 raise ... from ...、自定义异常、EAFP、LBYL。

重点认识异常层级:

Exception
├── ValueError
├── TypeError
├── OSError
└── ...

以及:异常不是单纯"报错",而是一种控制异常情况的机制。

实验 45min

创建 exception_demo.py,完成:

1. parse_int

实现 parse_int(s),分别处理 ValueErrorTypeErrorNone

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@dataclassfielddefault_factoryfrozen=True

类型标注:变量标注、参数标注、返回值标注、OptionalUnionlist[T]dict[K, V]CallableAny

理解:mypy 是静态类型检查工具,不负责在运行时执行类型检查。

实验 45min

创建 typed_demo.py

1. Record

@dataclass
class Record:
    ...

字段:idnametagscreated_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 Startedmypy cheat sheetpytest.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@abstractmethodtyping.ProtocolPEP 544、开闭原则 OCP 短文。

掌握抽象:抽象基类可以定义一个实现必须遵守的契约。

接口与实现,例如:

Reader
├── CSVReader
├── JSONReader
└── ExcelReader

Processor 不应该关心你到底是 CSV、JSON 还是 Excel,它只需要知道 Reader 接口。

依赖注入:Processor(reader) 而不是 Processor() 内部写死 CSVReader(),这样可以降低耦合。

实验 45min

产出:architecture.mdreaders.py

1. 定义 Reader 接口

class Reader(ABC):
    @abstractmethod
    def read(self, path: str) -> list[Record]:
        ...

    @abstractmethod
    def can_handle(self, path: str) -> bool:
        ...

2. 三种 Reader

创建 CSVReaderJSONReaderExcelReader,暂时只写空壳,方法内部 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.py
  • mypkg/
  • exception_demo.py
  • typed_demo.py
  • Week 1 项目完成目录拆分
  • logging 接入
  • 配置文件接入
  • exceptions.py
  • mypy 检查通过
  • 异常测试
  • pyproject.toml
  • architecture.md
  • Reader 接口设计
  • CSV / JSON / Excel Reader 骨架

七、Week 2 的核心认知变化

Week 1 主要解决:"Python 中的数据是什么?数据结构怎么工作?"

Week 2 开始解决:"代码应该如何组织?"

进一步:

Week 1
数据结构
  ↓
函数
  ↓
模块
  ↓
异常
  ↓
数据模型
  ↓
工程重构
  ↓
抽象接口
  ↓
插件式架构

最终希望形成一个重要的工程意识:好的代码不只是"能运行",还应该容易理解、容易修改、容易测试,并且能够在需求变化时控制修改范围。

Week 2 的最终目标,就是从"我能写一个功能"逐渐过渡到"我能把功能组织成一个可维护的小型 Python 项目"。

来源与延伸阅读

本周涉及的主要官方文档与规范:

  1. 01Week 1 学习计划|Python 对象模型与数据结构week-1计划
  2. 02学习计划:从 Python开始到认识Data/AI计划
  3. 03Week 1 学习成果|复杂度实测、对象模型与数据结构实战week-1成果
  4. 04Week 2 学习计划|Python 函数、抽象与模块化week-2计划
  5. 05Week 2 学习成果|模块机制、异常体系与工程化重构week-2成果