11 · 异常处理与日志

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:捕获并处理具体类型的异常。
  • elsetry没出错才执行(常用于「成功后的收尾」)。
  • 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 ValueErrorexcept Exception 更精准,不会误伤其他错误。

3. logging:专业的日志模块

3.1 为什么不用 print 调试

print 调试有三大问题:

  1. 分不清级别:所有输出混在一起,看不出哪些是错误、哪些是普通信息。
  2. 不方便开关 :上线后想关掉调试输出,得到处删 print
  3. 无处可查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. 常见错误与避坑

  1. except::吞掉所有异常,掩盖 bug。应捕获具体类型。
  2. except 顺序写反except Exception 放在 except ValueError 前面会把后者「截胡」。要从具体到宽泛排列。
  3. finallyreturn :会覆盖 try/except 的返回值,造成诡异行为。
  4. 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. 练习与思考

  1. 写一个 safe_divide(a, b) 函数,除零时捕获 ZeroDivisionError 并返回 None,同时记录一条 WARNING 日志。
  2. 定义一个 InvalidAgeError 异常,用于 set_age() 里年龄为负数时抛出。
  3. 把第 2 篇的某个脚本加上 logging,把关键步骤记录下来。
  4. 思考:为什么 else 块只在「没异常」时执行?它和「把代码写在 try 末尾」有什么区别?

💡 提示 :第 4 题的关键在于------else 里的代码不会被 tryexcept 误捕获,语义更清晰。

8. 延伸阅读


📚 本系列导航

如果觉得有用,欢迎收藏、点赞、转发,让更多人少走弯路。

相关推荐
auto_go1 小时前
Python 实战指南(7)——账本动不动就崩?先把“魔法字符串”和“裸报错”干掉
python
Beyond_System|系统之外1 小时前
【学编程】Python基础编程题100道(21-60)
开发语言·python·算法
根目录下的猫2 小时前
RK3588适配的轻量级AI模型推荐
人工智能·后端·python·目标检测
wno7042 小时前
Spring Security权限控制
java·python·spring
ShineWinsu3 小时前
对于Coze—AI:SDK的解析
人工智能·python·ai·sdk·项目·coze·字节跳动
右耳朵猫AI3 小时前
Python周刊2026W38 | 标准流编码修复、PEP 845/846 草案、解析器提速 10%、集合字典二次复杂度
python·ai·数据科学
微软技术分享3 小时前
大模型网络结构:模型接口测试用例参考
python
Bruce_Liuxiaowei4 小时前
Python 实例赋值遮蔽类属性:一个从不报错的静默陷阱
开发语言·python·语法糖
做运维的阿瑞4 小时前
Python 标准库汇总:分类速览与常用模块清单
linux·运维·python