11 · 异常处理与日志
导语 :程序不会总按你设想的方式运行------文件可能不存在,用户可能输入了字母而不是数字,网络可能超时。写程序不是祈祷不出错,而是主动处理错误、留下可排查的痕迹。这一篇讲异常处理(try/except)和日志(logging),让你的程序健壮、可维护。
1. 为什么需要异常处理
先看一个「一碰就碎」的程序:
python
number = int(input("请输入一个数字:"))
print(f"你输入的是 {number}")
用户一旦输入 abc,程序立刻崩溃:
text
ValueError: invalid literal for int() with base 10: 'abc'
这个 ValueError 就是异常(exception)------程序运行时抛出的错误信号。不处理它,程序就终止。处理它,程序就能优雅地应对:
python
try:
number = int(input("请输入一个数字:"))
print(f"你输入的是 {number}")
except ValueError:
print("输入的不是数字,请重新运行。")
💡 转行速览 :
try/except对应 Java 的try/catch、JS 的try/catch,但 Python 的except可以按类型精准捕获,且异常处理风格更简洁。
2. 核心概念
2.1 try / except / else / finally
四个关键字各司其职:
python
def read_config(path):
try:
f = open(path, encoding="utf-8") # 可能抛 FileNotFoundError
except FileNotFoundError:
print(f"文件不存在:{path}")
else:
content = f.read() # 没有异常才执行
f.close()
return content
finally:
print("无论成功失败,都会执行到这里")
try:放「可能出错」的代码。except:捕获并处理具体类型的异常。else:try块没出错才执行(常用于「成功后的收尾」)。finally:无论是否异常都执行(常用于释放资源,如关文件、关连接)。
2.2 捕获具体异常 vs 裸 except
python
try:
result = 10 / 0
except ZeroDivisionError: # 只捕获「除零」这一种
print("不能除以零")
永远别用裸 except: ------它会吞掉所有异常,包括 KeyboardInterrupt(Ctrl+C)、SystemExit 等,掩盖真实 bug:
python
# 🚫 反面示例:裸 except 吞掉一切,错误无从排查
try:
do_something()
except:
pass
2.3 raise 与自定义异常
主动抛出异常用 raise;也可以自定义异常类:
python
class InsufficientFundsError(Exception):
"""余额不足的自定义异常。"""
pass
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFundsError(f"余额不足:需要 {amount},只有 {balance}")
return balance - amount
try:
withdraw(100, 200)
except InsufficientFundsError as e:
print(f"取款失败:{e}")
# 输出:取款失败:余额不足:需要 200,只有 100
自定义异常继承自 Exception,让你的业务错误和系统错误区分开,调用方可以精确捕获。
2.4 常见内置异常
| 异常 | 触发场景 |
|---|---|
ValueError |
值类型对但内容不对(如 int("abc")) |
KeyError |
字典访问不存在的键 |
TypeError |
类型不匹配(如 1 + "a") |
FileNotFoundError |
打开不存在的文件 |
IndexError |
列表下标越界 |
ZeroDivisionError |
除以零 |
捕获的异常越具体越好 ------except ValueError 比 except Exception 更精准,不会误伤其他错误。
3. logging:专业的日志模块
3.1 为什么不用 print 调试
print 调试有三大问题:
- 分不清级别:所有输出混在一起,看不出哪些是错误、哪些是普通信息。
- 不方便开关 :上线后想关掉调试输出,得到处删
print。 - 无处可查 :
print只到屏幕,程序跑在服务器上时,你根本看不到,也留不下记录。
logging 解决了全部问题:分级、可开关、可写文件、带时间戳。
3.2 级别与基本配置
python
import logging
# 配置:只显示 INFO 及以上级别,带时间戳和级别
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
logging.debug("调试信息,默认不显示")
logging.info("程序启动")
logging.warning("磁盘空间不足,请注意")
logging.error("连接数据库失败")
输出(示例):
text
2026-08-29 10:00:00 [INFO] 程序启动
2026-08-29 10:00:00 [WARNING] 磁盘空间不足,请注意
2026-08-29 10:00:00 [ERROR] 连接数据库失败
级别从低到高:DEBUG < INFO < WARNING < ERROR < CRITICAL。设置了 INFO 级别后,低于它的 DEBUG 就不会输出------这就是「开关」。
3.3 输出到文件
python
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
filename="app.log", # 输出到文件
filemode="a", # 追加模式
encoding="utf-8",
)
logging.info("这条日志会写进 app.log 文件")
加上 filename 参数,日志就落盘了。真实项目通常还会用 RotatingFileHandler 做日志轮转(按大小切分),避免单个文件无限增长。
4. 实战演练:健壮的文件读取 + 日志
需求:读一个可能不存在的配置文件,把「读不到」「读到了」都记录到日志,且程序不崩溃。
python
import logging
from pathlib import Path
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
def load_config(path: str) -> dict:
"""读取配置文件,返回配置字典;失败返回空字典。"""
config_path = Path(path)
try:
content = config_path.read_text(encoding="utf-8")
except FileNotFoundError:
logging.error("配置文件不存在:%s", path)
return {}
except PermissionError:
logging.error("没有权限读取配置文件:%s", path)
return {}
logging.info("成功读取配置文件:%s", path)
# 假设配置是 key=value 每行一条
config = {}
for line in content.splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
if "=" in line:
key, _, value = line.partition("=")
config[key.strip()] = value.strip()
return config
if __name__ == "__main__":
cfg = load_config("config.ini")
if cfg:
print(f"读取到 {len(cfg)} 项配置:{cfg}")
else:
print("使用默认配置继续运行")
运行结果(文件不存在时):
text
2026-08-29 10:00:00 [ERROR] 配置文件不存在:config.ini
使用默认配置继续运行
💡 提示 :
logging.error("配置文件不存在:%s", path)用%s占位符而不是 f-string,是官方推荐写法------它延迟字符串格式化,级别被过滤时不会白白拼接,性能更好。
5. 常见错误与避坑
- 裸
except::吞掉所有异常,掩盖 bug。应捕获具体类型。 except顺序写反 :except Exception放在except ValueError前面会把后者「截胡」。要从具体到宽泛排列。- 在
finally里return:会覆盖try/except的返回值,造成诡异行为。 - 用
print代替日志 :无法分级、无法开关、无法落盘,生产环境必须用logging。
🚫 避坑 :
except (A, B) as e一次捕获多个异常没问题,但别写except A, B:------这是 Python 2 的语法,Python 3 会报错。
6. 小结
try/except/else/finally四段式:捕获、成功收尾、必定执行。- 捕获越具体的异常越好,禁止裸
except。 raise主动抛异常,自定义异常继承Exception。logging分级(DEBUG→CRITICAL)、可写文件、带时间戳,全面优于print。
7. 练习与思考
- 写一个
safe_divide(a, b)函数,除零时捕获ZeroDivisionError并返回None,同时记录一条WARNING日志。 - 定义一个
InvalidAgeError异常,用于set_age()里年龄为负数时抛出。 - 把第 2 篇的某个脚本加上
logging,把关键步骤记录下来。 - 思考:为什么
else块只在「没异常」时执行?它和「把代码写在try末尾」有什么区别?
💡 提示 :第 4 题的关键在于------
else里的代码不会被try的except误捕获,语义更清晰。
8. 延伸阅读
- Python 官方文档:Errors and Exceptions
- Python 官方文档:logging HOWTO
- 文件读写(
pathlib):见本系列第 12 篇
📚 本系列导航
- 上一篇 :《10 · 面向对象编程:类、继承与魔术方法》
- 下一篇 :《12 · 文件 I/O 与序列化》
- 系列总览 :《Python 从入门到精通》系列目录
如果觉得有用,欢迎收藏、点赞、转发,让更多人少走弯路。