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:捕获并处理具体类型的异常。
  • 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 调试有三大问题:

  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. 在 finally 里 return :会覆盖 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 里的代码不会被 try 的 except 误捕获,语义更清晰。

8. 延伸阅读


📚 本系列导航

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

相关推荐
全栈练习生8 小时前
AI Agent 沙箱
python·ai
SEO_juper10 小时前
用 Python 写一个 GEO 可见性检查脚本:你的网站现在能被 AI 引用吗
开发语言·人工智能·爬虫·python·seo·外贸独立站
用户83562907805111 小时前
使用 Python 对 Excel 工作表进行排序
后端·python
for_ever_love__11 小时前
MySQL 全文索引实战:FULLTEXT、ngram 中文分词与 MATCH AGAINST 到底该怎么用
java·python·mysql·全文检索·分词·索引·ngram
用户83562907805111 小时前
使用 Python 合并 PowerPoint 演示文稿
后端·python
weixin_4617694011 小时前
VS Code 中创建 Jupyter 文件(.ipynb)
ide·python·jupyter
2601_9669496512 小时前
多市场量化策略的数据接口应该如何设计:从数据层架构到策略接入
开发语言·python·数据分析·pandas·量化交易·股票数据·quantdash
全栈弄潮儿12 小时前
Python实战第1期:Python环境搭建与第一个程序
python
心易行者12 小时前
Agent应用+API端点商业化进阶实战:从单体智能体到可付费调用的API全流程
运维·服务器·人工智能·python·apache
weixin1997010801612 小时前
《1688图片空间API踩坑:img.upload 与 album.* 的防盗链与CDN缓存问题》(附Python源码)
开发语言·python·缓存