要点
模块被导入时会执行顶层代码。name 判断帮助区分“直接运行入口”和“作为工具被导入”。
1. REPL:即时反馈的实验场
在项目目录执行 uv run python(或 uv run python -q 简化输出)进入交互式解释器:
>>> 2 ** 10
1024
>>> name = "python"
>>> name.upper()
'PYTHON'
>>> sorted("python")
['h', 'n', 'o', 'p', 't', 'y']
>>> help(str.replace) # 随时查阅文档
上面是 REPL 会话记录(提示符与输出均属于交互式解释器),不是可直接保存为 .py 的脚本。
REPL 的高效用法:
| 特性 | 说明 |
|---|---|
_ | 保存上一次表达式的结果 |
| Tab 补全 | 自动补全属性与方法名 |
help(obj) | 查看对象文档 |
exit() 或 Ctrl+D | 退出 |
💡 REPL 适合验证”这一行能不能这么写”;一旦代码超过 10 行,就该写进脚本文件。
2. 第一个脚本
新建 hello.py:
"""练习脚手架:演示脚本结构与现代写法。"""
import sys
def greet(name: str, greeting: str = "Hello") -> str:
"""返回问候语。"""
return f"{greeting}, {name}!"
def main(argv: list[str] | None = None) -> int:
"""程序入口:解析参数并打印问候。"""
args = sys.argv[1:] if argv is None else argv
target = args[0] if args else "World"
print(greet(target))
return 0 # 退出码,0 表示成功
if __name__ == "__main__":
raise SystemExit(main())
运行:
uv run hello.py # Hello, World!
uv run hello.py Python # Hello, Python!
echo $? # 0(上一条命令的退出码)
逐点解析:
- 模块 docstring:文件首行三引号字符串,
help(hello)时展示 - 类型注解:
name: str、-> str为现代 Python 标配,编辑器据此提供提示 list[str] | None:3.10+ 直接用内置泛型和|联合类型,无需typing.Optionalmain返回退出码:让脚本可被 shell 与 CI 判断成败
下面给出一个包含正常参数、缺省参数和不合规参数的完整版本。它仍使用“可导入的函数 + __main__ 入口”结构,避免把参数解析分散到模块顶层:
"""A small import-safe command-line greeting program."""
import sys
def greet(name: str, greeting: str = "Hello") -> str:
return f"{greeting}, {name}!"
def main(argv: list[str] | None = None) -> int:
args = sys.argv[1:] if argv is None else argv
if len(args) > 2:
print("usage: hello.py [name] [greeting]", file=sys.stderr)
return 2
name = args[0] if args else "World"
greeting = args[1] if len(args) == 2 else "Hello"
print(greet(name, greeting))
return 0
if __name__ == "__main__":
raise SystemExit(main())
运行 python -I hello.py Ada Hi 会输出 Hi, Ada! 并以 0 退出。传入三个以上参数时,它会把用法写到标准错误并以 2 退出;导入该文件不会自行打印或退出。
3. __main__ 惯用法详解
if __name__ == "__main__": 是 Python 最著名的惯用法,机制如下:
- Python 每个
.py文件都是一个模块,模块有个字符串属性__name__ - 直接运行某文件时,该文件的
__name__被设为"__main__" - 被 import 时,
__name__等于模块名(如"hello")
# hello.py 底部
print(f"__name__ = {__name__}")
if __name__ == "__main__":
main()
uv run hello.py # __name__ = __main__(会执行 main)
uv run python -c "import hello" # __name__ = hello(不会执行 main)
为什么必须写它:没有这行,import hello 会立即执行 main()——测试框架、其他模块导入时都会”误触发”程序。加上守卫后,同一文件既能被导入复用,又能直接执行。
4. 脚本 vs 模块:组织雏形
当逻辑变多,把可复用部分拆为函数/类,__main__ 块只留”组装”:
# bookmarks.py 的典型骨架
def load(path: str) -> dict[str, str]:
...
def add(data: dict[str, str], title: str, url: str) -> None:
...
def main() -> int:
data = load("bookmarks.json")
add(data, "Python", "https://python.org")
return 0
if __name__ == "__main__":
raise SystemExit(main())
这个骨架贯穿整个入门路径,第 08 篇的综合项目将扩展为命令行工具。
最佳实践
把可复用逻辑放函数中,入口负责解析参数、调用和返回退出码。main 判断避免模块被导入时就执行命令;main 是约定名称,不是解释器要求。
docstring 说明输入约束、结果和可能失败之处,避免复述函数名。用导入测试确认没有意外读写,再分别验证正常执行与失败时的退出状态,便于脚本和 CI 可靠调用。
常见问题
Q1: python hello.py 和 python -m hello 有区别吗?
A: 有。前者按路径运行;后者把 hello 当模块运行,会先把它所在目录加入 sys.path,且 __name__ 同样是 "__main__"。包内脚本推荐 -m 方式。
Q2: 为什么 uv run python 而不是直接 python?
A: uv run 保证使用项目 .venv 的解释器,直接 python 可能命中系统版本,依赖不一致。
练习与实践
练习一:改造脚本
- 给
greet增加punctuation: str = "!"参数 - 支持
hello.py <name> <greeting>:第二个参数作为问候词 - 参数缺失时返回退出码 1 并打印用法提示
练习二:验证模块机制
- 在
main()中加入print(__name__) - 分别用
uv run hello.py与uv run python -c "import hello"观察__name__ - 写一段话解释两种输出差异的原因
提示:差异来自 Python 导入系统对”主模块”的约定,与变量与类型中的对象属性机制一脉相承。
相关文档
- 📄 变量与类型 — 理解
str、list[str]的类型世界 - 📄 内置函数全表 —
print、help等内建能力速查 - 📄 Python 一行式速查 — REPL 常用表达集锦