atexit.register 看上去只是「程序结束时调用一下」。但它到底在什么时刻跑、跑几个、出错了怎么办、线程算不算数——这篇用六个小脚本一个一个跑出来。
下面每一节对应一个脚本(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 那一行。所以别指望在回调里「追加一个更晚的清理」。
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 秒才会打印那一行,这行始终没出现:进程结束时它被直接丢弃,它手里的清理逻辑(关文件、刷缓冲)也一并没有机会执行。
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() 是带下划线的内部接口,这里只用来观察数量。)
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、threadingatexit-shutdown)