一个 Python 脚本随手写起来很容易,但写得"有结构"却常被忽略。shebang 行怎么写、import 分组顺序、常量放哪、入口函数怎么组织——这些细节看似琐碎,却直接影响脚本的可读性、可维护性和跨平台可移植性。下面逐项拆解,并给出一份可直接套用的模板。
shebang:让脚本自己声明解释器
shebang 是脚本第一行 #! 开头的声明,告诉操作系统用哪个解释器执行文件。Python 脚本推荐写法:
#!/usr/bin/env python3
而不是硬编码路径 #!/usr/bin/python3。原因很简单:env 会从当前用户的 PATH 中查找 python3,在虚拟环境、conda 环境、自定义安装路径下都能正确命中,硬编码路径则做不到。
实际操作中,加上 shebang 后还需要赋予执行权限:
chmod +x my_script.py
# 之后可以直接运行
./my_script.py
如果不加 shebang,就只能 python3 my_script.py 调用。两者功能一样,但 shebang 让脚本更像一个"可执行程序",在 Makefile、CI pipeline、cron 任务里调用更自然。
import 分组:三层顺序,每组之间空一行
Python 社区约定(PEP 8、Google Style、ruff 默认规则)将 import 分为三组,每组之间空一行:
- 标准库 —
os,sys,json,pathlib等 - 第三方库 —
requests,numpy,click等 - 本地模块 — 自己项目内的
my_utils,config等
# 1. 标准库
import json
import os
import sys
from pathlib import Path
# 2. 第三方库
import click
import requests
# 3. 本地模块
from my_utils import format_output
from config import DEFAULT_TIMEOUT
每组内部,import xxx 排在 from xxx import yyy 前面,再按模块名字字母序排列。这不是审美偏好——ruff 的 isort 规则默认就按这个顺序检查和自动修复,不遵守会直接报 lint 错误。
常量:集中放在模块顶部,命名全大写
脚本里硬编码的数值、URL、超时秒数等,应该提取为模块级常量,放在 import 之后、业务逻辑之前。命名用 UPPER_SNAKE_CASE:
DEFAULT_TIMEOUT = 30
MAX_RETRIES = 3
API_BASE_URL = "https://api.example.com"
OUTPUT_DIR = Path("results")
好处是修改配置时只改一处,而不是在代码里到处搜索魔法数字。ruff 的 flake8-eradicate 规则甚至能检测到未使用的常量,提醒你清理。
入口函数:用 main() + if __name__ == "__main__"
把核心逻辑放进 main() 函数,底部用标准守卫调用:
def main() -> None:
args = parse_args()
data = fetch_data(args.url, timeout=DEFAULT_TIMEOUT)
result = process(data)
write_output(result, OUTPUT_DIR)
if __name__ == "__main__":
main()
为什么不只是裸写逻辑?两个直接收益:
- 可测试:其他脚本或测试文件可以
import your_script后直接调用main()或内部函数,不会触发执行。 - 可复用:未来想把脚本改成一个库,
main()变成 API 入口,结构几乎不用动。
main() 的返回类型标注 -> None 不是必须的,但加上后 ruff 的类型检查更顺畅,也给读者明确信号:这个函数不返回有意义的结果,成功靠副作用(写文件、打印输出),失败靠异常或 sys.exit。
ruff 一键格式化与检查
上面所有结构规则——import 顺序、常量命名、未使用变量——ruff 都能自动检查和修复。在项目根目录创建 pyproject.toml:
[tool.ruff]
line-length = 88
[tool.ruff.lint]
select = ["I", "E", "W", "F", "UP"]
# I = isort (import排序)
# E/W = pycodestyle (格式)
# F = pyflakes (未使用变量等)
# UP = pyupgrade (现代Python语法)
然后运行:
# 检查所有问题
ruff check my_script.py
# 自动修复(import排序、格式等)
ruff check --fix my_script.py
# 格式化(类似black的风格)
ruff format my_script.py
养成习惯:写完脚本跑一次 ruff check --fix && ruff format,结构问题基本零残留。
可直接套用的完整模板
把上面所有规则合到一起,得到一份最小但完整的脚本骨架:
#!/usr/bin/env python3
"""Fetch data from API and save processed results to disk."""
# 1. 标准库
import json
import sys
from pathlib import Path
# 2. 第三方库
import requests
# 3. 本地模块
# (暂无)
# ── 常量 ──────────────────────────────────────────
API_BASE_URL = "https://api.example.com/data"
DEFAULT_TIMEOUT = 30
MAX_RETRIES = 3
OUTPUT_DIR = Path("results")
# ── 函数 ──────────────────────────────────────────
def fetch_data(url: str, timeout: int = DEFAULT_TIMEOUT) -> dict:
"""从远程 API 获取 JSON 数据。"""
for attempt in range(1, MAX_RETRIES + 1):
try:
resp = requests.get(url, timeout=timeout)
resp.raise_for_status()
return resp.json()
except requests.RequestException as exc:
print(f"第 {attempt} 次请求失败: {exc}")
sys.exit(1)
def process(data: dict) -> list[str]:
"""对原始数据做简单提取,返回关键字段列表。"""
return [item["name"] for item in data.get("items", []) if "name" in item]
def write_output(lines: list[str], dest: Path) -> None:
"""将结果逐行写入目标文件。"""
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_text("\n".join(lines), encoding="utf-8")
print(f"已写入 {dest},共 {len(lines)} 条")
# ── 入口 ──────────────────────────────────────────
def main() -> None:
data = fetch_data(API_BASE_URL)
result = process(data)
write_output(result, OUTPUT_DIR / "names.txt")
if __name__ == "__main__":
main()
使用方式:
# 1. 保存为 fetch_data.py
# 2. 安装依赖
pip install requests ruff
# 3. 赋予执行权限并运行
chmod +x fetch_data.py
./fetch_data.py
# 4. 检查结构合规
ruff check --fix fetch_data.py && ruff format fetch_data.py
这份模板覆盖了 shebang、import 三组分组、常量集中、main() 入口守卫,并且 ruff 一跑就能通过。下次写新脚本时,复制这个骨架再填充业务逻辑,比从空文件起步省心得多。
写脚本前的快速自检
每次提交脚本前,用这五条对照:
- shebang:是否有
#!/usr/bin/env python3? - import 顺序:标准库 / 第三方 / 本地,每组空一行?
- 常量:魔法数字是否已提取为
UPPER_SNAKE_CASE常量? - 入口:核心逻辑是否在
main()里,底部有if __name__守卫? - ruff:
ruff check和ruff format是否零报错?
五条全过,脚本结构就基本到位了。剩下的才是真正有意思的部分——业务逻辑本身。