随手写一个 .py 文件就能跑,但半年后打开再看——imports 乱成一团、常量散落各处、逻辑全挤在全局作用域里。这类脚本不是"能用就行",而是"能用但不敢改"。把脚本结构理清楚,成本不高,收益却很实在:阅读快、改起来放心、新同事上手也容易。
下面逐项拆解一个 Python 脚本该有的骨架,并给出一份可以直接套用的模板。
shebang:让脚本自己声明解释器
脚本第一行写上:
#!/usr/bin/env python3
这行告诉操作系统用哪个解释器执行。用 env python3 而不是硬编码 /usr/bin/python3,是因为不同机器上 Python 的安装路径可能不一样——macOS 用 Homebrew 装、Linux 发行版自带、容器里可能是 /usr/local/bin/python3。env 会从 PATH 里找,兼容性更好。
加了 shebang 后,脚本可以直接当作可执行文件调用:
chmod +x my_script.py
./my_script.py
不需要再写 python3 my_script.py,在 Makefile、CI 脚本或 cron 任务里更简洁。
imports:分组排序,一眼看清依赖
imports 不要随意堆叠。按功能分三组,每组之间空一行,组内按字母排序:
# 1. 标准库
import json
import os
import sys
# 2. 第三方库
import requests
from rich.console import Console
# 3. 本项目模块
from myapp.config import load_config
from myapp.utils import format_output
这样读代码时能快速判断:这个脚本依赖哪些外部包?哪些是自己的模块?如果第三方库突然缺了,第二组一眼就能定位。
手动排序容易遗漏,交给工具更可靠——后面会提到 Ruff。
常量集中放在顶部
脚本里的"魔法数字"和固定字符串,统一提取到 imports 下方、逻辑上方,用大写命名:
MAX_RETRIES = 3
DEFAULT_TIMEOUT = 30 # 秒
API_BASE_URL = "https://api.example.com/v1"
OUTPUT_ENCODING = "utf-8"
好处:改配置只动一处;读代码时不用在逻辑流里翻找硬编码值;常量名本身就在解释含义——30 不如 DEFAULT_TIMEOUT 清楚。
如果常量较多或需要按环境切换,可以进一步抽到单独的 config.py 或用环境变量覆盖:
API_BASE_URL = os.getenv("API_BASE_URL", "https://api.example.com/v1")
入口函数:把逻辑从全局作用域里拉出来
最常见的坏结构是所有代码直接写在文件顶层——import 之后就是一长串过程式逻辑。问题在于:全局变量难以追踪、无法被其他模块复用、pytest 或 doctest 无法针对局部函数测试。
标准做法是定义一个 main() 函数,然后用标准入口调用:
def main() -> None:
config = load_config()
data = fetch_data(config)
result = process(data)
print(format_output(result))
if __name__ == "__main__":
main()
if __name__ == "__main__" 这行保证:直接运行脚本时执行 main();被其他模块 import 时不会自动跑逻辑。脚本既能独立执行,也能作为库被引用。
用 Ruff 自动格式化与排序
手动维护格式和 import 顺序费时且不可靠。Ruff 一个工具覆盖了格式化和 lint,速度极快(比 Black + isort 快几十倍),配置也简单。
安装并运行:
pip install ruff
ruff format my_script.py # 格式化,替代 Black
ruff check --fix my_script.py # lint + 自动修复,含 import 排序
在项目根目录放一个 pyproject.toml 统一规则:
[tool.ruff]
line-length = 88
[tool.ruff.lint.isort]
section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"]
known-first-party = ["myapp"]
known-first-party 告诉 Ruff 哪些包是本项目模块,排序时归入第三组。团队所有人、CI 管线都用同一份配置,风格自然一致。
完整模板:直接套用
把上面所有要点合在一起,得到一份可以直接复制到新项目的脚本骨架:
#!/usr/bin/env python3
"""从 API 拉取数据并输出摘要——脚本结构模板示例."""
import json
import os
import sys
import requests
from myapp.config import load_config
from myapp.utils import format_output
# ── 常量 ──────────────────────────────────────────────
MAX_RETRIES = 3
DEFAULT_TIMEOUT = 30
API_BASE_URL = os.getenv("API_BASE_URL", "https://api.example.com/v1")
OUTPUT_ENCODING = "utf-8"
def fetch_data(config: dict) -> dict:
"""根据配置请求 API,失败时重试."""
for attempt in range(1, MAX_RETRIES + 1):
try:
resp = requests.get(
API_BASE_URL,
params=config.get("params"),
timeout=DEFAULT_TIMEOUT,
)
resp.raise_for_status()
return resp.json()
except requests.RequestException as exc:
print(f"请求失败(第 {attempt} 次):{exc}", file=sys.stderr)
sys.exit(1)
def process(data: dict) -> str:
"""将原始数据转为摘要文本."""
summary = {
"count": len(data.get("items", [])),
"first": data["items"][0]["name"] if data.get("items") else None,
}
return json.dumps(summary, ensure_ascii=False, indent=2)
def main() -> None:
config = load_config()
data = fetch_data(config)
result = process(data)
print(result)
if __name__ == "__main__":
main()
使用方式:复制这个骨架,改模块名、常量值和业务逻辑,然后 ruff format + ruff check --fix 过一遍即可提交。
采纳建议与取舍
| 场景 | 建议 |
|---|---|
| 一次性脚本、几十行 | 至少加 main() 和常量提取;shebang 和 import 排序可选 |
| 会长期维护的脚本 | 全部采用:shebang、分组 import、常量区、main()、Ruff |
| 团队项目 | 把 pyproject.toml 的 Ruff 配置提交进仓库,CI 加 ruff check 门禁 |
| 已有大量旧脚本 | 不必一次全改;新脚本用新结构,旧脚本改到时顺手补 main() 和常量 |
结构不是装饰——它决定了几个月后你打开这个文件时,是花 30 秒看懂还是花 10 分钟翻找。从下一个脚本开始用这套骨架,习惯很快就会养成。