写出干净可维护的 Python 脚本:从 shebang 到入口函数

2026-06-02 21 预计阅读时间:1 分钟
来源:realpython.com AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:7 分钟

随手写一个 .py 文件就能跑,但半年后打开再看——imports 乱成一团、常量散落各处、逻辑全挤在全局作用域里。这类脚本不是"能用就行",而是"能用但不敢改"。把脚本结构理清楚,成本不高,收益却很实在:阅读快、改起来放心、新同事上手也容易。

下面逐项拆解一个 Python 脚本该有的骨架,并给出一份可以直接套用的模板。

shebang:让脚本自己声明解释器

脚本第一行写上:

#!/usr/bin/env python3

这行告诉操作系统用哪个解释器执行。用 env python3 而不是硬编码 /usr/bin/python3,是因为不同机器上 Python 的安装路径可能不一样——macOS 用 Homebrew 装、Linux 发行版自带、容器里可能是 /usr/local/bin/python3env 会从 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 之后就是一长串过程式逻辑。问题在于:全局变量难以追踪、无法被其他模块复用、pytestdoctest 无法针对局部函数测试。

标准做法是定义一个 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 分钟翻找。从下一个脚本开始用这套骨架,习惯很快就会养成。


相关推荐