Python 3.14 标准库 sys.monitoring 全面详解#
一句话摘要:
sys.monitoring是 Python 3.12 起提供的低开销、事件驱动的运行时监控命名空间,它用「工具标识符 + 事件集合 + 回调」三要素让调试器、覆盖率工具、性能分析器等按需订阅执行事件,相比传统的sys.settrace/sys.setprofile在性能与可控粒度上都有本质提升。
1. 模块定位与用途#
sys.monitoring 并不是一个独立的模块,而是内嵌于 sys 模块内部的一个命名空间。这一点极其关键:import sys.monitoring 与 from sys.monitoring import events 都会抛出 ModuleNotFoundError。正确的使用方式永远是:
import sys
events = sys.monitoring.events # 先取命名空间,再使用其属性
该命名空间提供了激活和控制「执行事件监控」所需的函数与常量。当程序运行时会不断发生各类事件(函数调用、返回、行号推进、虚拟字节码指令执行、异常抛出等),sys.monitoring 允许工具在感兴趣的事件发生时收到回调。
监控 API 由三个组成部分构成:
组成部分 |
作用 |
|---|---|
工具标识符(Tool identifiers) |
一个 0~5 的整数及其关联名称,用于隔离不同工具,防止互相干扰 |
事件(Events) |
用 2 的幂整数常量表示的可订阅运行时事件 |
回调(Callbacks) |
事件触发时被调用的 Python 可调用对象 |
相对 sys.settrace / sys.setprofile 的优势#
sys.monitoring 是 PEP 669 的产物,其设计目标是取代旧有的跟踪/分析机制,主要优势在于:
按需注册的事件:旧机制里
sys.settrace的跟踪函数会对每一行、每一次调用都被调用,哪怕你只关心函数返回;新机制只在你显式开启的事件上触发回调。局部事件关闭:通过从回调返回
sys.monitoring.DISABLE,可以在「特定代码位置」永久关闭事件,已探测过的位置不再产生任何开销。事件的可组合位掩码:事件用 2 的幂整数表示,可用按位或(
|)自由组合出需要的事件集合。中断安全 / 开销可控:官方文档明确写了这样一句话——如果调试器把除少数断点之外的所有监控都禁用,那么「程序在调试器下运行将不产生额外开销」。这是旧机制无法做到的。
独立于
sys.settrace:二者可以共存,sys.monitoring不会被sys.settrace的全局跟踪所干扰(反之亦然,语义上彼此独立)。
2. 核心术语表#
以下术语均用平实语言解释,避免用术语解释术语:
术语 |
一句话平实解释 |
|---|---|
事件(event) |
程序运行到某个位置、执行某个动作时产生的信号,例如「某个函数开始执行」「某行代码即将执行」。 |
工具(tool) |
使用这套监控能力的一个程序或组件,例如调试器、覆盖率统计器、性能分析器。 |
工具 ID(tool identifier) |
一个 0~5 之间的整数编号,还带一个名称,用来让不同工具各用各的编号、互不打架。 |
回调(callback) |
你写的一段函数;当某个已开启的事件发生时,解释器会调用它,并传入该事件相关的参数。 |
全局事件(global event) |
对整个程序都开启的事件,无论代码在哪,触发时都会调用回调。 |
局部事件(local event) |
只对某个特定的代码对象开启的事件,只在那个函数/代码执行时才触发。 |
中断(discontinuity) |
通过回调返回 |
事件集合(event set) |
用按位或把多个事件整数拼起来得到的一把「开关位图」,用于一次性开启/关闭多个事件。 |
辅助事件(ancillary event) |
自身能被订阅、但受另一个事件控制才可见的事件,例如 |
3. 工具 ID 管理#
工具 ID 是 0(含)到 5(含)闭区间内的整数。同一时间最多只能有 6 个工具。在使用任何工具 ID 之前,都必须先「登记」它。
use_tool_id(tool_id, name, /) → None#
sys.monitoring.use_tool_id(tool_id: int, name: str, /) -> None
在
tool_id可被使用之前必须调用。tool_id必须在 0~5 闭区间内。name是必填参数(本教程依据 3.14.7 实测:省略会抛出TypeError: use_tool_id expected 2 arguments, got 1)。它作为该工具的名称,可在后续通过get_tool()查询,便于调试与协作。如果
tool_id已被占用,抛出ValueError。错误处理要点:尝试占用一个已登记的 ID,或传入超出 0~5 范围的 ID,都会得到
ValueError。
clear_tool_id(tool_id, /) → None#
sys.monitoring.clear_tool_id(tool_id: int, /) -> None
注销与该 tool_id 关联的所有事件与回调函数(但不清除名称,也不释放 ID 本身)。
free_tool_id(tool_id, /) → None#
sys.monitoring.free_tool_id(tool_id: int, /) -> None
应在工具不再需要该 tool_id 时调用。在释放 tool_id 之前,它会先自动调用 clear_tool_id()。
get_tool(tool_id, /) → str | None#
sys.monitoring.get_tool(tool_id: int, /) -> str | None
如果
tool_id已被使用,返回其名称(字符串);否则返回None。tool_id必须在 0~5 闭区间内。
预定义的工具 ID#
虚拟机处理事件时对所有 ID 一视同仁,但为便于工具之间协作,预定义了以下 ID:
sys.monitoring.DEBUGGER_ID = 0 # 调试器
sys.monitoring.COVERAGE_ID = 1 # 覆盖率工具
sys.monitoring.PROFILER_ID = 2 # 性能分析器
sys.monitoring.OPTIMIZER_ID = 5 # 优化器
编号 3 与 4 留给自定义工具。本教程的示例统一使用
3或4,避免与预定义 ID 冲突。
4. 事件类型详解#
以下逐一解释 sys.monitoring.events 命名空间中受支持的事件。全部名称与语义均以 Python 3.14 官方文档为准。
需要先行理解的两条通用规则:
每个事件都是 2 的幂整数常量,多个事件可用按位或组合,例如
PY_START | PY_RETURN表示同时选中两个事件。事件的完整状态用「事件集合」表示——某个事件是否开启,取决于该事件对应的二进制位是否被置位。
4.1 Python 函数事件#
事件 |
含义(文档原文语义) |
触发时机补充 |
|---|---|---|
|
开始一个 Python 函数 |
在函数被调用之后立即发生,此时被调用方的帧已在调用栈中 |
|
从一个 Python 函数返回 |
在 return 之前立即发生,被调用方的帧仍在栈中 |
|
从一个 Python 函数产出数据 |
在 yield 之前立即发生,被调用方的帧仍在栈中 |
|
恢复执行一个 Python 函数(用于生成器与协程函数) |
不含 |
|
一个 Python 函数由 |
与 |
|
在异常展开期间从一个 Python 函数退出 |
包括在该函数内直接引发、且被允许继续向外传播的异常 |
4.2 指令级事件(VM 指令与控制流)#
事件 |
含义(文档原文语义) |
|---|---|
|
一条与上一条指令行号不同的指令即将被执行(即实际推进到了新的一行) |
|
一条虚拟机(VM)指令即将被执行 |
|
在控制流图中发生一次无条件跳转 |
|
条件分支向左(3.14 新增) |
|
条件分支向右(3.14 新增) |
|
条件分支(3.14 已弃用,请改用 |
关于 BRANCH 的说明:它虽然仍存在于命名空间中,但已在 3.14 被标记弃用。官方文档指出,改用 BRANCH_LEFT 与 BRANCH_RIGHT 能获得更好性能,因为这两个事件「可以分别独立禁用」。
关于「左 / 右」的约定:如何呈现「左」「右」分支由工具自行决定;无法保证哪个分支是「左」、哪个是「右」,唯一保证的是在程序运行的整个持续时间里它是一致的。
4.3 关于 MARKER 事件的重要澄清#
一些资料可能提到 sys.monitoring.events.MARKER(标记事件)。在 Python 3.14 的官方文档中并不存在该事件,本教程在 3.14.7 上实测 sys.monitoring.events 的属性列表也不包含 MARKER。因此编写针对 3.14 的监控代码时,请不要依赖 MARKER;若有需要,请查阅你所用具体版本的最新文档确认其是否已加入。
4.4 调用与 C 边界事件#
事件 |
类别 |
含义(文档原文语义) |
|---|---|---|
|
局部事件 |
Python 代码中的一次调用(事件在调用之前发生) |
|
辅助事件 |
从任意可调用对象返回,Python 函数除外(事件在返回之后发生) |
|
辅助事件 |
从任意可调用对象引发异常,Python 函数除外(事件在退出之后发生) |
C_RETURN 与 C_RAISE 属于辅助事件:它们虽然能像其他事件一样被监控,但受 CALL 事件控制——只有当对应的 CALL 事件正在被监控时,C_RETURN / C_RAISE 才会被看到。
4.5 异常事件#
事件 |
含义(文档原文语义) |
|---|---|
|
一个异常被引发(排除那些会导致 |
|
一个异常被重新引发,例如在 |
|
一个异常被处理 |
|
一个人工的 |
4.6 STOP_ITERATION 事件的来龙去脉#
PEP 380 规定:从生成器或协程返回值时,会天然地引发 StopIteration 异常。但这种返回值方式非常低效,因此包括 CPython 3.12+ 在内的部分实现,只在该异常「对外可见」时才真正引发它。
为了让工具在不拖慢生成器/协程的前提下仍能监控到真实异常,就有了 STOP_ITERATION 事件。它的特点是:
可以被局部禁用(
RAISE事件则不能)。STOP_ITERATION事件与StopIteration异常的RAISE事件等价,在生成事件时被视为可互换。实现出于性能考虑会优先选择STOP_ITERATION,但也可能用StopIteration生成RAISE事件。
4.7 特殊值:NO_EVENTS、DISABLE、MISSING#
特殊值 |
含义 |
|---|---|
|
整数 |
|
可从回调函数返回的特殊值,用于禁用当前代码位置的事件 |
|
传给回调函数的特殊值,表示「本次调用不附带任何参数」 |
DISABLE 的特殊语义:只有局部事件能在指定代码位置被禁用。从回调返回 DISABLE 不会改变已设置的事件集合,也不会影响同一事件在其他代码位置的状态;它只是让「这一处位置」之后不再触发。可从回调返回 DISABLE 即可实现高性能监控。
MISSING 的特殊语义:它只出现在 CALL/C_RETURN/C_RAISE 的回调签名里,用于「被调用对象没有参数」的情形(详见第 6 节)。
5. 事件集合与查询#
events 命名空间#
所有事件常量都是 sys.monitoring.events 命名空间的属性。请注意:该命名空间并没有 __version__ 属性(3.14.7 实测 hasattr(sys.monitoring.events, '__version__') 为 False),一切以事件常量本身为准。
get_events(tool_id, /) → int#
sys.monitoring.get_events(tool_id: int, /) -> int
返回代表该工具所有活跃事件的整数(位掩码)。可用 & 判断某个事件是否已开启,或用 == events.NO_EVENTS 判断「一个事件都没开」。
set_events(tool_id, event_set, /) → None#
sys.monitoring.set_events(tool_id: int, event_set: int, /) -> None
激活 event_set 中所有已置位的事件。如果 tool_id 未在 use_tool_id 中登记,抛出 ValueError。默认情况下没有任何事件被激活。
get_local_events(tool_id, code, /) → int#
sys.monitoring.get_local_events(tool_id: int, code: types.CodeType, /) -> int
返回对 code 生效的所有局部事件(位掩码)。
set_local_events(tool_id, code, event_set, /) → None#
sys.monitoring.set_local_events(tool_id: int, code: types.CodeType, event_set: int, /) -> None
为 code 激活 event_set 中所有已置位的局部事件。如果 tool_id 未登记,抛出 ValueError。
restart_events() → None#
sys.monitoring.restart_events() -> None
重新启用之前被 DISABLE 针对所有工具关闭的所有事件。
注意:官方文档与 3.14.7 实测中,查询/设置局部事件只提供
get_local_events/set_local_events两个函数,不存在名为local_events的独立属性。
事件分类速查#
分类 |
包含的事件 |
能否用 |
|---|---|---|
局部事件(local) |
|
可以 |
辅助事件(ancillary) |
|
受 |
其他事件(other) |
|
不能 |
RERAISE是受支持的事件之一,其回调签名与RAISE等异常事件一致(见下节);官方文档在「其他事件」小节只列出了上述四个名称,这里按文档原文如实呈现。
6. 回调注册与回调签名#
register_callback(tool_id, event, func, /) → Callable | None#
sys.monitoring.register_callback(tool_id: int, event: int, func: Callable | None, /) -> Callable | None
用给定的
tool_id为event注册可调用对象func。如果同一
tool_id+ 同一event已经注册过另一个回调,旧回调会被注销并作为返回值返回;其他情况下返回None。这等价于「同一工具的同一事件只能有一个回调,重复注册会替换,而非叠加」。该调用会触发审计事件
sys.monitoring.register_callback,附带参数func。传入
func=None可注销:register_callback(tool_id, event, None)。回调可以在任何时刻注册或注销。
一个事件只会触发一次,即使它在全局与局部同时开启。因此若你的代码可能同时全局、局部开启某个事件,回调要写成能同时处理两种触发来源。
回调函数参数#
当一个已激活的事件发生时,已注册的回调会被调用。回调返回除 DISABLE 之外的任何对象都没有任何效果。各事件的回调签名如下:
(1)PY_START 与 PY_RESUME
func(code, instruction_offset)
(2)PY_RETURN 与 PY_YIELD
func(code, instruction_offset, retval)
(3)CALL、C_RAISE 与 C_RETURN(其中 arg0 可为 MISSING)
func(code, instruction_offset, callable, arg0)
code:发起调用的那个代码对象;callable:即将被调用的对象(即触发事件的那个对象);arg0:被调用对象的第一个参数;若调用没有参数,则arg0为sys.monitoring.MISSING;对实例方法而言,
callable是类上找到的函数对象,arg0是该实例(即方法的self参数)。
(4)RAISE、RERAISE、EXCEPTION_HANDLED、PY_UNWIND、PY_THROW 与 STOP_ITERATION
func(code, instruction_offset, exception)
(5)LINE
func(code, line_number)
(6)BRANCH_LEFT、BRANCH_RIGHT 与 JUMP
func(code, instruction_offset, destination_offset)
destination_offset是代码下一步将执行的位置。
(7)INSTRUCTION
func(code, instruction_offset)
7. 代码对象相关#
凡接收 types.CodeType 参数的函数(get_local_events、set_local_events),应当做好接受「并非由 Python 定义、但长得类似」对象的准备(参见 CPython 的 Monitoring C API)。也就是说,编码时不要把 code 严格假定为 types.CodeType 的实例。
在纯 Python 场景下,最常用的做法是通过函数的 __code__ 属性拿到代码对象:
def foo():
pass
foo.__code__ # 函数的代码对象
C.method.__code__ # 绑定方法对应的函数代码对象
而对于属性(property)、描述符等,可借助其底层函数对象取得代码对象,例如:
type(C).prop.fget.__code__ # property 的 getter 函数代码对象
type(C).prop.fset.__code__ # property 的 setter 函数代码对象
简言之:任何有
__code__的函数对象,其.__code__都能直接作为局部事件 API 的code参数。
8. 可运行示例#
以下示例在 Python 3.14.7 上运行通过,代码为完整可执行片段。
8.1 最小监控:统计 PY_START / PY_RETURN#
import sys
events = sys.monitoring.events
counts = {"start": 0, "return": 0}
def on_start(code, instruction_offset):
counts["start"] += 1
def on_return(code, instruction_offset, retval):
counts["return"] += 1
# 1. 登记工具 ID(用 4,避开预定义的 0/1/2/5)
sys.monitoring.use_tool_id(4, "quick-profiler")
# 2. 全局开启 PY_START | PY_RETURN 事件
sys.monitoring.set_events(4, events.PY_START | events.PY_RETURN)
# 3. 分别注册两个事件的回调
sys.monitoring.register_callback(4, events.PY_START, on_start)
sys.monitoring.register_callback(4, events.PY_RETURN, on_return)
def add(a, b):
return a + b
add(1, 2)
print(counts) # {'start': 1, 'return': 1}
# 4. 用完释放工具 ID
sys.monitoring.free_tool_id(4)
8.2 按代码对象开启局部事件 + DISABLE 优化#
import sys
events = sys.monitoring.events
hits = []
def on_line(code, line_number):
hits.append(line_number)
return sys.monitoring.DISABLE # 该行只报告一次,之后本地关闭
sys.monitoring.use_tool_id(3, "line-tracer")
sys.monitoring.register_callback(3, events.LINE, on_line)
def target():
x = 1
y = 2
return x + y
# 只对 target 的代码对象开启 LINE 局部事件(不污染其他代码)
sys.monitoring.set_local_events(3, target.__code__, events.LINE)
target()
print(hits) # [36, 37, 38] (行号取决于源码位置)
target() # 由于上一轮被 DISABLE 关闭,不再触发
print(hits) # 仍是 [36, 37, 38]
sys.monitoring.restart_events() # 重新启用被 DISABLE 的事件
target() # 再次触发三行
print(hits) # [36, 37, 38, 36, 37, 38]
sys.monitoring.free_tool_id(3)
8.3 CALL 事件与 MISSING,以及工具管理#
import sys
events = sys.monitoring.events
seen = []
def on_call(code, instruction_offset, callable, arg0):
seen.append((getattr(callable, "__name__", repr(callable)), arg0))
sys.monitoring.use_tool_id(4, "call-watcher")
print(sys.monitoring.get_tool(4)) # 'call-watcher'
sys.monitoring.register_callback(4, events.CALL, on_call)
sys.monitoring.set_events(4, events.CALL)
def foo(x=42):
return x
foo() # 无实参:arg0 为 sys.monitoring.MISSING
foo(7) # 有实参:arg0 为 7
result = seen[:] # 切片不触发 CALL,先取出记录
sys.monitoring.free_tool_id(4)
print([(name, arg) for name, arg in result])
# [('foo', <MISSING 对象>), ('foo', 7)]
print(result[0][1] is sys.monitoring.MISSING) # True
示例 3 里刻意用
seen[:]切片(而非函数调用)在释放工具前取出记录,是因为全局开启CALL后,所有 Python 代码中的调用(包括对free_tool_id等内置/绑定对象的调用)都会触发回调;若在监控区间内调用函数,这些调用本身也会被记录,从而「污染」输出。
9. 版本可用性与演进#
3.12 引入:
sys.monitoring随 PEP 669 进入标准库,提供了低开销的监视事件框架与工具 ID 机制。3.13:事件集合与 3.12 基本保持一致(仍只有
BRANCH一个条件分支事件),主要是在底层补充了生成 PEP 669 监控事件的PyMonitoringC API。3.14:新增
BRANCH_LEFT与BRANCH_RIGHT两个事件,用于替代旧的BRANCH事件,并将BRANCH标记为弃用(由 Mark Shannon 在 gh-122548 中贡献)。弃用理由是可以分别独立禁用这两个新事件,从而获得更好性能。
使用前请确认运行环境为 Python 3.12 及以上;在 3.11 及更早版本中该 API 不存在。
10. 性能与限制#
开销可控、可趋近于零:官方文档指出,只要调试器把除少数断点外的监控全部禁用,程序在调试器下运行即可做到「零额外开销」。关键在于善用局部事件与
DISABLE。INSTRUCTION开销最大:它会在每条 VM 指令执行前触发,属于最细粒度的监控,仅应在确有必要时开启。工具数量上限:仅 6 个工具 ID(0~5),用完必须
free_tool_id释放;忘记释放会长时间占用有限的 ID。同一事件只能有一个回调:同一
tool_id的同一event重复注册会替换旧回调(并返回旧回调),不会叠加执行。全局事件回调不能返回
DISABLE:为「全局事件」返回DISABLE会让解释器在非特定位置抛出ValueError(不提供 traceback)。DISABLE只对局部事件有效。回调返回值:返回除
DISABLE之外的任意对象均无任何效果。回调的重入与安全:回调期间执行的代码是否会再次触发同一工具的事件,官方文档未给出明确的重入保证。编写回调时应保持逻辑简单、无副作用,避免在回调中做昂贵或有状态的操作;并注意全局开启的事件回调可能在不同线程中被调用(监控是解释器进程级的)。
11. 注意事项 / 反模式#
❌ 不要写
import sys.monitoring或from sys.monitoring import events:二者都会ModuleNotFoundError。正确写法是import sys后使用sys.monitoring与sys.monitoring.events。❌ 不要在未
use_tool_id登记的情况下调用set_events/set_local_events:会抛ValueError。❌ 不要忘记
use_tool_id的name是必填参数(3.14.7 实测省略即报TypeError)。❌ 不要依赖 3.14 中不存在的
MARKER事件或events.__version__属性。❌ 不要在全局事件的回调里返回
DISABLE。❌ 不要依赖「同一事件的多个回调」:重复注册只保留最后一个(前一个被替换返回)。
⚠️ 全局开启
CALL/INSTRUCTION等高频事件会产生大量回调(连print也会被计入),仅对关心的函数使用局部事件以降低噪声。⚠️ 用完工具后
free_tool_id,避免耗尽 6 个 ID。⚠️ 事件若全局与局部同时开启,仍只触发一次,回调要能处理两种来源。
12. 与 contextvars 在异步监控场景的关系#
sys.monitoring 的事件与回调是「全局 / 代码对象」维度的,它本身不区分异步任务。在 asyncio 场景中,多个协程任务可能在同一条执行流上交错运行,同一个监控回调会在不同任务的上下文中被反复触发。
若希望「按任务」归因或隔离监控状态,可结合上一章的 contextvars 使用:asyncio 为每个任务维护独立的 context,回调被调用时可通过 ContextVar.get() 读取当前任务专属的状态,从而把监控数据正确归因到对应的逻辑流,而不是混在同一个全局计数里。二者互补:sys.monitoring 负责「何时、何地触发」,contextvars 负责「当前属于谁的上下文」。