☰
practical-python 教程第 3.5 节精讲:理解 Python 的 Main Module、`__main__` 守卫与命令行脚本编写
2026/10/5 6:37:59 网站建设 项目流程
  • 教程
  • 文档

【免费下载链接】practical-python

Practical Python Programming (course by @dabeaz)

项目地址:https://gitcode.com/gh_mirrors/pr/practical-python
点击查看免费下载

本篇技术指南基于 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.py
import 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/Ostdout/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)

项目地址:https://gitcode.com/gh_mirrors/pr/practical-python
点击查看免费下载

相关推荐

上一篇:如何通过 CDN 把 Harper 直接引入网页而不用构建工具?
下一篇:Ghost-Downloader-3案例研究:企业级应用

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询