登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  python教程

Python contextvars.Context 怎样复制上下文后运行回调

来源:17golang原创

时间:2026-10-18 02:04:05 143浏览 收藏

Python contextvars.Context 怎样复制上下文后运行回调

如果回调需要继承当前请求的上下文,又不能把修改写回外层,组合 copy_context() 和 Context.run() 就够了:先复制当前上下文,再让回调在副本里执行。回调通过 ContextVar.get() 读到副本中的值,期间的 set() 只记录在这个副本中;回调返回后,外层上下文仍保持原值。

官方文档:https://docs.python.org/3.14/library/contextvars.html

先看 copy_context 复制了什么

ContextVar 不等于普通全局变量。get() 读取的是当前线程上下文栈顶端的 Context。copy_context() 返回当前上下文的副本,适合在需要保留现有上下文、又要把后续工作隔离出来的场景使用。

先把变量放在模块级,再复制当前上下文:

from contextvars import ContextVar, copy_context

# 模块级创建 ContextVar,避免 Context 对变量产生不必要的强引用。
request_id = ContextVar("request_id", default="未设置")

# 当前上下文先保存请求标识,副本会继承这个值。
request_id.set("req-100")
ctx = copy_context()

# get() 读取副本里的值;get 的第二个参数用于处理变量未设置的情况。
print(ctx.get(request_id, "未设置"))
print(list(ctx.items()))

这里的复制是上下文对象层面的复制,不是把每个业务对象递归深拷贝一份。官方文档还明确说明,copy_context() 的复杂度是 O(1),上下文变量数量增加不会改变复制动作的复杂度。真正需要复制业务数据时,仍要由业务代码决定数据是否可变以及是否需要独立副本。

ContextVar、当前 Context、copy_context 和复制出的 Context 的静态关系说明图
图1:说明图展示当前上下文与复制出的 Context 之间的映射关系,不是截图或运行证据。

用 Context.run 包住一次回调

拿到副本后,调用 ctx.run(callable, *args, **kwargs)。它会临时把指定 Context 放到当前上下文栈顶,执行回调,再退出这个 Context。回调的返回值会原样返回;如果回调抛出异常,异常也会继续向外传播。

from contextvars import ContextVar, copy_context

# 使用默认值,让外层在没有显式设置时也有可读结果。
request_id = ContextVar("request_id", default="outer")
request_id.set("req-100")
ctx = copy_context()

def worker():
    # 读取的是 ctx 中继承来的值,而不是另一个全局变量。
    before = request_id.get()
    request_id.set("req-100-worker")
    after = request_id.get()
    return before, after

result = ctx.run(worker)
print(result)
print(request_id.get())

按照这个关系,worker() 看到的是 req-100,并把新值写入 ctx 的执行域;ctx.run(worker) 返回后,外层读取仍是 req-100。注意这不是“复制出一个全局变量”,而是让同一个 ContextVar 在不同 Context 中拥有不同的当前值。

Context.run 包住回调并隔离 ContextVar 修改的边界说明图
图2:结构图展示外部上下文、回调执行域和退出后的外部值之间的边界关系。

回调结束后为什么不会污染外层值

关键在于 Context 有自己的进入和退出边界。调用 run() 时,Context 被压入当前线程的上下文栈;回调返回后它从栈顶退出,之前的上下文重新成为当前上下文。因此,回调里通过 set() 修改的值会记录在副本里,而不是直接覆盖外层 Context。

如果需要检查副本里有没有某个变量,可以用映射接口:

from contextvars import ContextVar, Context

# 空 Context 不包含任何变量,显式检查比直接下标读取更稳妥。
trace_id = ContextVar("trace_id")
empty = Context()

if trace_id in empty:
    value = empty[trace_id]
else:
    # get() 可提供缺省值;直接 empty[trace_id] 则会抛 KeyError。
    value = empty.get(trace_id, "没有 trace_id")

print(value)

Context() 创建的是空 Context,不会自动继承调用方的值。需要继承当前值时使用 copy_context();需要明确从空环境开始时才直接构造 Context()。

四个容易误判的排错点

1. 把 Context 当成可并发进入的共享容器

同一个 Context 不能在已经进入时再次进入,包括从另一个线程进入;这种情况下会抛出 RuntimeError。如果多个工作单元需要同时运行,应为每个工作单元准备自己的 Context 副本,而不是让它们争用同一个正在运行的 Context。

2. 误以为 Context.copy() 会深拷贝变量值

Context.copy() 是浅复制语义。上下文中的值如果本身是可变对象,复制后仍可能指向同一个对象。需要隔离列表、字典或业务实例时,应在业务边界显式复制值,不能把 Context 复制当成业务数据深拷贝。

3. 忽略回调异常会继续传播

run() 不会替回调吞掉异常。若回调需要转换错误类型或补充日志,应在回调外层用正常的 try/except 处理;不要因为使用了 Context 就假设异常会被上下文管理自动消化。

4. 用 ctx[var] 读取未设置的变量

ctx[var] 在变量没有值时会抛出 KeyError;var.get() 则会按调用参数、变量默认值或异常规则读取。排查上下文缺失时,先选择明确的缺省值,能更快区分“变量没设置”和“设置了空字符串”这两种情况。

一张表记住选择

需求写法要注意什么
继承当前上下文copy_context()得到 Context 副本,不深拷贝业务值
从空环境开始Context()不会自动带入当前 ContextVar 值
在指定上下文执行ctx.run(callback)进入后回调读取/修改都属于该 Context
检查变量是否存在var in ctx 或 ctx.get(var, default)避免未设置时直接下标访问

常见问题

copy_context() 会复制 ContextVar 本身吗?

不会创建新的 ContextVar。它复制的是当前 Context 对象及其变量到值的关联;同一个 ContextVar 可以在不同 Context 中读取到不同的当前值。

ctx.run() 执行后需要手动恢复吗?

不需要。正常返回或异常退出都会离开该 Context,外层当前上下文由运行时恢复。只有业务数据本身的清理,才需要另外使用 try/finally 或资源上下文管理器。

什么时候不该使用 contextvars?

如果值只是单次函数调用的明确输入,直接传参数更容易理解。contextvars 更适合请求标识、租户信息、追踪信息这类需要沿调用链隐式传递、又要保持并发隔离的上下文状态。

记住一条主线即可:copy_context() 负责准备隔离副本,Context.run() 负责让回调在副本中运行,回调里的修改留在副本,退出后外层值恢复原状。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>