Week 2 学习成果|闭包、decorator 与工程化重构
Week 2 的实际产出:Day 1 参数组合与闭包、Day 2 自定义 module 与 import 机制、Day 3 闭包与作用域深化、Day 4 decorator 简化版、Day 5–6 项目重构(目录拆分、logging、dataclass、type hints)的完整总结与实验记录。
本篇对应 Week 2 学习计划,与一年学习计划总纲 Phase I 的 Week 2 · 函数、作用域、闭包与模块化(等级 L2)一致,按总纲 Part II「每周固定执行模板」编排。计划篇保留任务与要求,这里放实际做出来的东西和实验记录。
🎯 本周目标
L1
- ✅ 已掌握函数参数、作用域、闭包、lambda、import / module / package
L2
- 🟡 已完成参数组合、闭包、自定义 module;decorator 简化版整理中;项目重构(目录拆分、logging、dataclass、type hints)进行中
L3
- 总纲本周不设 L3
📚 Core Knowledge
- 函数进阶:positional / keyword / default 参数、
*args/**kwargs、可变默认参数陷阱、lambda - 作用域与闭包:LEGB、
global/nonlocal、闭包捕获定义时的环境 - decorator:接受函数、返回新函数的高阶函数,本质依赖闭包
- 模块与包:module / package / subpackage、三种 import、
sys.modules缓存、sys.path查找、python -m、循环导入 - 工程化:分层(storage → core → cli → config)、dataclass、type hints、logging
✍️ Human Core
- 手写:参数组合(positional / keyword / default)
- 手写:闭包(
make_counter、outer/inner) - 手写:decorator 简化版
- 手写:自定义 module(
mypkg/)
🤖 AI Build
- 将 Week 1 项目重构为
core//storage//cli//config/ - 加入 dataclass
- 加入 type hints
- 加入 logging
🧪 Experiment
Hypothesis
- 通过目录拆分、dataclass、type hints 与 logging,Week 1 项目能在不改变对外行为的前提下变得可维护、可测试。
Experiment
- 将 Week 1 的 KV 项目按职责拆为 storage / core / cli / config 四层,接入 logging 与
config.json,补充 dataclass 与类型标注,并用 pytest 验证行为不变。
Result
- 重构后行为不变,测试通过;
basicConfig实验确认「谁先被 import,谁就决定全局日志格式」——详见 Day 5 实验 A / B。
Conclusion
- 验证了假设:重构在不改变行为的前提下提升了可维护性;
basicConfig必须只在入口调用一次。
📝 数智工程师考试映射
- Python 函数、作用域、闭包与 decorator
- Python 模块与包机制(import、
sys.path、sys.modules) - 代码组织、分层与工程化重构
✅ Acceptance
总纲 Week 2 验收:
L1
- 能独立解释:
x = outer()
x()
为什么还能访问 outer() 中定义的变量。
L2
- 手写参数组合、闭包、decorator 简化版、自定义 module
- 将 Week 1 项目重构为
core//storage//cli//config/,加入 dataclass、type hints、logging
L3
- 总纲本周不设 L3
🧠 本周最重要的问题
为什么
x = outer(); x()还能访问outer()中定义的变量?
因为闭包会把“定义时的环境”和函数对象绑在一起:inner 被返回后,它仍持有对 outer 局部作用域(enclosing)的引用,x() 时按 LEGB 先查到 E 层的 n。只要这个函数对象还活着,被它引用的外层变量就不会被回收。
❓ 尚未解决
- 业务代码手写熟练度不足,需要不看参考从零重写
KVService与cli.py
📊 Final Level
L2
Day 1 成果|参数组合与闭包
对应计划:Day 1|函数进阶:参数、作用域、闭包
参数组合
| 写法 | 说明 |
|---|---|
| positional | f(1, 2) 按位置传 |
| keyword | f(a=1, b=2) 按名字传 |
| default | def f(a, b=10) 可省略 |
可变默认参数陷阱:
def append_to(item, target=[]):
target.append(item)
return target
print(append_to(1)) # [1]
print(append_to(2)) # [1, 2] ← 不是 [2],默认 list 被共享了
print(append_to(3, [])) # [3] ← 显式传新 list 才隔离
原因:默认参数只在函数定义时求值一次,target=[] 这个 list 被所有调用共享。修复:target=None,函数内再 if target is None: target = []。
闭包计数器
def make_counter():
n = 0
def counter():
nonlocal n
n += 1
return n
return counter
counter = make_counter()
print(counter()) # 1
print(counter()) # 2
print(counter()) # 3
a = make_counter()
b = make_counter()
print(a(), a()) # 1 2
print(b()) # 1 ← 两个计数器相互独立
关键理解:counter 捕获的是 make_counter 的局部变量 n(enclosing 层),而不是它当时的值。每次调用 make_counter() 都会新建一个独立的 n,所以 a、b 互不影响。nonlocal 让 counter 内部可以修改外层的 n。
验收
- 默认参数定义时求值一次 → 可变默认值被共享
*args收集位置参数成 tuple;**kwargs收集关键字参数成 dict- 闭包捕获的是变量(环境),不是值(所以要用
nonlocal修改) - “函数是对象”意味着:可赋值、可当参数传、可当返回值
Day 2 成果|自定义 module 与 import 机制
对应计划:Day 2|自定义 module 与包
模块 / 包 / 子包
| 概念 | 对应什么 | 例子 |
|---|---|---|
| 模块 module | 一个 .py 文件 | core.py → 模块 mypkg.core |
| 包 package | 一个目录,含 __init__.py | mypkg/ → 包 mypkg |
| 子包 subpackage | 包里面的包 | mypkg/utils/ → 子包 mypkg.utils |
实验用的结构:
mypkg/
├── __init__.py
├── core.py
└── utils/
├── __init__.py
└── text.py
一句话:目录 = 包 / 子包;.py 文件 = 模块;点号表示层级。
三种 import 写法
# 1. import x —— 绑定包名
import mypkg.core
mypkg.core.hello() # 当前命名空间只绑定 mypkg,必须写全 mypkg.core
# 2. from x import y —— 绑定里面的名字
from mypkg import core
core.hello()
from mypkg.core import hello
hello()
# 3. from x import y as z —— 重命名
from mypkg.core import hello as hi
hi()
| 写法 | 当前绑定谁 | 调用方式 |
|---|---|---|
import mypkg.core | mypkg | mypkg.core.hello() |
from mypkg import core | core | core.hello() |
from mypkg.core import hello | hello | hello() |
from mypkg.core import hello as hi | hi | hi() |
__name__ 与 main 守卫
- 直接运行文件:
__name__ == "__main__" - 被导入时:
__name__是模块导入路径,如"mypkg.core"
if __name__ == "__main__":
hello()
| 运行方式 | core.py 里的 __name__ |
|---|---|
python mypkg/core.py | "__main__" |
python -m mypkg.core | "__main__" |
import mypkg.core | "mypkg.core" |
python file.py vs python -m pkg.mod
| 对比项 | python mypkg/core.py | python -m mypkg.core |
|---|---|---|
sys.path[0] | mypkg/ | 当前工作目录 |
| 包上下文 | 无 | 有 |
| 相对导入 | 失败 | 成功 |
| 适合 | 独立脚本 | 包内模块 |
相对导入 from .utils import text:python mypkg/core.py 报 attempted relative import with no known parent package;python -m mypkg.core 正常。
__init__.py 与 re-export
# mypkg/__init__.py
from .core import hello
# 外部
from mypkg import hello
hello()
好处:对外隐藏内部结构,重构内部文件时不影响外部调用。
import 查找顺序与缓存
sys.modules缓存 —— 先看这个模块之前有没有被导入过,有就直接用,不再重新执行。sys.path—— 缓存里没有,就按sys.path里的目录顺序查找。
结论:第一次导入会执行模块顶层代码并放入 sys.modules;第二次导入直接复用缓存,不再执行。
循环导入
成因:两个模块在顶层互相 import,其中一个还没执行完,另一个就试图使用它还没定义好的名字。解法:把 import 移到函数内部(延迟导入),或抽公共模块解耦。
重点实验:from x import y 到底绑定了什么?
# counter.py
count = 0
def inc():
global count
count += 1
# main.py
import counter
from counter import count
counter.inc()
print(counter.count, count)
输出 1 0。from counter import count 绑定的是导入那一刻 count 指向的整数 0,不是变量名本身;counter.inc() 改的是 counter 模块自己的全局变量。
验收四题
- import 同一模块两次,模块代码执行几次? 只执行一次,第二次复用
sys.modules缓存。 __init__.py能做什么? 执行包初始化、re-export、定义__all__、组织公开接口。python file.py和python -m pkg.mod的区别? 前者当独立脚本,无包上下文;后者当包内模块,有包上下文,相对导入成功。- 循环导入为什么发生?怎么解? 顶层互相导入,一方没执行完另一方就使用它。解法:延迟导入到函数内,或抽公共模块解耦。
Day 3 成果|闭包与作用域深化
对应计划:Day 3|闭包与作用域深化
总纲 Week 2 的验收就落在这一条:
x = outer()
x()
为什么 x() 还能访问 outer() 中定义的变量?
def outer():
n = 0
def inner():
nonlocal n
n += 1
return n
return inner
x = outer()
print(x()) # 1
print(x()) # 2
outer() 返回的是 inner 这个函数对象,它带着 outer 的作用域环境一起被返回(闭包)。x() 执行时按 LEGB 找 n:L 层没有 → E 层(outer 的局部作用域)找到 n。只要 x 还引用着 inner,n 就不会被回收。nonlocal n 让 inner 能修改这个外层变量,而不只是读取。
LEGB 查找顺序
| 层 | 含义 |
|---|---|
| L Local | 当前函数内部 |
| E Enclosing | 外层嵌套函数 |
| G Global | 模块顶层 |
| B Built-in | 内置(print、len…) |
从 L 往外逐层找,找到即停。
global / nonlocal
| 关键字 | 声明效果 |
|---|---|
global x | x 属于全局 → 函数内赋值改的是全局变量 |
nonlocal x | x 属于外层函数 → 函数内赋值改的是外层变量 |
不加声明却赋值,又想读写外层/全局变量 → 报 UnboundLocalError(赋值让名字变局部,局部还没赋值就被读取)。
Day 4 成果|decorator 简化版
对应计划:Day 4|decorator 简化版
def log(func):
def wrapper(*args, **kwargs):
print(f"call {func.__name__} args={args} kwargs={kwargs}")
return func(*args, **kwargs)
return wrapper
@log
def add(a, b):
return a + b
print(add(1, 2))
输出:
call add args=(1, 2) kwargs={}
3
关键理解:
@log等价于add = log(add):把原函数传进log,拿回被包一层的新函数。wrapper用*args/**kwargs透传任意参数,这样装饰器对任何签名都通用。- 装饰器本质是闭包:
wrapper记住了外层的func,调用时先执行附加逻辑再执行原函数。 - 简化版故意没用
functools.wraps,所以add.__name__会变成"wrapper"——这正是“简化”的代价,正式版需要wraps保留元信息。
Day 5 成果|L2 重构:目录拆分与 logging
对应计划:Day 5|L2 重构(一):目录拆分 + logging
目录结构
kv_project/
├── app/
│ ├── __init__.py
│ ├── storage/
│ │ ├── __init__.py
│ │ ├── list_store.py # ListKeyValueStore
│ │ └── dict_store.py # DictKeyValueStore
│ ├── core/
│ │ ├── __init__.py
│ │ └── kv_service.py # KVService
│ ├── cli.py # main(),程序入口
│ └── config.py # AppConfig + load_config()
├── tests/
│ ├── __init__.py
│ ├── test_list_store.py
│ └── test_dict_store.py
├── config.json
└── .venv/
四层职责
| 层 | 文件 | 职责 | 例子 |
|---|---|---|---|
| storage | list_store.py / dict_store.py | 只管数据怎么存、怎么取 | set / get / delete / exists / keys |
| core | kv_service.py | 业务逻辑,调 storage | 容量限制:满了拒绝 |
| cli | cli.py | 用户入口,解析命令,组装一切 | set name alice |
| config | config.py | 读 JSON → AppConfig | capacity=100 |
依赖方向:
cli → core → storage
config ← 谁需要谁读(实际只有 cli 读)
关键点:core 不读配置(配置值由 cli 读好后传进去);core 不知道底层是 list 还是 dict,只调 store.set / store.get——这叫依赖注入。
配置分离
config.json 只放值:
{
"storage_backend": "dict",
"capacity": 100,
"log_level": "INFO"
}
config.py 放“怎么读”:
import json
from dataclasses import dataclass
@dataclass
class AppConfig:
storage_backend: str = "dict"
capacity: int = 100
log_level: str = "INFO"
def load_config(path: str = "config.json") -> AppConfig:
with open(path, encoding="utf-8") as f:
data = json.load(f)
return AppConfig(**data)
转换链路:config.json → json.load() → dict → AppConfig(**data) → AppConfig 实例。
logging
# 1. 每个模块顶部
import logging
logger = logging.getLogger(__name__)
# 2. 打日志
logger.debug("细节")
logger.info("正常流程")
logger.warning("警告")
logger.error("出错")
# 3. 入口只配一次
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(name)s %(levelname)s %(message)s",
)
日志级别:DEBUG < INFO < WARNING < ERROR < CRITICAL。
basicConfig 铁律:basicConfig() 只能由应用入口调一次。底层模块只 getLogger,绝不 basicConfig。
实验 A:在 dict_store.py 里也加 basicConfig(STORAGE: ...)
结果格式变成 STORAGE: ...、级别变成 DEBUG,连 cli.py 的日志也被改成 STORAGE: 格式。原因:python -m app.cli 先从 cli.py 顶部 import app.storage.dict_store,dict_store.py 的 basicConfig 抢先生效,main() 里的 basicConfig 被静默忽略。
实验 B:删掉 storage 里的 basicConfig —— 恢复成 cli.py 配的格式。
结论:谁先被 import,谁就决定了全局日志格式和级别;入口想统一配置,就会被架空。
测试
uv run pytest -v
结果:10 passed in 0.02s。重构没有改变原有行为。
Day 6 成果|L2 重构:dataclass 与 type hints
对应计划:Day 6|L2 重构(二):dataclass 与 type hints
@dataclass 是什么
标准库 dataclasses 里的装饰器,自动为“数据类”生成样板方法。
from dataclasses import dataclass, field
@dataclass
class Record:
id: int
name: str
tags: list[str] = field(default_factory=list)
created_at: str = ""
| 方法 | 何时生成 |
|---|---|
__init__ | 默认 |
__repr__ | 默认 |
__eq__ | 默认 |
__setattr__ / __delattr__ | 仅 frozen=True 时 |
原理:@dataclass 读类里的类型标注(存在 __annotations__),据此生成 __init__ 等方法。
默认值规则
| 类型 | 写法 |
|---|---|
| 不可变(int / str / bool) | count: int = 0 |
| 可变(list / dict / set) | tags: list[str] = field(default_factory=list) |
原因:可变默认值会被所有实例共享,和 Day 1 的函数默认参数是同一个坑。
frozen=True 与 __post_init__
@dataclass(frozen=True)
class Item:
name: str
- 实例创建后字段不可改,赋值抛
FrozenInstanceError __post_init__在__init__执行完后自动调用,用来做数据校验:
def __post_init__(self):
if self.price < 0:
raise ValueError("price 不能为负数")
类型标注
运行时不做检查,只是“说明”,给编辑器、mypy、AI、未来的你看。
| 写法 | 意思 |
|---|---|
int / str / bool | 基本类型 |
list[int] | 整数列表 |
dict[str, int] | 键 str、值 int |
Optional[int] | int 或 None |
Union[int, str] | int 或 str(3.10+ 可写 int | str) |
Callable[[int], str] | 接收 int、返回 str 的函数 |
用在哪:
x: Optional[int] = None # 变量
def f(a: int, b: str) -> bool: ... # 参数 + 返回值
def g() -> Optional[Record]: ... # 返回值可能是 None
name: Optional[str] = None # dataclass 字段
mypy
静态类型检查工具,运行前检查,不执行代码。能查参数 / 返回值类型不对、Optional 没判空就用、访问不存在的属性等;不能查具体值是否合理、业务逻辑错误。
本周为 storage、core 的公开函数补全类型标注后运行 mypy 作为重构的验证手段。
来源与延伸阅读
- 01Week 1 学习计划|Python 对象模型与数据结构
- 02一年学习计划:从 Python 到 Data/AI 系统的 52 周
- 03Week 1 学习成果|复杂度实测、对象模型与数据结构实战
- 04Week 2 学习计划|Python 函数、作用域、闭包与模块化
- 05Week 2 学习成果|闭包、decorator 与工程化重构
- 06Week 3 学习计划|异常、测试与 Debugging
- 07Week 3 学习成果|异常、traceback 与 Debug 起步
- 08Week 4 学习成果|Big-O 与四类基础算法
- 09Week 5 学习成果|Stack、Queue 与 MinHeap
- 10Week 6 学习成果|图算法与 Python Engineering Toolkit