- 教程
- 文档
【免费下载链接】practical-python
Practical Python Programming (course by @dabeaz)
本篇技术指南基于 practical-python 课程仓库中的 Notes/03_Program_organization/05_Main_module.md 展开,系统讲解 Python 中"主模块(Main Module)"的概念、if __name__ == '__main__'惯用法、程序模板,以及如何把模块改造成可复用的命令行工具。读完本篇,你将掌握一套可立即套用的 Python 脚本编写模板,理解sys.argv、标准 I/O、环境变量与退出码的实战用法,并能把report.py、pcost.py这类程序改造成既可作为库导入、又可直接从终端运行的双重形态脚本。
1. 从 C/Java 的main函数说起:Python 为什么没有它
许多编程语言为程序定义了一个固定的入口函数。例如 C/C++:
int main(int argc, char *argv[]) { ... }Java 中则是:
class myprog { public static void main(String args[]) { ... } }这类函数是应用启动时第一个被执行的函数,程序逻辑从它开始展开。
Python 刻意没有引入这样的main函数或方法。取而代之的是一个main module(主模块)概念:主模块就是解释器启动时最先运行的那个源文件。
bash % python3 prog.py ...无论文件名是什么(prog.py、report.py、ticker.py都可以),只要是你把它作为参数传给解释器,这个文件就成为主模块。这一点与"函数入口"的模型有本质差异——它把"程序从哪里开始执行"从"特定函数"变成了"整个文件从上到下顺序执行"。
2.if __name__ == '__main__':主程序守卫惯用法
既然 Python 按文件顺序执行代码,那么如何区分"这个文件是被当作程序直接运行"还是"被别的模块 import 进来当库用"?答案藏在每个模块自动拥有的特殊变量__name__中。
标准实践是,凡是可能作为主脚本运行的模块,都采用如下约定:
# prog.py ... if __name__ == '__main__': # Running as the main program ... statements ...被if语句包裹的这些语句,才真正构成"主程序"。这个写法被广泛称为__main__检查或主程序守卫(main guard)。
2.1 主程序运行 vs 库导入
任何一个 Python 文件都有两种身份,取决于它被如何使用:
bash % python3 prog.py # 作为主程序运行import prog # 作为库导入在这两种情况下,__name__都等于模块的名字(即文件名去掉.py)。唯一的例外是:只有当该文件作为主程序运行时,它的__name__才会被解释器特殊设置为字符串'__main__'。
通常我们不希望属于主程序的语句在库导入时也执行。因此,对可能被两种方式使用的代码,几乎总是要加上这个判断:
if __name__ == '__main__': # 被 import 加载时不会执行 ...从仓库源码可以印证这一点:Solutions/3_14/pcost.py 中,文件在模块顶层直接写了sys.argv读取和print('Total cost:', cost),这导致import pcost时这些语句会立即执行(模块顶层代码在 import 时全部执行)。这正是课程接下来要通过重构解决的问题。而在 Solutions/3_16/pcost.py 中,同样的逻辑被收进main(args)函数并用守卫包裹:
def main(args): if len(args) != 2: raise SystemExit('Usage: %s portfoliofile' % args[0]) filename = args[1] print('Total cost:', portfolio_cost(filename)) if __name__ == '__main__': import sys main(sys.argv)3. 程序模板(Program Template):把逻辑收进函数
基于上述约定,编写一个结构清晰的 Python 程序的标准模板如下:
# prog.py # Import statements (libraries) import modules # Functions def spam(): ... def blah(): ... # Main function def main(): ... if __name__ == '__main__': main()这个模板的要点是:
- 顶部集中导入所需库;
- 中间定义各类函数,只描述逻辑、不立即执行;
main()函数集中组织主流程;- 最底部用守卫调用
main(),保证只有作为主程序运行时才真正开始干活。
对照仓库中 Solutions/3_16/report.py 的实现,该模板被完整落地:read_portfolio()、read_prices()、make_report_data()、print_report()、portfolio_report()等函数各司其职,main(args)只负责校验参数并调用portfolio_report(args[1], args[2]),最后以守卫结束:
def main(args): if len(args) != 3: raise SystemExit('Usage: %s portfile pricefile' % args[0]) portfolio_report(args[1], args[2]) if __name__ == '__main__': import sys main(sys.argv)这种结构带来的直接收益是:模块既可以import report后调用report.main([...])或report.portfolio_report(...)作为库复用,也可以从命令行直接执行。
4. 命令行工具与sys.argv
Python 常被用来编写命令行工具(command-line tools),即从 shell / 终端执行的脚本:
bash % python3 report.py portfolio.csv prices.csv这类脚本的典型应用场景包括自动化、后台任务等。
4.1 命令行参数列表
命令行本质上是一串文本字符串。以上述命令为例,参数列表存放在sys.argv中:
# 对应上面的 bash 命令 sys.argv # ['report.py', 'portfolio.csv', 'prices.csv']注意sys.argv[0]永远是脚本自身的名字,真正的用户参数从sys.argv[1]开始。
一个处理参数的简单示例:
import sys if len(sys.argv) != 3: raise SystemExit(f'Usage: {sys.argv[0]} ' 'portfile pricefile') portfile = sys.argv[1] pricefile = sys.argv[2] ...4.2 仓库中的参数校验实践
仓库中 Solutions/3_16/pcost.py 的参数处理是典型的len(sys.argv)校验模式:程序只接受一个数据文件参数,若参数个数不等于 2(脚本名 + 文件名),立即抛出SystemExit并打印用法提示:
def main(args): if len(args) != 2: raise SystemExit('Usage: %s portfoliofile' % args[0])同理,Solutions/6_15/ticker.py 中实时行情工具main(args)要求恰好 3 个参数(投资组合文件、日志文件、输出格式),不足或超出都直接退出:
def main(args): if len(args) != 4: raise SystemExit('Usage: %s portfoliofile logfile fmt' % args[0]) ticker(args[1], args[2], args[3])把参数个数校验、Usage提示放在main()开头,是编写健壮 CLI 脚本的一致做法。
5. 标准 I/O(Standard I/O)
标准输入输出(stdio)是三个与普通文件行为一致的特殊"文件"对象:
sys.stdout sys.stderr sys.stdin- 默认情况下,
print的输出流向sys.stdout; - 输入从
sys.stdin读取; - 回溯信息(traceback)和错误信息流向
sys.stderr。
关键认知:stdio 并不一定连接终端,它可能被连接到终端、普通文件、管道等任意地方。正因如此,命令行程序可以轻松参与 Unix 管道与重定向:
bash % python3 prog.py > results.txt # 或 bash % cmd1 | python3 prog.py | cmd2重定向与管道意味着:只要程序遵守"从sys.stdin读、往sys.stdout写"的约定,它就能自然地与其他命令行工具组合,成为数据处理流水线的一环。
6. 环境变量与os.environ
环境变量在 shell 中设置,Python 程序通过os.environ读取。os.environ是一个包含这些键值对的字典:
bash % setenv NAME dave bash % setenv RSH ssh bash % python3 prog.pyimport os name = os.environ['NAME'] # 'dave'需要注意的是,程序对环境变量的修改会反映到它后续启动的任何子进程中——这是环境变量跨进程传递的基本机制。在实际 CLI 工具设计中,环境变量常用来传递不需要写在命令行里的配置(如 API 密钥、默认路径等)。
7. 程序退出:用异常处理
Python 程序的退出通过异常机制处理:
raise SystemExit raise SystemExit(exitcode) raise SystemExit('Informative message')也可以使用替代写法:
import sys sys.exit(exitcode)非零的退出码表示出错。Unix 约定中,退出码 0 表示成功,非 0 表示各种失败。前面提到的参数校验失败场景就是通过raise SystemExit('Usage: ...')让程序以非零状态退出并同时向用户打印提示信息——一条语句同时完成"报错"与"退出"。
8.#!行:让脚本可直接执行
在 Unix 上,脚本文件的第一行可以写上 shebang(#!行),让系统直接以 Python 解释器启动该脚本:
#!/usr/bin/env python3 # prog.py ...#!/usr/bin/env python3的作用是从环境变量PATH中查找python3解释器,比硬编码/usr/bin/python3更具可移植性。它要求脚本文件具有可执行权限:
bash % chmod +x prog.py # 然后即可直接执行 bash % prog.py ... output ...注:Windows 上的 Python Launcher(
py)同样会读取#!行来确定使用哪个语言版本。
9. 脚本模板(Script Template):命令行程序的完整骨架
最后,课程给出一个专门用于"以命令行脚本方式运行"的完整模板,它是前述程序模板的升级版:
#!/usr/bin/env python3 # prog.py # Import statements (libraries) import modules # Functions def spam(): ... def blah(): ... # Main function def main(argv): # Parse command line args, environment, etc. ... if __name__ == '__main__': import sys main(sys.argv)与程序模板相比,脚本模板的变化在于:
- 首行加入
#!行,支持直接执行; main()接收argv参数(即sys.argv的引用),在函数内部完成命令行参数、环境变量等的解析;- 守卫块中引入
sys并调用main(sys.argv)。
注意守卫块中import sys的位置:把sys的导入放在if块内部是常见做法,这样当模块被当作库导入时不会无谓地引入依赖;当然,把import sys放在文件顶部同样完全可行,两种方式都成立。
10. 动手练习:把report.py与pcost.py改造成双重形态程序
原文档配有两道实战练习(Exercise 3.15 与 3.16),它们正是把前文理论落地到仓库程序中的过程。
10.1 Exercise 3.15:为report.py与pcost.py添加main()函数
在report.py中添加一个接收命令行选项列表的main()函数,使其输出与之前完全一致。完成之后,可以像这样在交互式解释器中调用它:
>>> import report >>> report.main(['report.py', 'Data/portfolio.csv', 'Data/prices.csv']) Name Shares Price Change ---------- ---------- ---------- ---------- AA 100 9.22 -22.98 IBM 50 106.28 15.18 CAT 150 35.46 -47.98 MSFT 200 20.89 -30.34 GE 95 13.48 -26.89 MSFT 50 20.89 -44.21 IBM 100 106.28 35.84 >>>同样修改pcost.py,使其具备相似的main()函数:
>>> import pcost >>> pcost.main(['pcost.py', 'Data/portfolio.csv']) Total cost: 44671.15 >>>注意这里main()接收的正是sys.argv形式的字符串列表(['report.py', 'Data/portfolio.csv', 'Data/prices.csv']),这也解释了为什么main(args)需要自行校验len(args)——因为被import后调用时,sys尚未介入,参数完全由调用者提供。仓库中的参考答案 Solutions/3_16/report.py 与 Solutions/3_16/pcost.py 即此练习的最终形态。
输出的数据来源为仓库 Work/Data/portfolio.csv(含name,shares,price三列)与 Work/Data/prices.csv,CSV 解析由 Solutions/3_16/fileparse.py 中的parse_csv()函数完成。
10.2 Exercise 3.16:让程序可以从命令行直接执行
进一步修改report.py和pcost.py,使它们能作为脚本在命令行上直接运行:
bash $ python3 report.py Data/portfolio.csv Data/prices.csv Name Shares Price Change ---------- ---------- ---------- ---------- AA 100 9.22 -22.98 IBM 50 106.28 15.18 CAT 150 35.46 -47.98 MSFT 200 20.89 -30.34 GE 95 13.48 -26.89 MSFT 50 20.89 -44.21 IBM 100 106.28 35.84 bash $ python3 pcost.py Data/portfolio.csv Total cost: 44671.15实现方式正是在每个文件末尾追加:
if __name__ == '__main__': import sys main(sys.argv)有了这段守卫,命令行运行与库导入两种场景得以共存:直接执行时__name__为'__main__',main()被调用;被import时守卫不成立,只有函数定义被加载,副作用代码不会执行。
11. 扩展观察:该模式的后续演化
main()+ 守卫的模式在课程后续部分被反复沿用并演进:
- Solutions/6_15/ticker.py 的实时行情工具将参数扩展为
portfoliofile logfile fmt三个; - 在第九部分(Solutions/9_5/porty-app/porty/report.py),
report.py被改造为porty包内的模块(from . import fileparse),其main(args)依旧保持len(args) != 4的校验并同样以if __name__ == '__main__': main(sys.argv)收尾——说明从单文件脚本演进到包结构后,这个骨架依然适用。
另外,对比 Solutions/3_14/report.py 可以看到:改造前,文件顶层直接硬编码了portfolio_report('../../Work/Data/portfolio.csv', '../../Work/Data/prices.csv')并在 import 时立即执行;改造后这部分移入main(),配合守卫使程序真正具备"双重身份"。
12. 小结
本节核心结论可归纳为一张速查表:
| 主题 | 关键要点 |
|---|---|
| 主模块 | Python 无main函数,最先运行的源文件即主模块 |
__name__ | 模块名;仅主程序运行时被设为'__main__' |
| 守卫惯用法 | if __name__ == '__main__': main()区分运行/导入两种身份 |
| 命令行参数 | sys.argv为字符串列表,argv[0]是脚本名 |
| 参数校验 | len(sys.argv)不符即raise SystemExit('Usage: ...') |
| 标准 I/O | stdout/stdin/stderr可连接终端、文件或管道 |
| 环境变量 | os.environ字典,修改会传递给子进程 |
| 程序退出 | raise SystemExit(code)或sys.exit(code),非 0 表示错误 |
#!行 | Unix 上声明解释器,需chmod +x赋予可执行权限 |
| 脚本模板 | #!行 + 导入 + 函数 +main(argv)+ 守卫调用 |
掌握"主模块 +__main__守卫 +main(argv)"这套组合,你的每个 Python 文件就同时具备了库与可执行程序两种身份,这是编写自动化脚本、命令行工具以及后续模块化大型程序的基础能力。更多相关内容可继续阅读本节之前的 模块基础(3.4) 与之后的 设计讨论(3.6),完整课程目录见 Notes/Contents.md。
- 教程
- 文档
【免费下载链接】practical-python
Practical Python Programming (course by @dabeaz)
相关推荐
Practical Python项目解析:Python脚本的组织与函数化编程
Practical Python项目解析:Python脚本的组织与函数化编程 痛点:为什么你的Python脚本总是变成"意大利面条代码"? 你是否曾经遇到过这样
教程文档Wand-Enhancer:5 步解锁 WeMod 完整功能,手机远程控制一并搞定
Wand Enhancer:5 步解锁 WeMod 完整功能,手机远程控制一并搞定 从沙发走到电脑前改一个修改器开关,再走回来——这个来回折腾,每个用大屏打游戏
桌面应用前端终极指南:如何用ER-Save-Editor轻松管理你的艾尔登法环存档
终极指南:如何用ER Save Editor轻松管理你的艾尔登法环存档 你是否曾因为更换电脑而无法加载自己辛苦培养的角色?或者想要与朋友分享完美Build却因S
桌面应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考