bigywater.tech
Python · 79

atexit:程序退出时哪些清理代码会执行

atexit.register 看上去只是「程序结束时调用一下」。但它到底在什么时刻跑、跑几个、出错了怎么办、线程算不算数——这篇用六个小脚本一个一个跑出来。

后进先出
3 个回调的执行顺序与注册顺序相反
0.61 s
atexit 等到非守护线程结束之后才开始
late 没跑
在退出阶段里新注册的回调不会执行

做的事:每个问题一个独立脚本

每次都在新的子进程里执行,输出是它们的原样标准输出 / 标准错误

下面每一节对应一个脚本(s1.py…s6.py),脚本内容在每节里给出。命令后面的「退出码」是 shell 拿到的进程退出码。

验证一:顺序是后进先出

s1.py import atexit def make(n): def f(*a, **k): print(f" handler {n} 执行, 参数={a} {k}") f.__name__ = f"h{n}"; return f atexit.register(make(1)) atexit.register(make(2), "x", k=1) @atexit.register def decorated(): print(" handler decorated 执行 (用装饰器注册)") print("main 结束, 注册顺序: h1, h2, decorated")
实测 · 本机执行输出
python3 s1.py
main 结束, 注册顺序: h1, h2, decorated
  handler decorated 执行 (用装饰器注册)
  handler 2 执行, 参数=('x',) {'k': 1}
  handler 1 执行, 参数=() {}
  (退出码 0)

注册顺序是 h1、h2、decorated,执行顺序正好相反。参数照常传入。这和 with/ExitStack 的「先进后出」逻辑一致:后注册的清理通常依赖先注册的资源,所以要先清理它。

验证二:一个回调抛异常,其它回调还会执行吗

s2.py import atexit @atexit.register def a(): print(" a 执行") @atexit.register def boom(): raise RuntimeError("清理函数里出错了") @atexit.register def c(): print(" c 执行 (最后注册, 最先执行)") print("main 结束")
实测 · 本机执行输出
python3 s2.py
main 结束
Exception ignored in atexit callback <function boom at 0x10827a770>:
Traceback (most recent call last):
  File "<实验目录>", line 5, in boom
    def boom(): raise RuntimeError("清理函数里出错了")
RuntimeError: 清理函数里出错了
  c 执行 (最后注册, 最先执行)
  a 执行
  (退出码 0)

会。异常被打印为 Exception ignored in atexit callback 加完整回溯,其余回调(c 和 a)都执行了,进程退出码仍然是 0。含义是:清理函数里的错误不会让整个进程「以失败结束」,监控如果靠退出码判断,是发现不了的——需要看日志。

验证三:在退出阶段再注册的回调不执行

s3.py import atexit def late(): print(" late: 在退出阶段里注册的 handler 执行了") @atexit.register def first(): print(" first 执行, 并在其中再注册 late") atexit.register(late) print("main 结束")
实测 · 本机执行输出
python3 s3.py
main 结束
  first 执行, 并在其中再注册 late
  (退出码 0)

first 在执行时又注册了 late,但输出里没有 late 那一行。所以别指望在回调里「追加一个更晚的清理」。

验证四:线程与 atexit 的先后

非守护线程先被等完,守护线程被静默丢弃
s4.py import atexit, threading, time T0 = time.time() def log(m): print(f" [{time.time()-T0:4.2f}s] {m}", flush=True) def nd(): time.sleep(0.6); log("非守护线程 干完活") def dm(): for i in range(30): time.sleep(0.1) log("守护线程 干完活 (如果看到这行说明它没被杀)") threading.Thread(target=nd).start() threading.Thread(target=dm, daemon=True).start() atexit.register(lambda: log("atexit handler 执行")) log("main 函数返回")
实测 · 本机执行输出
python3 s4.py
  [0.00s] main 函数返回
  [0.61s] 非守护线程 干完活
  [0.61s] atexit handler 执行
  (退出码 0)

main 函数在 0.00 s 就返回了,可是 atexit 回调直到 0.61 s 才执行——解释器先等非守护线程结束。守护线程要循环约 3 秒才会打印那一行,这行始终没出现:进程结束时它被直接丢弃,它手里的清理逻辑(关文件、刷缓冲)也一并没有机会执行。

验证五:unregister

s5.py import atexit def f(): print(" f 执行") def g(): print(" g 执行") atexit.register(f); atexit.register(f); atexit.register(g) print("注册后 handler 个数:", atexit._ncallbacks()) atexit.unregister(f) print("unregister(f) 之后个数:", atexit._ncallbacks(), "(f 注册了两次, 一次全部移除)")
实测 · 本机执行输出
python3 s5.py
注册后 handler 个数: 3
unregister(f) 之后个数: 1 (f 注册了两次, 一次全部移除)
  g 执行
  (退出码 0)

同一个函数注册了两次,unregister(f) 一次就把两次都移除了,最后只剩 g 执行。(atexit._ncallbacks() 是带下划线的内部接口,这里只用来观察数量。)

验证六:sys.exit 与 atexit

s6.py import atexit, os @atexit.register def h(): print(" atexit 执行 (sys.exit(3) 已经调用过)") import sys print("调用 sys.exit(3)"); sys.exit(3)
实测 · 本机执行输出
python3 s6.py
调用 sys.exit(3)
  atexit 执行 (sys.exit(3) 已经调用过)
  (退出码 3)

sys.exit(3) 之后回调照样执行,进程最终的退出码是 3——atexit 不会改变退出码。其他退出方式(异常、信号、os._exit)下 atexit 是否执行,见第 90 篇《Python 进程退出:正常返回、异常和强制终止有什么区别》。

分步演示:一次退出里,谁先谁后

每一步的内容都是上面脚本的真实输出
atexit 在退出流程里的位置
此刻的阶段 / 观察

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7,标准库 atexit、threading
    • 没有验证:多线程同时注册的线程安全性、子解释器里的 atexit、被信号杀死时的行为(见第 90 篇 atexit-shutdown)
    京ICP备2026067502号-1京公网安备11010502063487号