完整讲解 · 5 段教学·配 4 道练习题·预计 30 分钟
本页是本章的通读版,可直接读完全部讲解。想动手写代码、跑判分,去 闯关模式。
各位写代码时,是不是几乎都干过这种事——
代码跑出问题了,怎么办?随手在可疑位置插一行 print('xxx'),再跑一次。然后再插一行 print('yyy'),再跑。最后整个文件里散落着十几个 print('111')、print('here')、print(user),bug 修好之后忘了删。
本地玩玩还能凑合,可一旦把代码部署到服务器,问题就来了:
「线上挂了,咋回事?」打开服务器一看,啥都没有。
Python 标准库里早就备好了答案——logging。一行不长的代码,就能让输出自带「时间戳 + 级别 + 模块名」,还能同时写到控制台、文件、远程服务器,并且可以一键切换「调试时啥都打、上线后只打 ERROR」。
这一节就从最简单的 logging.info('hi') 开始。
最简单的写法只有两行:
import logging
logging.info("两点水开始打卡了")跑一下——啥都没输出。
这是新手第一次用 logging 几乎都会踩的坑。原因是:logging 默认级别是 WARNING,比它低的 INFO 和 DEBUG 直接被丢掉了。
把上面那行换成 logging.warning(...) 再跑:
import logging
logging.warning("两点水忘了打卡")终于看到输出:
WARNING:root:两点水忘了打卡
注意这一行的格式:级别 + logger 名字 + 内容,三段式。root 是默认 logger 的名字。
要让 INFO 也打出来,先调一行 basicConfig:
import logging
logging.basicConfig(level=logging.INFO)
logging.info("两点水开始打卡了")输出:
INFO:root:两点水开始打卡了
basicConfig(level=logging.INFO) 把 root logger 的级别调成 INFO——INFO 及以上都会被打出来。
⚠️ 「为什么默认是 WARNING?」官方解释:
logging是给「库的作者」准备的,库要是默认就打一堆 INFO 会污染最终用户的输出。所以默认只打「值得用户注意」的 WARNING 及以上。
logging 一共有 5 个内置级别,从低到高:
| 级别 | 数值 | 含义 | 啥时候用 |
|---|---|---|---|
DEBUG | 10 | 调试细节 | 开发期间打变量、看流程 |
INFO | 20 | 关键流程 | 服务启动、请求进来、任务完成 |
WARNING | 30 | 警告但不影响运行 | 配置文件用了默认值、降级了 |
ERROR | 40 | 错误,部分功能挂了 | 抓到异常、外部接口超时 |
CRITICAL | 50 | 致命,整个程序得停 | 数据库连不上、关键依赖不可用 |
数值是给「过滤」用的——级别 = 30 时,所有 < 30 的日志都被丢掉,只保留 30、40、50。
「WARNING 和 ERROR 怎么区分?」一个简单的标准——这一次操作还能完成吗?能完成就 WARNING,不能完成就 ERROR。
import logging
logging.basicConfig(level=logging.DEBUG)
logging.debug("调试看的,临时变量")
logging.info("正常流程的关键节点")
logging.warning("不对劲但能继续")
logging.error("这次操作失败了")
logging.critical("整个程序要挂")跑一下,五条全部打出来。
basicConfig 能配的东西不止 level,还有 format 控制每条日志的样子:
import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
)
logging.info("两点水准备打卡")
logging.warning("打卡机故障")输出:
2026-04-28 09:00:00 - INFO - 两点水准备打卡
2026-04-28 09:00:00 - WARNING - 打卡机故障
是不是比 print 看着舒服多了?时间戳、级别、消息全有了。
| 占位符 | 含义 |
|---|---|
%(asctime)s | 时间戳(用 datefmt 控制格式) |
%(name)s | logger 名字(默认 root,或 getLogger(name) 指定的) |
%(levelname)s | 级别名(DEBUG / INFO / ...) |
%(message)s | 实际打的内容 |
%(module)s | 调用所在的模块名 |
%(funcName)s | 调用所在的函数名 |
%(lineno)d | 调用所在的行号 |
把它们组合起来,能拼出非常详细的日志:
format='%(asctime)s [%(levelname)s] %(name)s:%(lineno)d - %(message)s'⚠️
basicConfig只在「root logger 还没配过 handler」时才会生效,第二次调用不起作用。所以整个程序里只调一次,通常放在程序入口处。如果想强制覆盖(比如调试时反复调),加
force=True:python到闯关页运行这段 →logging.basicConfig(level=logging.INFO, force=True)
新手最常见的反模式是直接 import logging 之后到处 logging.info(...)、logging.error(...)。这其实是用了 root logger——所有模块共用同一个 logger,分不清日志是哪儿来的。
正经做法:每个模块开一个独立的 logger:
import logging
logger = logging.getLogger(__name__)
def punch_in(user):
logger.info("用户 %s 开始打卡", user)__name__ 是 Python 内置的变量。在 mymodule.py 里 __name__ == 'mymodule',在包 app/services/punch.py 里 __name__ == 'app.services.punch'。这个名字会出现在每条日志里,一眼就能看出日志是哪个模块打的。
app.services 整组打 DEBUG、app.api 只打 WARNINGapp.services.punch 找不到配置时会去查 app.services,再查 app,再查 rootimport logging
logging.basicConfig(
level=logging.INFO,
format='%(name)s - %(levelname)s - %(message)s',
)
logger = logging.getLogger('app.punch')
logger.info("用户开始打卡")
logger.warning("迟到了")输出:
app.punch - INFO - 用户开始打卡
app.punch - WARNING - 迟到了
写错误日志最容易踩的坑——只 logger.error('xxx') 不带异常信息:
try:
1 / 0
except Exception as e:
logging.error('出错了') # ❌ 完全看不出错在哪正确写法是 logger.exception(),它会自动把当前 traceback 带进去:
try:
1 / 0
except Exception as e:
logging.exception('出错了') # ✓ 自动附上完整 traceback输出:
ERROR:root:出错了
Traceback (most recent call last):
File "main.py", line 4, in <module>
1 / 0
ZeroDivisionError: division by zero
写日志时最有用的一条建议:在
except里永远用logger.exception(),不要用logger.error(str(e))。
读完了?动手练一遍才算真会。
去闯关模式练习 →