浏览知识库目录

Python

命令行、配置与日志

用 argparse、TOML、环境变量和 logging 构建可配置、可诊断的命令行入口。

命令行、配置与日志

命令行入口负责把文本参数转换为应用调用,再把结果转换为输出和退出码。它不应承载数据库细节或业务规则。配置和日志也应在入口完成组装,而不是散落在各模块。


一、学习目标

  • 使用 argparse 设计子命令
  • 返回稳定的退出码
  • 用 TOML 和环境变量加载配置
  • 区分用户输出与诊断日志
  • 避免记录令牌和隐私数据

二、设计子命令

import argparse


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="study-tasks",
        description="管理本地学习任务",
    )
    subparsers = parser.add_subparsers(dest="command", required=True)

    add = subparsers.add_parser("add", help="新增任务")
    add.add_argument("title")

    listing = subparsers.add_parser("list", help="列出任务")
    listing.add_argument("--status", choices=["todo", "done"])

    complete = subparsers.add_parser("done", help="完成任务")
    complete.add_argument("task_id", type=int)
    return parser

效果:

study-tasks --help
study-tasks add "阅读 Python 文档"
study-tasks list --status todo
study-tasks done 1

参数解析只负责语法层校验;标题非空、任务是否存在等属于应用服务。


三、入口函数与退出码

from collections.abc import Sequence


def main(argv: Sequence[str] | None = None) -> int:
    args = build_parser().parse_args(argv)
    try:
        dispatch(args)
    except TaskError as exc:
        print(f"错误:{exc}")
        return 2
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

常用约定:

  • 0:成功
  • 2:参数或可预期的用户输入错误
  • 其他非零值:应用按文档定义

测试可直接调用 main(["add", "阅读"]),无需修改真实的 sys.argv


四、TOML 配置

study_tasks.toml

[study_tasks]
database = "tasks.db"
api_url = "https://api.example.com/tasks"

读取:

import tomllib
from pathlib import Path


def load_toml(path: Path) -> dict[str, object]:
    if not path.exists():
        return {}
    with path.open("rb") as file:
        value = tomllib.load(file)
    section = value.get("study_tasks", {})
    if not isinstance(section, dict):
        raise ValueError("[study_tasks] 必须是 TOML 表")
    return section

tomllib 只读 TOML。应用通常不需要自动重写用户配置。


五、环境变量与优先级

import os

database = os.getenv("STUDY_TASKS_DB") or str(
    config.get("database", "tasks.db")
)

本项目采用:

  1. 环境变量
  2. TOML 配置
  3. 程序默认值

同一项目必须固定优先级并写进文档。密钥只从环境或受保护的秘密系统读取,不写入公开配置和版本库。


六、日志

import logging

logger = logging.getLogger(__name__)


def complete(task_id: int) -> None:
    logger.info("completing task id=%s", task_id)

入口统一配置:

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(name)s %(message)s",
)

使用日志参数占位,而不是预先构造 f-string:

logger.debug("loaded %d tasks", len(tasks))

日志级别:

  • DEBUG:开发诊断细节
  • INFO:正常生命周期事件
  • WARNING:可恢复异常状况
  • ERROR:当前操作失败
  • CRITICAL:服务无法继续

七、用户输出与日志分离

print("已创建 #3 阅读 Python 文档")
logger.info("task created id=%s", task.id)

print 是命令输出协议的一部分,适合管道和用户阅读;日志用于诊断,通常写到标准错误或日志系统。

如果命令提供 --json,标准输出必须只包含 JSON,日志不能混入。


八、敏感信息

禁止记录:

  • API Token、密码和 Cookie
  • 完整数据库连接串
  • 用户隐私字段
  • 未经处理的完整请求体

可以记录请求 ID、任务 ID、耗时、状态码和经过白名单处理的字段。


九、常见错误

  • 在每个模块重复调用 basicConfig
  • 配置优先级随调用顺序变化。
  • 捕获异常后同时打印、记录并再次在上层记录。
  • 成功和错误都返回退出码 0
  • 把业务服务写成依赖 argparse.Namespace

十、练习与自测

  1. 为 CLI 增加 delete 子命令和明确退出码。
  2. 实现 --verbose,只改变日志级别。
  3. 测试环境变量覆盖 TOML,而 TOML 覆盖默认值。

自测:

  • 参数解析与业务校验为什么应分层?
  • 日志和标准输出分别服务谁?
  • 配置优先级为何必须固定?

十一、官方资料

上一篇:文件系统、序列化、正则与时间 | 下一篇:SQLite、事务与数据访问