文章
合集Python 标准库第 7 / 8 篇

Python 标准库 Day 5:命令行、日志与调试

1. 问题引入

继续博客项目。前几天写的 blog_analytics.py,现在你想做几件事:

  1. 让脚本支持 python blog_analytics.py --top 10 --author Alice 这种命令行参数,而不是改源码
  2. 满屏的 print() 调试语句删也不是、留也不是——线上要不要打日志?打到哪儿?
  3. 程序莫名其妙出错,想"暂停在某一行看变量"
  4. 给脚本加上 --help 自动生成帮助文档
  5. 区分"普通信息""警告""错误"——用户应该看到什么、运维应该看到什么

这些事让代码从"能跑的脚本"变成"能交付的工具"。今天讲四个模块,是工程化的第一步。

2. 主题讲解

2.1 argparse —— 命令行参数解析

没有 argparse 的时代

import sys
top_n = int(sys.argv[1])           # 用户不传会 IndexError
author = sys.argv[2] if len(sys.argv) > 2 else None

问题:

  • 没有帮助文档
  • 类型检查全靠你自己写
  • 参数顺序写死,不能用 --top 10 这种语法
  • 错误提示烂

argparse 的标准模板

import argparse

parser = argparse.ArgumentParser(description="博客分析工具")

parser.add_argument("--top", type=int, default=10, help="显示前 N 篇")
parser.add_argument("--author", type=str, help="只看某个作者")
parser.add_argument("--verbose", "-v", action="store_true", help="详细输出")
parser.add_argument("input", type=str, help="输入文件路径")  # 位置参数

args = parser.parse_args()

print(args.top)        # int
print(args.author)     # str 或 None
print(args.verbose)    # bool
print(args.input)      # str

跑起来:

$ python script.py data.json --top 5 --author Alice -v
$ python script.py --help    # 自动生成的帮助文档

三种参数类型

类型写法调用方式
位置参数add_argument("input")script.py data.json
可选参数add_argument("--top")script.py --top 10
开关(flag)add_argument("--verbose", action="store_true")script.py --verbose

几个关键参数

parser.add_argument(
    "--top",
    type=int,                # 自动转类型,传错会报错
    default=10,              # 不传时的默认值
    required=True,           # 必须传(位置参数默认必须,可选参数默认可选)
    choices=[5, 10, 20],     # 限制取值
    help="显示前 N 篇文章",   # 帮助文档
)

子命令模式(高级,用得多)

像 git 一样支持 git commit / git push 这种子命令:

parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest="command")


top_parser = subparsers.add_parser("top", help="显示热门文章")
top_parser.add_argument("--n", type=int, default=10)


tags_parser = subparsers.add_parser("tags", help="标签统计")

args = parser.parse_args()

if args.command == "top":
    show_top(args.n)
elif args.command == "tags":
    show_tags()

调用:

python blog.py top --n 5
python blog.py tags

子命令模式适合"一个工具有多个子功能"——pip install、docker run、git commit 都是这个模式。

常见误区

误区 1:参数名带不带 **--**?

  • 带 --:可选参数,用 --name value 调用
  • 不带:位置参数,按顺序传

误区 2:参数名里的 **-** 怎么访问?

parser.add_argument("--max-size", type=int)
args.max_size       # ← 自动转成下划线,注意!

误区 3:直接退出怎么办

parser.error("文件不存在")    # 打印错误并退出,标准做法
sys.exit(1)                   # 也行,但不打印用法提示

2.2 logging —— 替代 print

为什么要用 logging

print 调试有几个核心问题:

  • 上线后忘删,日志噪音爆炸
  • 没有时间戳、没有级别、没有出处
  • 不能输出到文件、不能轮转
  • 关不掉——除非删代码

logging 解决了全部这些。

5 个日志级别

import logging

logging.debug("调试信息")        # 10  最详细
logging.info("普通信息")          # 20
logging.warning("警告")           # 30  默认显示这个级别及以上
logging.error("错误")             # 40
logging.critical("致命错误")      # 50

怎么选(这是面试常问的):

级别含义例子
DEBUG调试细节"正在解析文件 X" / 变量值
INFO正常事件"服务启动" / "用户登录"
WARNING异常但不致命"配置缺失,使用默认值" / "API 响应慢"
ERROR出错但能恢复"网络请求失败,重试中"
CRITICAL系统崩溃级别"数据库连不上" / "磁盘满"

最简单的配置

import logging

logging.basicConfig(
    level=logging.INFO,                 # 显示 INFO 及以上
    format="%(asctime)s [%(levelname)s] %(message)s",
)

logging.info("开始处理")
logging.warning("配置缺失")

输出:

2026-04-29 10:30:00,123 [INFO] 开始处理
2026-04-29 10:30:00,124 [WARNING] 配置缺失

推荐的工程化用法

不直接用 logging.info(...),而是先创建 logger:

import logging

logger = logging.getLogger(__name__)    # __name__ 是当前模块名

def process_file(path):
    logger.info("开始处理 %s", path)
    try:
        ...
    except Exception:
        logger.exception("处理失败")    # 自动带堆栈!

logger.exception(...) 是个神器——在 except 里调用,自动把完整 traceback 打到日志里,比 logger.error(str(e)) 信息丰富得多。

同时输出到文件和终端

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    handlers=[
        logging.FileHandler("app.log", encoding="utf-8"),
        logging.StreamHandler(),         # 终端
    ],
)

常见误区

**误区 1:用 **logger.info(f"x = {x}")** 而不是 ****logger.info("x = %s", x)**

logger.info(f"用户 {user.name} 登录")            # 字符串总是会拼接
logger.info("用户 %s 登录", user.name)            # 只有日志级别开启时才拼接

性能差异:DEBUG 日志被关闭时,f-string 仍然会执行(拼接字符串),而 %s 写法等到真要输出时才拼接。生产代码推荐 %s。

误区 2:模块顶层用 **logging.info**,不创建 logger

直接 logging.info(...) 用的是"根 logger",多模块协作时分不清是谁打的日志。**永远 ****logger = logging.getLogger(__name__)**。

误区 3:logging 配置写在多处

basicConfig 应该只在程序入口调用一次。库代码里只创建 logger,不配置——配置由调用方决定。

2.3 sys —— 解释器接口

sys 模块是 Python 解释器本身的接口,五个最常用的:

sys.argv —— 命令行参数原始列表


sys.argv     # ['script.py', '--top', '10']
sys.argv[0]   # 永远是脚本名

argparse 内部就是用它解析的。

sys.exit(code) —— 退出程序

sys.exit(0)    # 正常退出
sys.exit(1)    # 错误退出(任何非零都表示错)
sys.exit("配置错误")    # 带消息退出,写到 stderr,code=1

Unix 约定:退出码 0 是成功,非 0 是各种失败。脚本配合 shell 的 &&``|| 时这很重要。

sys.stdout / sys.stderr —— 标准输出/错误流

print("普通输出")              # 默认走 stdout
print("错误信息", file=sys.stderr)    # 错误信息走 stderr

为什么区分:stdout 是"程序的产出",stderr 是"程序的诊断"。Shell 里可以分开重定向:

python script.py > output.txt 2> error.log

错误信息走 stderr,避免污染脚本的"正常输出"。

sys.path —— 模块搜索路径

sys.path     # ['', '/usr/lib/python3.13', ...]
sys.path.append("/my/extra/path")    # 临时加搜索路径

debug 找不到模块时常用。

sys.platform —— 系统标识

sys.platform     # 'linux' / 'darwin' / 'win32'

写跨平台代码时区分系统。

2.4 pdb —— 调试器

最常用:打断点

def buggy_function(data):
    breakpoint()        # ← 程序运行到这一行会停下,进入交互模式
    result = process(data)
    return result

breakpoint() 是 Python 3.7+ 的内置函数,等价于 import pdb; pdb.set_trace(),但更短。

跑到这一行后会出现:

> /path/to/file.py(5)buggy_function()
-> result = process(data)
(Pdb)

几个核心命令(背下来)

命令作用
n
(next)
执行下一行(不进入函数)
s
(step)
执行下一行(进入函数内部)
c
(continue)
继续运行直到下一个断点
l
(list)
显示当前位置周围的代码
p 变量名打印变量
pp 变量名pretty print(适合 dict/list)
w
(where)
显示调用栈
q
(quit)
退出调试

打印表达式

调试时直接输入 Python 表达式就能算:

(Pdb) data
['a', 'b', 'c']
(Pdb) len(data)
3
(Pdb) [x.upper() for x in data]
['A', 'B', 'C']

这就是 pdb 比 print 强的地方——你能在断点处运行任何代码探索状态。

出错后事后调试

python -m pdb script.py     # 进入 pdb 启动模式

或在代码里捕获异常后进入 pdb:

import pdb

try:
    risky_operation()
except Exception:
    pdb.post_mortem()    # 进入异常发生时的状态

何时用 pdb vs print vs logging

场景工具
临时探索"这个变量长啥样"print
或直接 IPython 跑
复杂逻辑卡在某一步breakpoint()
+ pdb
长期、生产环境的诊断logging
性能分析Day 6 会讲

2.5 把这些缝起来:脚本工程化的标准结构

"""
blog_cli.py - 博客分析命令行工具
"""
import argparse
import logging
import sys
from pathlib import Path

logger = logging.getLogger(__name__)


def setup_logging(verbose: bool):
    """根据 --verbose 决定日志级别。"""
    level = logging.DEBUG if verbose else logging.INFO
    logging.basicConfig(
        level=level,
        format="%(asctime)s [%(levelname)s] %(message)s",
    )


def parse_args():
    parser = argparse.ArgumentParser(description="博客分析工具")
    parser.add_argument("input", type=Path, help="输入 JSON 文件")
    parser.add_argument("--top", type=int, default=10, help="显示前 N 篇")
    parser.add_argument("--author", type=str, help="只看某个作者")
    parser.add_argument("-v", "--verbose", action="store_true", help="详细输出")
    return parser.parse_args()


def main():
    args = parse_args()
    setup_logging(args.verbose)
    
    logger.debug("参数: %s", args)
    
    if not args.input.exists():
        logger.error("文件不存在: %s", args.input)
        sys.exit(1)
    
    logger.info("开始处理 %s", args.input)
    # ... 实际业务逻辑 ...
    logger.info("完成")


if __name__ == "__main__":
    main()

这就是 Python 脚本的"工业化外壳"——99% 的命令行工具都长这样。记住这个骨架。

3. Maybe Useful 旁注

旁注 1:argparse 的对手**:argparse 是标准库唯一选择,但生态里有 click、typer 这些第三方库,API 更优雅(用装饰器声明命令)。typer 用类型注解自动生成参数,FastAPI 一脉相承。学会 argparse 就够,但听过 click/typer 的名字。

旁注 2:logging 的设计很复杂**:完整的 logging 有 logger、handler、filter、formatter 四层抽象,可以做到"DEBUG 级别写文件,ERROR 级别发邮件,INFO 级别推 Slack"。今天讲的是基础,复杂场景查文档。

旁注 3:% 格式化在 logging 里不是历史遗留**:是有意设计——logger 接收"格式串 + 参数",只在真要输出时才拼接。这是 Python 内建唯一明显推荐用 % 的地方。

旁注 4:breakpoint() 的环境变量**:可以通过 PYTHONBREAKPOINT=ipdb.set_trace 让 breakpoint() 用更好用的 ipdb 替代 pdb(要先 pip install ipdb)。PYTHONBREAKPOINT=0 能在生产环境禁用所有断点,避免误开发的断点。

旁注 5:面试高频题:

  • "print 和 logging 的区别?" → 必答
  • "怎么调试 Python 程序?" → 提到 breakpoint() / pdb 加分
  • "exit code 0 和非 0 的区别?" → Unix 基础
  • "logging 的 5 个级别什么时候用?" → 工程素养

旁注 6:现代调试工具:VSCode、PyCharm 都内置了图形化调试器,比 pdb 友好。但 pdb 是 SSH 远程调试服务器时的唯一选择——所以还是要会。

4. 代码实践

把前几天的 blog_analytics.py 改造成完整命令行工具:

"""
blog_cli.py - 博客分析命令行工具(工业化版)
用法:
    python blog_cli.py data.json --top 5
    python blog_cli.py data.json --author Alice -v
    python blog_cli.py --help
"""
import argparse
import json
import logging
import sys
from collections import Counter, defaultdict
from pathlib import Path
import heapq

logger = logging.getLogger(__name__)


def setup_logging(verbose: bool):
    """配置日志输出。"""
    level = logging.DEBUG if verbose else logging.INFO
    logging.basicConfig(
        level=level,
        format="%(asctime)s [%(levelname)s] %(message)s",
        datefmt="%H:%M:%S",
    )


def load_posts(path: Path) -> list[dict]:
    """加载文章数据,错误时退出。"""
    if not path.exists():
        logger.error("文件不存在: %s", path)
        sys.exit(1)
    
    try:
        with open(path, encoding="utf-8") as f:
            posts = json.load(f)
    except json.JSONDecodeError as e:
        logger.exception("JSON 解析失败")    # 自动带堆栈
        sys.exit(2)
    
    logger.info("加载了 %d 篇文章", len(posts))
    return posts


def analyze_top(posts: list[dict], n: int, author: str | None):
    """按阅读量排序,打印 top n。"""
    if author:
        posts = [p for p in posts if p["author"] == author]
        logger.debug("过滤后剩 %d 篇", len(posts))
    
    if not posts:
        logger.warning("没有匹配的文章")
        return
    
    top = heapq.nlargest(n, posts, key=lambda p: p["pv"])
    
    print(f"\n阅读量 Top {n}:")
    for i, p in enumerate(top, 1):
        print(f"  {i}. {p['title']:<20} {p['pv']:>6} ({p['author']})")


def analyze_tags(posts: list[dict]):
    """标签统计。"""
    counter = Counter()
    for p in posts:
        counter.update(p.get("tags", []))
    
    print("\n标签热度:")
    for tag, count in counter.most_common():
        print(f"  {tag}: {count}")


def parse_args():
    parser = argparse.ArgumentParser(
        description="博客文章分析工具",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,    # 帮助文档显示默认值
    )
    parser.add_argument("input", type=Path, help="文章 JSON 文件路径")
    
    subparsers = parser.add_subparsers(dest="command", required=True)
    
    # 子命令 top
    top_p = subparsers.add_parser("top", help="按阅读量排序")
    top_p.add_argument("-n", type=int, default=10, help="显示前 N 篇")
    top_p.add_argument("--author", type=str, help="过滤作者")
    
    # 子命令 tags
    subparsers.add_parser("tags", help="标签统计")
    
    parser.add_argument("-v", "--verbose", action="store_true", help="详细输出")
    
    return parser.parse_args()


def main():
    args = parse_args()
    setup_logging(args.verbose)
    
    logger.debug("解析后的参数: %s", args)
    
    posts = load_posts(args.input)
    
    if args.command == "top":
        analyze_top(posts, args.n, args.author)
    elif args.command == "tags":
        analyze_tags(posts)


if __name__ == "__main__":
    main()

用法演示

准备测试数据 data.json:

[
  {"title": "Python 入门", "author": "Alice", "tags": ["python"], "pv": 1500},
  {"title": "Web 开发", "author": "Bob", "tags": ["python", "web"], "pv": 5000}
]

跑起来:

$ python blog_cli.py data.json top -n 5
10:30:00 [INFO] 加载了 2 篇文章

阅读量 Top 5:
  1. Web 开发              5000 (Bob)
  2. Python 入门            1500 (Alice)

$ python blog_cli.py data.json top --author Alice
$ python blog_cli.py data.json tags
$ python blog_cli.py data.json top -v       # 显示 DEBUG 日志
$ python blog_cli.py --help                  # 自动生成帮助

5. 练习题

理论题

  1. print 和 logger.info 的区别?哪些场景该用哪个?
  2. logging 的 5 个级别分别什么时候用?举一个实际例子。
  3. 子命令模式(add_subparsers)和普通参数有什么区别?什么时候用?
  4. logger.error(str(e)) 和 logger.exception("出错") 的区别?
  5. 为什么要区分 stdout 和 stderr?

代码实操题

题目 A:扩展上面的 blog_cli.py——加一个 --output 参数,把结果保存为 JSON 文件,而不是 print 到终端。

提示:argparse 加一个 --output 参数(默认 None),如果指定了就用 json.dump 写文件。

题目 B:写一个装饰器 @log_calls,自动记录被装饰函数的调用:参数、返回值、耗时。用 logger.debug 打到日志。

提示:综合 Day 2 装饰器 + Day 3 性能计时 + Day 5 logging。

题目 C:把 Day 2 写的 blog_stats.py 改造成命令行工具——配置文件路径、输出路径、是否显示进度,全部通过命令行参数控制。

提示:add_argument("--config", default="config.toml") 之类的。

思考题

你的脚本会处理大文件,跑半小时。中途 Ctrl+C 中断、或者出错——你怎么知道到哪一步崩了?日志要怎么打才能事后定位?如果生产环境每天跑一次,日志怎么管理(不能让一个文件无限大)?

6. 当天总结

今天学了什么:

四个模块对应工程化的四个方面:

  • argparse —— 命令行接口(CLI)
  • logging —— 日志输出
  • sys —— 解释器接口(argv / exit / stderr)
  • pdb —— 交互式调试

最重要的概念:

  1. **生产代码不要 ****print**——除非是给用户看的最终输出,否则用 logger
  2. logging 五个级别——DEBUG / INFO / WARNING / ERROR / CRITICAL,要会判断
  3. argparse 标准模板——99% 的脚本都长那个样
  4. **breakpoint()**** 是调试入口**——一定要会
  5. 退出码约定——0 成功,非 0 失败

需要反复练习:

  • argparse 的三种参数类型
  • logging 的 logger 和级别
  • pdb 的几个命令(n / s / c / p / q)

和明天的衔接:

明天 Day 6 讲并发——threading / multiprocessing / concurrent.futures / asyncio / subprocess。具体衔接点:

  • 今天的脚本是单线程,处理大文件慢——明天会讲怎么并行
  • subprocess 启动子进程,能拿到 stdout/stderr 处理(今天讲了 stdout/stderr,明天会用上)
  • 并发场景下 logging 的线程安全问题