- 示例工程
- 教程
【免费下载链接】python-mastery
Advanced Python Mastery (course by @dabeaz)
本篇技术指南以 Advanced Python Mastery 课程的 Exercise 1.6 为骨架,系统讲解 Python 中模块的定义方式、import语句的加载语义,以及if __name__ == '__main__'主模块保护这一核心实践。文中所有代码与数据均取自当前仓库的真实文件(Solutions/1_6/pcost.py、Data/portfolio.dat 等),读者完成后将掌握:如何把前几节写好的函数与类封装成可导入的模块、如何在交互式解释器中调用模块功能,以及如何让同一份代码既可作为脚本运行又可作为库被复用。
一、练习目标与前置环境准备
本练习聚焦两个目标:
- 定义模块(Defining modules)
- 使用 import 语句(Using the import statement)
与之前练习不同,本练习不创建新文件(Files Created: None),只修改上一节生成的pcost.py。它的前提是你在 Exercise 1.4 中把pcost.py改造成了带异常处理的portfolio_cost(filename)函数,并在 Exercise 1.5 中创建了stock.py的Stock类。
环境检查:sys.path 与解释器重启
文档在开头特别强调:做模块相关练习时,运行环境是否正确至关重要。如果import语句总是失败,第一件事就是检查sys.path:
>>> import sys >>> sys.path ['', '/usr/local/lib/python3.11', ...]- 列表中的空字符串
''代表当前工作目录,Python 导入模块时会优先从这里查找; - 如果你把
pcost.py、stock.py放在了一个"奇怪"的目录(如桌面下某个深层文件夹),而解释器启动目录不包含它,import就会抛出ModuleNotFoundError; - 解决办法:在存放这些
.py文件的同一目录下启动 Python,或者把该目录追加进sys.path(如sys.path.append('/path/to/your/files'))。
同时,练习要求先重启解释器会话:在 IDLE 中执行 "Shell > Restart Shell",会看到##################== RESTART ##################==提示;在 Unix 环境下直接退出再重启 Python。这样做的意义在于:保证随后的导入行为是"全新加载",避免之前会话残留的命名空间干扰实验结论。
二、第 (a) 部分:用 import 语句加载已有程序
在之前练习中,你已经写出了两个程序pcost.py与stock.py。现在通过import把它们当作模块加载,并直接复用其功能:
>>> import pcost 44671.15 >>> pcost.portfolio_cost('Data/portfolio2.dat') 19908.75 >>> from stock import Stock >>> s = Stock('GOOG', 100, 490.10) >>> s.name 'GOOG' >>> s.cost() 49010.0 >>>逐行解读这段会话,背后是三条核心机制:
import pcost会执行模块的全部顶层代码。当前仓库中 Solutions/1_6/pcost.py 是带__name__保护后的版本,而练习开始时你的pcost.py末尾还留有 Exercise 1.4 要求添加的无条件调用print(portfolio_cost('Data/portfolio.dat'))——所以第一次import pcost才会打印出44671.15(这正是 Data/portfolio.dat 的核算结果,见下文第五节)。这是理解__name__保护必要性的关键动机。pcost.portfolio_cost('Data/portfolio2.dat')是"模块名.函数名"的属性访问。函数定义在模块顶层,导入后即可通过模块对象调用。注意这里的文件路径Data/portfolio2.dat是相对于当前工作目录的(而不是相对于模块文件所在目录),因为运行时是解释器进程负责打开文件。from stock import Stock把名字直接注入当前命名空间。与import stock(需要stock.Stock(...)访问)不同,from ... import ...让你直接用Stock构造对象。练习中的s.cost()返回49010.0,对应100 股 × 490.10 元,与 Solutions/1_5/stock.py 中Stock.cost()的实现完全吻合。
如果导入失败怎么办
文档给出的诊断路径非常实用:
- 确认运行目录:确保 Python 启动目录与
.py文件所在目录一致(os.getcwd()可查看); - 确认 sys.path 覆盖:若目录不一致,要么
cd过去,要么把目录加入sys.path; - 还可通过
print(pcost.__file__)查看模块实际加载自哪个文件,用于排查"导入到了旧版本文件"的问题。
三、第 (b) 部分:Main Module——if __name__ == '__main__'保护
问题场景
你的pcost.py最后一行是无条件执行的print(portfolio_cost('Data/portfolio.dat'))。作为脚本运行时这没问题,但一旦被别人import pcost,这行打印也会跟着执行——模块被导入时产生"副作用输出",这在真实工程中是不可接受的。
修改方案
在 Exercise 1.6 的官方解答 与仓库参考实现 Solutions/1_6/pcost.py 中,完整的pcost.py如下:
# pcost.py def portfolio_cost(filename): total_cost = 0.0 with open(filename) as f: for line in f: fields = line.split() try: nshares = int(fields[1]) price = float(fields[2]) total_cost = total_cost + nshares * price # This catches errors in int() and float() conversions above except ValueError as e: print("Couldn't parse:", line) print("Reason:", e) return total_cost if __name__ == '__main__': print(portfolio_cost('Data/portfolio.dat'))关键改动只有一处:把末尾的无条件打印缩进放进if __name__ == '__main__':分支内。函数定义部分原封不动,异常处理(except ValueError打印坏行与原因)也完整保留。
两种运行方式的验证
方式一:作为主程序运行(脚本模式)
bash % python3 pcost.py 44671.25 bash %方式二:作为库导入(模块模式)——不应有任何输出
>>> import pcost >>>第二次导入与第一次(第 (a) 部分末尾未修改版本)的对比一目了然:修改后import pcost静默完成,只加载函数定义,不再打印任何东西。
原理深挖:__name__到底是什么
- 每个模块在运行时都有一个内置变量
__name__; - 当模块作为主程序被
python3 pcost.py执行时,解释器把它命名为'__main__',于是if __name__ == '__main__':为真,打印执行; - 当模块被
import pcost导入时,__name__被设置为模块名'pcost',条件为假,分支跳过; - 因此这行保护把"顶层执行代码"与"可复用的定义"隔离开来,让同一文件身兼两职:既能命令行直跑,又能被安全导入。
需要补充的工程常识是:import对同一模块只会执行一次顶层代码(后续导入直接复用sys.modules中的缓存对象),这也是"重启解释器后再做本练习"能观察到一致行为的原因之一。
四、从模块到复用:为什么这是 Python 工程的基石
把代码拆成模块并加上__name__保护,是 Python 从"脚本"走向"程序库"的第一步,其收益在课程后续练习中被反复放大:
- Exercise 3.3 起出现的共享模块
reader.py,把 CSV/数据文件读取逻辑从各脚本中抽离,多个脚本共同import reader(参考 Solutions/3_3/reader.py); - Exercise 9.3 更进一步,把模块组织成包(package)
structly,内部再划分tableformat子包与formats格式插件目录(见 Solutions/9_3/structly); teststock.py(如 Solutions/6_1/teststock.py)则直接from stock import Stock后编写单元测试——这正是本练习from stock import Stock语法在真实测试代码中的典型用法。
可以说,本节的import、from ... import ...、if __name__ == '__main__'三者组合,构成了后续所有"可测试、可复用"代码的组织基础。
五、数据核算:用仓库真实数据验证结果
为了让输出可复现,这里把两个数据文件的账目完整核算一遍。
Data/portfolio.dat(共 7 行持仓记录):
AA 100 32.20 IBM 50 91.10 CAT 150 83.44 MSFT 200 51.23 GE 95 40.37 MSFT 50 65.10 IBM 100 70.44逐行累加份额 × 单价:
| 股票 | 份额 | 单价 | 小计 |
|---|---|---|---|
| AA | 100 | 32.20 | 3220.00 |
| IBM | 50 | 91.10 | 4555.00 |
| CAT | 150 | 83.44 | 12516.00 |
| MSFT | 200 | 51.23 | 10246.00 |
| GE | 95 | 40.37 | 3835.15 |
| MSFT | 50 | 65.10 | 3255.00 |
| IBM | 100 | 70.44 | 7044.00 |
| 合计 | 44671.15 |
Data/portfolio2.dat(4 行记录):
AA 50 27.10 HPQ 250 43.15 MSFT 25 50.15 GE 125 52.10合计为50×27.10 + 250×43.15 + 25×50.15 + 125×52.10 = 1355.00 + 10787.50 + 1253.75 + 6512.50 = 19908.75。
一个值得注意的事实核对:交互式会话中pcost.portfolio_cost('Data/portfolio2.dat')返回19908.75,与上面核算完全一致;而练习文档第 (b) 部分示例显示运行python3 pcost.py输出44671.25,按当前仓库的 Data/portfolio.dat 实际核算应为44671.15(与第 (a) 部分交互式import pcost打印的结果相同)。可以推断44671.25对应的是课程编写时期旧版本数据文件,以你本地仓库数据实测为准。这个小细节恰好说明:动手运行永远比照抄文档数字更可靠。
六、练习自检清单
完成本练习后,你可以对照以下清单确认自己真正掌握了模块机制:
- 能否在交互式解释器中执行
import pcost且不产生任何输出?(证明__name__保护生效) - 能否用
pcost.portfolio_cost('Data/portfolio.dat')拿到44671.15? - 能否用
from stock import Stock创建对象并调用cost()得到49010.0? - 当
import失败时,是否知道通过sys.path与工作目录定位问题? - 是否理解"
import会执行模块顶层代码"与"__name__在两种运行方式下取值不同"这两条底层机制?
如果全部通过,说明你已经具备了 Python 模块化开发的基础能力;接下来的课程将在这个基础上,把模块演化为包、引入验证器与元编程等高级主题。
本文基于 Exercises/ex1_6.md 及仓库内对应解答 Exercises/soln1_6.md、参考实现 Solutions/1_6/pcost.py 与数据文件编写,源码与数据均可在当前仓库中直接查阅与复现。
- 示例工程
- 教程
【免费下载链接】python-mastery
Advanced Python Mastery (course by @dabeaz)
相关推荐
Python 包化实战:将模块重构为 structly 包(Advanced Python Mastery Exercise 9.2)
Python 包化实战:将模块重构为 structly 包(Advanced Python Mastery Exercise 9.2) 导读 :本篇文章对应 A
示例工程教程python-mastery 课程实战:用 `if __name__ == '__main__'` 守卫把 Python 脚本改造成可复用模块(Exercise 1.6 详解)
python mastery 课程实战:用 if __name__ == '__main__' 守卫把 Python 脚本改造成可复用模块(Exercise 1
示例工程教程Python模块与包管理终极指南:import机制深度剖析与实战技巧
Python模块与包管理终极指南:import机制深度剖析与实战技巧 Python模块与包管理是每个Python开发者必须掌握的核心技能!无论你是Python初
示例工程教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考