Week 2 学习成果|模块机制、异常体系与工程化重构
Week 2 的实际产出:Day 2 模块与 import 机制、Day 3 异常处理的完整总结与实验记录。
本篇是 Week 2 的学习成果,与 学习计划 分开成文:计划篇保留当时的任务与要求,这里放实际做出来的东西和实验记录。目前有书面成果的是 Day 2(模块与 import 机制)和 Day 3(异常处理),Day 4–7 完成后再补进来。
成果:Day 2 模块与包 | Day 3 异常处理
Day 2 成果|模块与包:import 机制
对应计划:Day 2|模块与包:import 机制
模块 / 包 / 子包
| 概念 | 对应什么 | 例子 |
|---|---|---|
| 模块 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
mypkg 是顶层包,mypkg.core 是模块,mypkg.utils 是子包,mypkg.utils.text 是子包里的模块。
一句话:目录 = 包 / 子包;.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() # 当前命名空间绑定 core
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__:
- 直接运行文件:
__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 |
|---|---|---|
__name__ | "__main__" | "__main__" |
sys.path[0] | mypkg/ | 当前工作目录 |
| 包上下文 | 无 | 有 |
| 相对导入 | 失败 | 成功 |
| 适合 | 独立脚本 | 包内模块 |
相对导入实验:
from .utils import text
python mypkg/core.py→ 报错attempted relative import with no known parent packagepython -m mypkg.core→ 正常
原因:python file.py 把文件当独立脚本,没有父包;python -m pkg.mod 把模块放在包上下文里运行,有父包。
__init__.py 与 re-export
__init__.py 的作用:
- 导入包时执行初始化代码
- 准备包对象上要暴露的名字
- re-export,让外部可以直接
from mypkg import hello - 定义
__all__,控制from mypkg import * - 组织公开接口,隐藏内部结构
# mypkg/__init__.py
from .core import hello
# 外部
from mypkg import hello
hello()
好处:对外隐藏内部结构,重构内部文件时不影响外部调用。
import 查找顺序与缓存
导入时的顺序:
sys.modules缓存 —— 先看这个模块之前有没有被导入过,有就直接用,不再重新执行。sys.path—— 缓存里没有,就按sys.path里的目录顺序查找。
sys.path 通常包括:当前目录或脚本目录、PYTHONPATH 里的目录、Python 标准库目录、site-packages 第三方库目录。
结论:第一次导入会执行模块顶层代码并放入 sys.modules;第二次导入直接复用缓存,不再执行。
循环导入
成因:两个模块在顶层互相 import,其中一个还没执行完,另一个就试图使用它还没定义好的名字。
# a.py
import b
def a_func():
print("a_func")
# b.py
import a
def b_func():
print("b_func")
a.a_func() # 此时 a 可能还没定义完 a_func
常见解法:
- 把
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
解释:
import counter后,counter.count是 0。from counter import count把counter.count当时指向的整数0绑定到main.py的count。counter.inc()修改的是counter模块自己的全局变量count,使其变成 1。main.py里的count仍指向旧整数0。
结论:from x import y 绑定的是导入那一刻 y 指向的对象,不是变量名本身。整数不可变,重新赋值不会影响已绑定的名字。
验收四题
- import 同一模块两次,模块代码执行几次?为什么? 只执行一次。第一次执行后放入
sys.modules缓存,第二次直接复用。 __init__.py能做什么? 执行包初始化、re-export、定义__all__、组织公开接口。python file.py和python -m pkg.mod的区别? 前者当独立脚本运行,无包上下文,相对导入失败;后者当包内模块运行,有包上下文,相对导入成功。- 循环导入为什么发生?最常见的解法是什么? 两个模块顶层互相导入,一方还没执行完另一方就使用它。解法:延迟导入到函数内,或抽公共模块解耦。
Day 2 一句话总结:模块是 .py 文件,包是含 __init__.py 的目录。import 先查 sys.modules 缓存,再按 sys.path 查找。from x import y 绑定的是对象,不是变量名。直接运行用 main 守卫;包内模块用 python -m 跑,相对导入才有效。循环导入靠延迟导入或解耦解决。
Day 3 成果|异常处理
对应计划:Day 3|异常处理:健壮性基础
基本结构:try / except / else / finally
try:
... # 可能出错的代码
except ValueError:
... # 捕获并处理 ValueError
else:
... # try 中没有异常时执行
finally:
... # 无论有没有异常,最后都执行
| 情况 | 执行顺序 |
|---|---|
| try 成功 | try → else → finally |
| try 失败并被捕获 | try → except → finally |
| try 失败但没被捕获 | try → finally,异常继续往上抛 |
关键点:
else:只有 try 没抛异常时才执行。finally:无论成功、失败、return,都会执行。finally里的return会覆盖 try 里的return,所以不要在finally里随便写return。
except 匹配规则
except从上往下匹配,第一个匹配上的就执行。- 父类可以捕获子类,所以子类异常要写在父类异常前面。
BaseException
├── KeyboardInterrupt
├── SystemExit
├── GeneratorExit
└── Exception
├── ValueError
├── TypeError
├── KeyError
├── OSError
└── ...
裸 except: vs except Exception:
| 写法 | 捕获范围 | 危险点 |
|---|---|---|
except Exception: | Exception 及其子类 | 不抓 KeyboardInterrupt、SystemExit |
裸 except: | 所有 BaseException 子类 | 会抓 Ctrl+C、sys.exit(),程序可能停不下来 |
结论:裸 except: 更危险,因为它连退出信号都吞。
raise 与异常链
raise ValueError("值不对") # 主动抛异常
except ValueError:
...
raise # 重新抛出当前异常
try:
int("abc")
except ValueError as e:
raise RuntimeError("配置解析失败") from e
traceback 会出现:
The above exception was the direct cause of the following exception:
含义:对外暴露 RuntimeError("配置解析失败"),对内保留 ValueError 作为直接原因。
一句话:raise ... from e = 上层业务异常包装 + 保留底层技术根因。
自定义异常
class AppError(Exception):
pass
class ConfigError(AppError):
pass
class DataError(AppError):
pass
Exception
└── AppError
├── ConfigError
└── DataError
作用:按业务语义区分错误类型、让调用方精准捕获、可以附加额外信息。
值得自定义的场景:需要区分「配置错误 / 数据错误 / 网络错误」等业务类型;需要让上层只捕获 AppError;需要给异常附加字段,比如 key、path。
不值得的场景:
class MyError(ValueError):
pass
只换名字、不加信息、不做层级区分,就不值得。
EAFP vs LBYL
LBYL(先检查,再操作):
if key in config:
return config[key]
else:
return "default"
EAFP(先操作,出错再处理):
try:
return config[key]
except KeyError:
return "default"
EAFP 好处:不用自己写复杂预检查;代码更短、更直接;避免检查和操作之间状态变化。
EAFP 风险:try 范围太大,出错难定位;except 太宽,容易吞掉不该吞的异常;捕获后不处理,程序带伤继续跑。
一句话:EAFP 省去预检查,靠异常处理;但 try 要小,except 要具体。
反模式
裸 except::
try:
do_something()
except:
pass
危险:会吞掉 KeyboardInterrupt、SystemExit。
吞异常不记录:
try:
do_something()
except Exception:
pass
危险:错误被隐藏,调试无线索。
正确做法:
except Exception:
logging.exception("出错了")
raise
或者只捕获具体异常,并明确处理。
今天完成的实验
实验 1:parse_int(s)
处理了 int("abc") → ValueError、int(None) → TypeError;成功时走 else 并返回结果,出错时打印提示,隐式返回 None。
实验 2:try / else / finally
观察到成功时 try → else → finally,失败时 try → except → finally。
实验 3:自定义异常层级
class AppError(Exception): pass
class ConfigError(AppError): pass
class DataError(AppError): pass
验证了 except AppError 能捕获 ConfigError。
实验 4:LBYL 改 EAFP
def get_config_EAFP(config, key):
try:
return config[key]
except KeyError:
return "default"
重点实验:raise ... from ...
观察到两个异常同时出现在 traceback 中,以及:
The above exception was the direct cause of the following exception:
附加实验:finally 里的 return
def f():
try:
return "try"
finally:
return "finally"
最终返回 "finally",说明 finally 里的 return 会覆盖 try 里的 return。
验收四题
except Exception和裸except:的区别?except Exception只抓普通异常;裸except:连KeyboardInterrupt、SystemExit都抓,更危险。else什么时候执行?finally呢?else在 try 无异常时执行;finally无论有没有异常都执行。- 什么时候值得自定义异常类? 需要按业务语义区分错误、精准捕获、附加额外信息时。
- EAFP 是什么?相比 LBYL 的好处和风险? EAFP 是先做,出错再处理。好处是省去复杂预检查;风险是 try 和 except 范围失控会吞错。
Day 3 一句话总结:try 放可能出错的代码,except 处理已知错误,else 走成功分支,finally 做兜底清理。异常要么明确处理,要么记录后 raise,不要吞。需要分层时用 raise ... from e。优先 EAFP,但 try 要小,except 要具体。