python中需用sys.excepthook捕获未处理异常,配合atexit做尽力清理、faulthandler捕获c级崩溃,并注意降级逻辑中对象生命周期风险。

Python中用sys.excepthook捕获未处理异常
当程序抛出未被try/except捕获的异常(比如主线程崩溃、信号中断、C扩展段错误后Python层残留异常),默认行为是打印 traceback 并退出。想在此刻执行清理或降级逻辑,必须替换sys.excepthook——它只在「异常彻底无人接手」时触发,不是所有错误都走这条路。
常见误区是以为它能捕获KeyboardInterrupt或SystemExit,其实不会:这两个是BaseException子类,被解释器特殊处理,excepthook默认忽略它们。如需响应Ctrl+C,得单独注册signal.signal(signal.SIGINT, ...)。
实操建议:
- 在程序启动早期(如
if __name__ == "__main__":开头)就设置sys.excepthook,避免模块导入期间出错漏捕获 - 函数签名必须为
func(type, value, traceback),三参数缺一不可,否则会引发新异常导致进程直接终止 - 降级逻辑里避免再抛异常——可在内部用
try/except包住日志写入、网络上报等操作 - 调用完自定义逻辑后,显式调用
sys.__excepthook__(type, value, traceback)保留原始打印行为,方便调试
对关键资源使用atexit.register()做兜底清理
atexit注册的函数会在Python解释器正常退出(包括sys.exit()、主模块执行完毕)或收到致命错误后尝试运行,但它不保证100%执行——如果进程被os._exit()、kill -9或发生栈溢出/C段错误,atexit完全失效。
所以它适合做「尽力而为」的清理,比如关闭临时文件、释放锁文件、标记服务状态为离线,但不能依赖它完成数据持久化或事务回滚。
实操建议:
- 注册函数应尽量轻量,避免I/O阻塞或复杂计算,防止拖慢进程退出
- 不要在
atexit函数里调用logging.shutdown()后再用logging.info()——日志器可能已被销毁 - 若需区分退出原因(正常 vs 异常),可配合全局标志位:
is_crashing = False,在excepthook里设为True,atexit函数中据此跳过某些逻辑
用faulthandler.enable()捕获C级崩溃现场
Python的sys.excepthook对纯Python异常有效,但遇到C扩展导致的段错误(Segmentation fault)、总线错误(Bus error)或栈溢出,解释器可能直接终止,连hook都不调用。这时需要faulthandler——它是Python内置模块,专为这类信号设计。
启用后,当进程收到SIGSEGV、SIGBUS、SIGFPE、SIGABRT等信号时,会立即打印当前所有线程的Python堆栈(含C帧),然后终止。这个输出可重定向到文件,便于事后分析。
实操建议:
- 在程序最开头(甚至早于
logging.basicConfig())调用faulthandler.enable(),确保信号处理器尽早安装 - 生产环境建议写入文件:
faulthandler.enable(file=open('/var/log/myapp.fault', 'a')),避免输出混在stdout/stderr中丢失 - 注意它不替代
excepthook:两者互补,faulthandler抓底层信号,excepthook抓Python异常,都要配 - Windows下部分信号支持有限,
SIGSEGV可用,SIGBUS不可用
降级逻辑中避免依赖未初始化或已销毁的对象
致命错误发生时,对象生命周期处于不确定状态:模块可能只导入了一半,全局变量可能是None,单例实例可能尚未创建,或者GC已开始回收。此时若在excepthook或atexit里访问这些对象,会引发二次异常,导致进程静默退出,连错误日志都留不下。
实操建议:
- 所有降级函数内使用的变量、模块、函数,必须在注册前就完成初始化并赋值给局部作用域,例如:
_logger = logging.getLogger("fallback"),然后atexit.register(lambda: _logger.info("shutting down")) - 避免在降级逻辑中调用
import——导入过程本身可能失败;所需模块应在主流程开头导入完毕 - 对数据库连接、HTTP会话等长连接,优先调用
.close()而非.shutdown(),前者更轻量且兼容性更好 - 如果降级逻辑包含网络请求(如上报崩溃),务必设置超时:
requests.post(url, timeout=2),防止卡死
真正棘手的从来不是「怎么写降级代码」,而是「降级代码运行时,哪些东西还活着、哪些已经没了」——这没法靠文档保证,只能靠提前缓存引用、限制依赖范围、以及反复用kill -SEGV测试验证。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











