
第六章 日志体系:logging 让排错效率翻倍
-
- [0. 先把话说清楚:日志不是"多打几行字"](#0. 先把话说清楚:日志不是“多打几行字”)
- [1. 为什么 print 调试会让你越做越慢](#1. 为什么 print 调试会让你越做越慢)
-
- [1.1 print 的三大致命缺陷](#1.1 print 的三大致命缺陷)
- [2. 先给你一个"可交付日志体系"的标准答案](#2. 先给你一个“可交付日志体系”的标准答案)
- [3. logging 的正确姿势:别在每个文件里乱配](#3. logging 的正确姿势:别在每个文件里乱配)
- [4. 落地实现:一套可直接复制的 logging 模板](#4. 落地实现:一套可直接复制的 logging 模板)
-
- [4.1 `src/myproj/core/logging.py`](#4.1
src/myproj/core/logging.py)
- [4.1 `src/myproj/core/logging.py`](#4.1
- [5. 在模块里写日志:三句话就够](#5. 在模块里写日志:三句话就够)
-
- [5.1 模块写法模板](#5.1 模块写法模板)
- [5.2 写日志的"信息密度原则"](#5.2 写日志的“信息密度原则”)
- [6. 数据工程场景的"必打日志点"(建议照抄)](#6. 数据工程场景的“必打日志点”(建议照抄))
-
- [6.1 数据读取阶段](#6.1 数据读取阶段)
- [6.2 清洗阶段](#6.2 清洗阶段)
- [6.3 特征/训练阶段](#6.3 特征/训练阶段)
- [6.4 导出/报告阶段](#6.4 导出/报告阶段)
- [7. 异常处理:让日志成为"可解释的失败"](#7. 异常处理:让日志成为“可解释的失败”)
-
- [7.1 正确姿势:捕获异常 + logger.exception](#7.1 正确姿势:捕获异常 + logger.exception)
- [8. 日志与 runs:把日志变成"可复现证据链"的一部分](#8. 日志与 runs:把日志变成“可复现证据链”的一部分)
- [9. 常见坑:你踩一次就会记住,但最好别踩](#9. 常见坑:你踩一次就会记住,但最好别踩)
-
- [坑 1:重复 handler 导致日志输出两遍/三遍](#坑 1:重复 handler 导致日志输出两遍/三遍)
- [坑 2:日志太多,控制台刷屏](#坑 2:日志太多,控制台刷屏)
- [坑 3:日志没有模块名/行号,定位困难](#坑 3:日志没有模块名/行号,定位困难)
- [坑 4:只记日志,不记参数与配置快照](#坑 4:只记日志,不记参数与配置快照)
- [10. 本章最低交付(MDR):你必须交付什么?](#10. 本章最低交付(MDR):你必须交付什么?)
- [11. 小结:日志体系让你从"调试"进入"运维视角"](#11. 小结:日志体系让你从“调试”进入“运维视角”)
(从"print 调试"升级到"可追溯、可复现、可运维"的工程日志)
你一定经历过这种绝望时刻:
- 代码在你电脑上能跑,换个数据就崩
- 报错信息只给你一句
KeyError或NoneType - 你开始疯狂插
print():
print(df.shape)、print(df.head())、print(x) - 终于跑通了,但两天后问题复现,你又得重新插一遍
这不是你不够努力,而是你的项目缺一个"工程系统":日志体系。
在真实交付里,日志不是"锦上添花",而是你能否排错、能否复现、能否验收的关键证据链。
尤其是数据处理与 AI 工程场景:数据漂移、依赖变化、参数变化、外部接口波动......没有日志,你根本不知道问题发生在哪一步。
本章我们只做一件事:
把你的项目从 print 调试升级为 logging 日志体系,让排错效率翻倍。
0. 先把话说清楚:日志不是"多打几行字"
很多人对日志的误解是:"我多输出一些信息就行"。
真正的日志体系至少要解决三件事:
- 可定位:问题发生在哪个阶段、哪一条数据、哪个参数
- 可追溯:同一次运行的参数、环境、关键产物能对应起来
- 可验收:交付时,日志是"证据链",能证明你做了什么、怎么做的
你可以把日志理解为项目的"黑匣子"。出事时靠它还原现场。
1. 为什么 print 调试会让你越做越慢
1.1 print 的三大致命缺陷
- 不可分级:所有输出一锅端,想看关键点很难
- 不可追踪:没有时间戳、模块名、行号,无法快速定位
- 不可收敛:项目一大,你会插满 print,删也不是,不删也不是
而 logging 天然解决这些问题:
- 有等级(DEBUG/INFO/WARNING/ERROR)
- 有格式(时间、模块、函数、行号)
- 可写文件(runs/log.txt 形成证据链)
- 可统一管理(全项目一套规范)
2. 先给你一个"可交付日志体系"的标准答案
这一章你最终要达到的效果是:
- 每次运行生成一个
runs/<timestamp>/log.txt - 终端输出 INFO(简洁可读)
- 文件记录 DEBUG(详细可追溯)
- 错误发生时,日志能告诉你:
哪一步、什么输入、什么配置、什么异常栈
用 Mermaid 画出来就是这样:
启动脚本/CLI
加载配置 configs + env
初始化日志 logging
运行 ETL/训练/评估
关键节点写 INFO/DEBUG
异常写 ERROR + stacktrace
runs//log.txt
可追溯复现/可验收交付
3. logging 的正确姿势:别在每个文件里乱配
新手最常见的错误是:
在每个 .py 文件里写一套 basicConfig(),结果日志重复输出、格式混乱、级别不受控。
正确做法是:
全项目只在一个地方初始化日志 ,其余模块只负责 logger = logging.getLogger(__name__)。
推荐放在:
src/myproj/core/logging.py
4. 落地实现:一套可直接复制的 logging 模板
下面这段代码我建议你直接作为项目模板使用。它满足:
- 控制台输出:简洁 INFO
- 文件输出:详细 DEBUG
- 自动写入
runs/<ts>/log.txt - 支持日志轮转(可选)
- 避免重复 handler(常见坑)
4.1 src/myproj/core/logging.py
python
from __future__ import annotations
import logging
from pathlib import Path
from typing import Optional
def setup_logging(
run_dir: str | Path,
level: str = "INFO",
console_level: str = "INFO",
file_level: str = "DEBUG",
log_name: str = "log.txt",
) -> None:
"""
初始化项目日志体系:
- 控制台输出:默认 INFO
- 文件输出:默认 DEBUG,写入 runs/<ts>/log.txt
"""
run_dir = Path(run_dir)
run_dir.mkdir(parents=True, exist_ok=True)
log_path = run_dir / log_name
# 根 logger
logger = logging.getLogger()
logger.setLevel(logging.DEBUG) # 根级别设为最低,让 handler 控制输出级别
# 防止重复添加 handler(Notebook/重复运行时很常见)
if logger.handlers:
logger.handlers.clear()
# 统一格式
fmt_console = logging.Formatter(
fmt="%(asctime)s | %(levelname)s | %(message)s",
datefmt="%H:%M:%S",
)
fmt_file = logging.Formatter(
fmt="%(asctime)s | %(levelname)s | %(name)s:%(lineno)d | %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
# 控制台 handler
ch = logging.StreamHandler()
ch.setLevel(getattr(logging, console_level.upper(), logging.INFO))
ch.setFormatter(fmt_console)
# 文件 handler
fh = logging.FileHandler(log_path, encoding="utf-8")
fh.setLevel(getattr(logging, file_level.upper(), logging.DEBUG))
fh.setFormatter(fmt_file)
logger.addHandler(ch)
logger.addHandler(fh)
# 记录初始化信息(非常重要,便于验收)
logger.info("Logging initialized.")
logger.info(f"Log file: {log_path}")
你只需要在入口脚本(scripts/CLI)里调用一次 setup_logging(),全项目就统一了。
5. 在模块里写日志:三句话就够
5.1 模块写法模板
python
import logging
logger = logging.getLogger(__name__)
def clean_df(df):
logger.info("Start cleaning dataframe.")
logger.debug(f"Input shape: {df.shape}")
# ...
return df
5.2 写日志的"信息密度原则"
日志不是写作文,关键在"能定位问题"。我建议你遵循这个顺序:
- INFO:阶段性节点(开始/结束/产物路径/关键指标)
- DEBUG:细节(shape、列名、缺失率、参数)
- WARNING:非致命异常(缺列但已回退、某些行被丢弃)
- ERROR:致命异常(直接失败,必须带栈)
6. 数据工程场景的"必打日志点"(建议照抄)
很多人不知道日志该写什么。这里给你一份"数据项目必打点"清单:
6.1 数据读取阶段
- 文件路径、文件大小(可选)
- 编码、分隔符、sheet 名(Excel)
- 读取后 shape、列名
6.2 清洗阶段
- 缺失值比例(全局 + 关键列)
- 去重前后行数
- 异常值处理策略(clip/remove)与处理数量
6.3 特征/训练阶段
- seed、数据划分比例
- 训练样本数/验证样本数
- 关键指标(AUC/F1/RMSE)
- 模型保存路径
6.4 导出/报告阶段
- 输出文件路径
- 产物数量(图表、表格)
- runs 目录完整性检查
Read
Clean
Feature
Train
Eval
Export
runs/log.txt + metrics.json + artifacts
7. 异常处理:让日志成为"可解释的失败"
很多项目失败时只有一句"报错了"。
交付型项目要求你做到:失败也能解释。
7.1 正确姿势:捕获异常 + logger.exception
python
import logging
logger = logging.getLogger(__name__)
try:
# 可能失败的逻辑
...
except Exception:
logger.exception("Pipeline failed with unexpected error.")
raise
logger.exception() 会自动把 stacktrace 写到日志里,这是排错效率翻倍的关键。
8. 日志与 runs:把日志变成"可复现证据链"的一部分
你在上一章已经做了 runs/<ts>/config_snapshot.json。
这章你要补齐:runs/<ts>/log.txt。
一套最小可验收的 runs 目录应该像这样:
text
runs/2026-01-10_1030/
config_snapshot.json
log.txt
metrics.json
artifacts/
当你把这些东西交给别人,对方不仅能复现,还能审计你的过程。
这就是"工程化科研/AI 项目"和"随缘 Notebook"的本质区别。
9. 常见坑:你踩一次就会记住,但最好别踩
坑 1:重复 handler 导致日志输出两遍/三遍
解决:初始化时 if logger.handlers: logger.handlers.clear()
坑 2:日志太多,控制台刷屏
解决:控制台 INFO,文件 DEBUG(分级输出)
坑 3:日志没有模块名/行号,定位困难
解决:文件日志格式加 %(name)s:%(lineno)d
坑 4:只记日志,不记参数与配置快照
解决:日志 + config_snapshot 必须成对出现
10. 本章最低交付(MDR):你必须交付什么?
按照专栏学习协议,本章完成后你至少交付:
src/myproj/core/logging.py:统一日志初始化- 任意入口脚本/CLI:调用
setup_logging(run_dir=...) - 任意核心模块:至少 5 条有效日志(INFO/DEBUG/WARNING 各至少一条)
- 一次真实运行:生成
runs/<ts>/log.txt - README 增加 "Logs" 小节:告诉别人日志在哪里、怎么看
你做到这 5 条,你的项目排错效率会非常明显地提升。
11. 小结:日志体系让你从"调试"进入"运维视角"
当你掌握 logging,你会突然理解为什么工程师排错那么快:
因为他们不是在"猜",而是在读证据链。
日志体系带来的不是"多输出一点信息",而是:
- 可定位(哪一步出错)
- 可追溯(这次运行用的什么参数)
- 可复现(别人能按日志复原现场)
- 可验收(交付有证据、有过程、有产物)
下一章我们会进入更"硬核"的效率工具:断点调试(VSCode Debug),让你在运行时直接进入变量现场,把排错效率再提升一个量级。
如果你愿意,把你现在项目里最常出现的 3 类报错(例如 KeyError、文件路径、类型不匹配)发我,我可以按本章的日志打法给你一份"日志点位设计清单",告诉你这些错误应该在哪些节点打哪些日志,做到一次定位。