sys.excepthook 是python唯一可靠的全局异常入口,仅处理主线程未捕获异常;threading.excepthook自3.8起支持但需手动注册且对守护线程无效;钩子须早注册、三参数、防自身崩溃,并避免日志竞态与耗时操作。

sys.excepthook 是唯一可靠的全局异常入口
Python 中没有“全局 try-catch”,sys.excepthook 是程序未捕获异常(即逃逸到顶层的异常)的最终处理点。它只对主线程生效,且不会干扰已有 try/except 逻辑 —— 换句话说,你写的 except 还是照常工作,只有漏掉的才会流到这里。
常见错误是试图用 threading.excepthook 覆盖所有线程,但 Python 3.8+ 才支持,且子线程默认不继承主钩子;更稳妥的做法是显式为每个线程设置钩子,或统一用 concurrent.futures 管理任务。
- 必须在程序启动早期注册,比如
if __name__ == "__main__":块开头 - 函数签名固定为
func(type, value, traceback),三参数缺一不可 - 若想保留默认行为(如打印到 stderr),需在自定义逻辑末尾手动调用
sys.__excepthook__(type, value, traceback)
记录异常时别直接 print 或 logging.error
直接用 print() 或裸调 logging.error() 在多线程/多进程场景下容易丢日志、混行、甚至死锁。根本原因是:异常可能发生在任意线程,而 logging 的 handler(尤其是 FileHandler)不是线程安全的,且默认格式化会触发 traceback.format_exception(),该函数在极少数情况下会因对象状态异常而二次崩溃。
- 用
logging.getLogger().critical()替代print(),并确保 logger 已配置好handlers和propagate=False - 避免在钩子里做耗时操作(如网络上报、磁盘 flush),可先写入队列,由单独线程消费
- 务必捕获钩子自身异常,否则会导致静默失败:整个钩子函数应包在
try...except BaseException:内
threading.excepthook 需要显式启用且版本敏感
Python 3.8 引入了 threading.excepthook,但它默认不生效 —— 必须手动赋值给 threading.excepthook,且仅对“未设置 daemon 或未被 join()”的线程抛出的异常有效。很多用户误以为设了就万事大吉,结果发现线程崩溃没日志,其实是线程提前退出或被守护了。
- 注册方式:
threading.excepthook = my_excepthook,函数签名与sys.excepthook相同 - 对于
ThreadPoolExecutor启动的任务,异常会被吞掉,需配合future.exception()主动检查 - Python threading.settrace() + 全局异常监控,但开销大,不推荐生产环境
异步任务(asyncio)必须单独处理
sys.excepthook 对 asyncio 无效。协程里未 await 的异常、未被 asyncio.create_task() 捕获的异常,会进 asyncio.get_event_loop().set_exception_handler(),而不是系统钩子。
- 必须调用
loop.set_exception_handler(my_async_handler),handler 接收loop、context两个参数 -
context字典里可能有"exception"(异常对象)、"message"(字符串)、"task"(Task 实例),优先取context.get("exception") - 若使用
asyncio.run(),需在它内部手动获取 loop 并设置;若用自建 loop,注册时机要在run_forever()前
真正麻烦的是混合场景:一个线程里跑 asyncio loop,另一个线程里跑普通同步代码 —— 这时得同时维护三套钩子:sys、threading、asyncio,且各自作用域不能重叠。最容易被忽略的是信号(如 SIGSEGV)和 C 扩展崩溃,它们根本不会触发任何 Python 层钩子,只能靠外部监控或 faulthandler.enable() 捕获。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











