Python 标准库 Day 5:命令行、日志与调试
1. 问题引入
继续博客项目。前几天写的 blog_analytics.py,现在你想做几件事:
- 让脚本支持
python blog_analytics.py --top 10 --author Alice这种命令行参数,而不是改源码 - 满屏的
print()调试语句删也不是、留也不是——线上要不要打日志?打到哪儿? - 程序莫名其妙出错,想"暂停在某一行看变量"
- 给脚本加上
--help自动生成帮助文档 - 区分"普通信息""警告""错误"——用户应该看到什么、运维应该看到什么
这些事让代码从"能跑的脚本"变成"能交付的工具"。今天讲四个模块,是工程化的第一步。
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. 练习题
理论题
print和logger.info的区别?哪些场景该用哪个?- logging 的 5 个级别分别什么时候用?举一个实际例子。
- 子命令模式(
add_subparsers)和普通参数有什么区别?什么时候用? logger.error(str(e))和logger.exception("出错")的区别?- 为什么要区分 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—— 交互式调试
最重要的概念:
- **生产代码不要 **
**print**——除非是给用户看的最终输出,否则用 logger - logging 五个级别——DEBUG / INFO / WARNING / ERROR / CRITICAL,要会判断
- argparse 标准模板——99% 的脚本都长那个样
**breakpoint()**** 是调试入口**——一定要会- 退出码约定——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 的线程安全问题