Python上下文管理之ContextVar源码解析

目录

1、contextvar的使用

1.1、在普通函数中的使用

1.2、在异步函数中的使用

2、contextvar源码分析

2.1、ContextMeta,ContextVarMeta和TokenMeta三个元类

2.2、contextvar基本的上下文管理方式

2.3、contextvar之Context实现

2.4、Contextvar之Token的实现

2.5、Contextvar之ContextVar的实现

前言

  • 在Python3.7后官方库出现了contextvars模块, 它的主要功能就是可以为多线程以及asyncio生态添加上下文功能,即使程序在多个协程并发运行的情况下,也能调用到程序的上下文变量, 从而使程序逻辑解耦。
  • 在flask框架中,flask1.x版本使用了LocalStack来管理上下文,LocalStack是基于Local即一个字典来实现的上下文管理,在flask2.x版本以后使用了ContextVar管理上下文,那么ContextVar实现上文管理的原理是什么?带个这个问题,分别从ContextVar的使用和源码去看看它是如何完成上下文管理的吧

注:下面所有Python代码都是基于Python3.9版本的

注:如果对异步函数定义和使用不清楚可见Python异步编程之协程-CSDN博客

1、contextvar的使用

1.1、在普通函数中的使用

1、contextvar的基本使用,包括set()设置值、get()获取值、reset()重置值、获取old_value值、默认值设置

from contextvars import ContextVar

ctx = ContextVar("debug test")
# 设置默认值的方式
# ctx = ContextVar("debug test", default="high")

# 给ctx设置第一个值
token1 = ctx.set("hello")
# 给ctx设置第二个值
token2 = ctx.set("world")

# 获取当前ctx的最新值
print(ctx.get()) # world

# 获取token2对象的旧值
print(token2.old_value) # hello

# 重置token2,此时ctx的当前值变为旧值 hello
ctx.reset(token2)
print(ctx.get()) # hello

# 获取token1的旧值,因为token1没有旧值,所以会返回默认设置对象<Token.MISSING>
print(token1.old_value) # <Token.MISSING>

# 再次把token1的值重置,此时如果ContextVar()初始化没有设置默认值,get()函数返回也没有设置默认值,则会抛出LookupError异常
# 如果存在默认值优先返回get()函数设置的默认值,其次在是ContextVar()初始化设置的默认值
ctx.reset(token1)
print(ctx.get()) # 此处抛出LookupError异常

# get()函数设置默认值方式
# print(ctx.get("tom")) # 此处返回tom

2、run()函数的使用,用于执行一个可调用对象时隔离当前上下文;contextvars.Context 它负责保存 ContextVars 对象和设置的值之间的映射(保存上下文实际是Context完成的),但实际使用中不会直接通过 contextvars.Context 来创建保存上下文,而是通过 contentvars.copy_context 函数来创建。

import contextvars

ctx1 = contextvars.ContextVar("ctx1")
ctx2 = contextvars.ContextVar("ctx2")
ctx1.set("hello")
ctx2.set("world")

# 复制一个当前的上下文对象(注意这是一个带有当前上下文值的新对象)
context = contextvars.copy_context()

# 遍历得到当前上下文所有的ContextVar对象
for ctx, value in context.items():
    print(ctx.get(), ctx.name, value)

    """
    输出结果:
    world ctx2 world
    hello ctx1 hello
    """

def task():
    print(f"befor task: {ctx1.get()} context[ctx1]: {context[ctx1]}")
    ctx1.set("task")
    print(f"after task: {ctx1.get()} context[ctx1]: {context[ctx1]}")

    """
    输出结果:
    befor task: hello context[ctx1]: hello
    after task: task context[ctx1]: task
    """

context.run(task)

print(f"ctx1: {ctx1.get()}  context[ctx1]: {context[ctx1]}") # ctx1: hello  context[ctx1]: task
  • 注意:为什么context[ctx1]在task函数中由hello改变成task,出了task()函数context[ctx1]还是task而不是hello。不应该和ctx1一样保持上下文一致吗?
    • ctx1保持上下文一致是因为在task()函数内和在task()函数外使用的上下文不是同一个上下文所以才会保持一致;
    • 再看context[ctx1]对象不管是在task()函数内还是在task()函数外context对象自始至终都是同一个,所以context对象中ctx1的值在task()函数内发生了变化,在task()函数外同样也会跟着变化(因为context对象在task()内外指向的都是同一块内存地址)

1.2、在异步函数中的使用

1、异步函数中的使用

  • 在异步函数中是如何实现协程上下文切换呢?我们知道在 asyncio 中,Task 是用于封装协程的对象,它代表一个可以被调度和执行的异步任务;其作用有:
    • a.封装协程:Task 对象可以封装一个协程函数或可迭代对象(如生成器),使其变成可调度的异步任务。通过 asyncio.create_task() 或 loop.create_task() 方法创建 Task 对象。
    • b.异步任务的调度和执行:Task 对象可以被添加到事件循环中,由事件循环调度和执行。事件循环会在适当的时机切换任务,以实现异步执行。一旦 Task 对象被添加到事件循环中,它就可以与其他任务共享 CPU 时间,实现并发执行。
    • c. 取消和异常处理:Task 对象可以被取消,即停止其执行。可以使用 Task.cancel() 方法取消任务,并通过捕获 asyncio.CancelledError 异常来处理取消操作。此外,Task 对象还提供了 add_done_callback() 方法,用于注册回调函数以处理任务执行完毕或抛出异常的情况。
    • d. 获取任务状态和结果:通过 Task 对象可以获取任务的当前状态,包括已完成、正在运行、已取消等。使用 Task.done() 方法可以检查任务是否已完成,而 Task.result() 方法可以获取任务的返回值(如果有)或引发的异常(如果有)。

Task初始化源码如下:

class Task(futures._PyFuture):  # Inherit Python Task implementation
                                # from a Python Future implementation.
    _log_destroy_pending = True
    def __init__(self, coro, *, loop=None, name=None):
        super().__init__(loop=loop)
        if self._source_traceback:
            del self._source_traceback[-1]
        if not coroutines.iscoroutine(coro):
            self._log_destroy_pending = False
            raise TypeError(f"a coroutine was expected, got {coro!r}")
        if name is None:
            self._name = f'Task-{_task_name_counter()}'
        else:
            self._name = str(name)

        self._must_cancel = False
        self._fut_waiter = None
        self._coro = coro
        self._context = contextvars.copy_context()
        self._loop.call_soon(self.__step, context=self._context)
        _register_task(self)
  • 从第19行代码可以看到Task中上下文对象是根据contextvars.copy_context()来实现的,这个过程和上面run()函数的使用原理一样,所以我们知道在异步函数中实现上下文管理是通过contextvars.copy_context()来实现的(注:不同Python版本asyncio实现上下文方式可能不一样。contextvars模块是在Python3.7引入的;asyncio 是在 Python 3.4 版本引入的标准库)。
  • 下面是一个示例说明contextvars在异步函数中的使用:
import asyncio
import contextvars

# 创建一个ContextVar
ctx = contextvars.ContextVar("ctx")
ctx.set("hello")

async def task(value, delay):
    print("task: ", ctx.get())
    ctx.set(value)
    await asyncio.sleep(delay)
    print("task: ", ctx.get())

async def main():
    # 异步执行函数
    await asyncio.gather(task('task1', 1), task('task2', 2))
    print("main: ", ctx.get())

asyncio.run(main())

# 执行结果
task:  hello
task:  hello
task:  task1
task:  task2
main:  hello
  • 执行过程:
    • 输出结果中的前两个“task:  hello”是同时打印出来的,
    • 然后先打印出:“task:  task1”,在打印出:“task:  task2”,
    • 最后打印出:“main:  hello”。
  • 结论:从上面的执行过程来看异步执行的task('task1', 1),和task('task2', 2)在ctx.set(value)之后内部的上下文是隔离的,执行完成之后又恢复到了之前的上下文。所以在异步函数中使用contextvars模块管理上下文非常方便。

2、contextvar使用的浅拷贝问题

  • contextvars.copy_context()的作用是拷贝一份当前的上下文对象,注意这里拷贝是浅拷贝,也就是当前上下文的一个引用,只有在发生写数据的时候才会进行深拷贝生成一个新的上下文对象。这里和Linux中的进程fork运用了同一种技术:写时拷贝;这就是为什么我会把上面示例的“在ctx.set(value)之后”标注为红色。下面看一个示例:
import asyncio
import contextvars

ctx1 = contextvars.ContextVar('ctx1')
ctx2 = contextvars.ContextVar("ctx2")
ctx1.set("hello")
ctx2.set({"name": "张三"})
print("befroe ctx1_value_addr: ", id(ctx1.get())) # befroe ctx1_value_addr:  2070012128240
print("befroe ctx2_value_addr: ", id(ctx2.get())) # befroe ctx2_value_addr:  2070012049728

async def main():
    ctx1.set(1)  # 此时会进行写时拷贝
    print("main ctx1_value_addr: ", id(ctx1.get())) # main ctx1_value_addr:  2069998823728
    ctx2_dict = ctx2.get() # 此处只是引用即浅拷贝
    print("main ctx2_value_addr: ", id(ctx2_dict)) # main ctx2_value_addr:  2070012049728
    ctx2_dict["name"] = "李四"
    print(f"main inner ctx1: {ctx1.get()}  ctx2: {ctx2.get()}") # main inner ctx1: 1  ctx2: {'name': '李四'}


print(f"before run ctx1: {ctx1.get()}  ctx2: {ctx2.get()}") # before run ctx1: hello  ctx2: {'name': '张三'}
asyncio.run(main())
print(f"after run  ctx1: {ctx1.get()}  ctx2: {ctx2.get()}") # after run  ctx1: hello  ctx2: {'name': '李四'}

# 执行结果
befroe ctx1_value_addr:  2070012128240
befroe ctx2_value_addr:  2070012049728
before run ctx1: hello  ctx2: {'name': '张三'}
main ctx1_value_addr:  2069998823728
main ctx2_value_addr:  2070012049728
main inner ctx1: 1  ctx2: {'name': '李四'}
after run  ctx1: hello  ctx2: {'name': '李四'}
  • 首先我们分析打印的ctx值,在before run打印的ctx的值和after run的对比发现ctx2的值发生了变化,乍一看显然有点不符合异步执行切换上下文的情况啊,
  • 然后我们再进入到main()的内部看看ctx1和ctx2有什么差别导致出现这样的结果;发现ctx1使用了set()函数也就是重新设置了值,ctx2只使用了get()函数没有重新设置值,结合上面所说的contextvars.copy_context()的作用是拷贝一份当前的上下文对象,只是进行浅拷贝,貌似可以解释这个问题,为了验证实际是不是真是这样,
  • 我们分别在main()外和main()内打印出了ctx1值和ctx2值的地址,发现ctx1值前后的地址发生了改变,ctx2值前后的地址并没有发生变化,正好就对应上面所说的浅拷贝情况。

2、contextvar源码分析

查看contextvar源码的方式

  • 1、在gitlab上查看:https://github.com/MagicStack/contextvars/blob/master/tests/init.py
  • 2、下载contextvars查看源码:pip install contextvars

2.1、ContextMeta,ContextVarMeta和TokenMeta三个元类

  • 三个元类的功能:防止用户来继承Context,ContextVar和Token
  • 实现原理:只有 contextvars 模块中的 Context 类才能被用作基类,如果其他类尝试继承自 Context,则会触发 TypeError。
  • 查看ContextMeta元类的源码如下:
class ContextMeta(type(collections.abc.Mapping)):
    # contextvars.Context is not subclassable.
    def __new__(mcls, names, bases, dct):
        cls = super().__new__(mcls, names, bases, dct)
        # 只有 contextvars 模块中的 Context 类才能被用作基类,如果其他类尝试继承自 Context,则会触发 TypeError。
        if cls.__module__ != 'contextvars' or cls.__name__ != 'Context':
            raise TypeError("type 'Context' is not an acceptable base type")
        return cls

2.2、contextvar基本的上下文管理方式

  • 从下面源码可以看到contextvar是基于threading.local()生成自己的全局context,从源码中可以看出_state是threading.local()的引用;我们知道线程的上下文管理可以基于threading.local()实现,作为基于线程实现的协不能直接基于threading.local()实现上下文管理;所以contextvar模块提供了支持协程的上下文管理;以下源码是contextvar模块基本的上下文管理方式,实现复杂的线程、协程的上文管理就是基于此部分:
# 复制上下文
def copy_context():
    # 复制当前上下文返回一个新的上下文
    return _get_context().copy()

# 获取上下文
def _get_context():
    # 从_state中获取context对象
    ctx = getattr(_state, 'context', None)
    if ctx is None:
        # 如果ctx对象为空,则使用Context初始化一个ctx对象
        ctx = Context()
        # 把ctx对象赋值给_state.context
        _state.context = ctx
    # 返回ctx
    return ctx

# 设置上下文
def _set_context(ctx):
    _state.context = ctx

# 定义全局context使用threading.local()实现,用于线程、协程的上下文管理
_state = threading.local()

2.3、contextvar之Context实现

  • Context作用:创建上下文对象,存储上下文,完成上下文对象切换
  • Context源码实现如下:
class Context(collections.abc.Mapping, metaclass=ContextMeta):

    def __init__(self):
        # 初始化一个不可变字典对象,用于保存上下文
        self._data = immutables.Map()
        # 初始化一个_prev_context对象,用于保存之前的上下文
        self._prev_context = None

    # 用于执行一个可执行对象,并不影响当前上下文
    def run(self, callable, *args, **kwargs):
        if self._prev_context is not None:
            raise RuntimeError(
                'cannot enter context: {} is already entered'.format(self))
        # 获取当前的上下文对象并保存到_prev_context对象中
        self._prev_context = _get_context()
        try:
            # 设置上下文为当前对象的上下文,到切换上下文的作用
            _set_context(self)
            # 执行可调用对象
            return callable(*args, **kwargs)
        finally:
            # 执行完成可调用对象之后,切换到之前的上下文
            _set_context(self._prev_context)
            self._prev_context = None
    # 复制上下文
    def copy(self):
        # 创建一个新的上下文对象
        new = Context()
        # 把当前上下文的值赋值给新的上下文对象
        new._data = self._data
        # 返回一个新的上下文对象
        return new
  • 1、其中使用了immutables.Map()创建了一个不可变的字典对象赋值self._data对象,也就是每次对self._data新增值都会返回一个新的对象,并不会影响到原来的对象;目的是:防止调用copy方法后得到的上下文的变动会影响到了原本的上下文变量。下面是对immutables.Map()使用的示例:
data = immutables.Map()
data1 = data.set("name", "张三")
data2 = data1.set("age", "18")
print(data1)
print(data2)

# 输出结果
immutables.Map({'name': '张三'})
immutables.Map({'name': '张三', 'age': '18'})
  • 2、run()函数的使用:生成一个新的上下文变量给另外一个线程使用(可调用对象), 并且这个新的上下文变量跟原来的上下文变量是一致的。
    • 执行run的时候需要copy一个新的上下文来调用传入的函数;根据immutables.Map的属性, 函数中对上下文的修改并不会影响旧的上下文变量,达到进程复制数据时的写时复制的目的,这样可以降低内存的开销提高程序的执行效率。
    • 函数执行完了会再次切换到旧的上下文, 从而完成一次上下文切换.

run()使用示例如下:

from contextvars import ContextVar

ctx_g = ContextVar("test")
ctx_g.set({"name": "张三"})

def main():
    new = ctx_g.set({"name": "李四"})
    print(f"main: old_value: {new.old_value}; new_value: {ctx_g.get()}")

ctx = contextvars.copy_context()
ctx.run(main)

print("ctx_g", ctx_g.get())

# 输出结果
main: old_value: {'name': '张三'}; new_value: {'name': '李四'}
ctx_g {'name': '张三'}

2.4、Contextvar之Token的实现

  • 作用:上下文的本质是一个栈, 每次set一次对象就向栈增加一层数据, 每次reset就是pop掉顶层的数据, 而在Contextvars中, 通过Token对象来维护栈数据之间的交互和ContextVar配合使用。
  • 源码如下:
class Token(metaclass=TokenMeta):

    MISSING = object()

    def __init__(self, context, var, old_value):
        # 存储当前上下文对象
        self._context = context
        # 储存当前set的数据
        self._var = var
        # 存储上次set的数据
        self._old_value = old_value
        # 当前token对象是否使用表示
        self._used = False

    @property
    def var(self):
        return self._var

    @property
    def old_value(self):
        return self._old_value

    def __repr__(self):
        r = '<Token '
        if self._used:
            r += ' used'
        r += ' var={!r} at {:0x}>'.format(self._var, id(self))
        return r

2.5、Contextvar之ContextVar的实现

  • ContextVar在实际使用中使用最多的类,它提供了set(设置值)、get(获取值)、reset(重置值)三个方法,通过Context类实现值的写入获取,通过Token类实现set和reset。
  • 1、初始化ContextVar对象:
class ContextVar(metaclass=ContextVarMeta):
    def __init__(self, name, *, default=_NO_DEFAULT):
        if not isinstance(name, str):
            raise TypeError("context variable name must be a str")
        # 用于调试
        self._name = name
        # 给当前对象设置默认值
        self._default = default
  • 2、set()  为当前上下文设置变量,返回带有旧值的Token对象
def set(self, value):
    # 获取当前上下文对象"Context"
    ctx = _get_context()
    # 临时变量保存当前上下文的_data值
    data = ctx._data
    try:
        # 获取Context的旧对象
        old_value = data[self]
    except KeyError:
        # 获取不到就赋值为object(全局唯一)
        old_value = Token.MISSING
    # 设置的新的值
    updated_data = data.set(self, value)
    # 更新当前上下文的_data
    ctx._data = updated_data
    # 返回带有旧值的token
    return Token(ctx, self, old_value)
  • 3、get()   从当前上下文获取变量
def get(self, default=_NO_DEFAULT):
    # 获取当前上下文"Context"
    ctx = _get_context()
    try:
        # 返回当前上下文的值
        return ctx[self]
    except KeyError:
        pass
    # 返回调用get时设置的默认值
    if default is not _NO_DEFAULT:
        return default
    # 返回初始化ContextVar时设置的默认值
    if self._default is not _NO_DEFAULT:
        return self._default
    # 如果没有返回值则抛出异常
    raise LookupError
  • 4、reset()   清除当前上下文数据
def reset(self, token):
    # token是已被使用
    if token._used:
        raise RuntimeError("Token has already been used once")
    # token是否是当前contextvar对象
    if token._var is not self:
        raise ValueError(
            "Token was created by a different ContextVar")
    # token中的上下文是否和当前上下文一直
    if token._context is not _get_context():
        raise ValueError(
            "Token was created in a different Context")

    # 获取token中上下文对象
    ctx = token._context
    # 如果没有旧值直接删除该值
    if token._old_value is Token.MISSING:
        ctx._data = ctx._data.delete(token._var)
    else:
        # 如果存在旧值就把当前上下文的值设置旧值
        ctx._data = ctx._data.set(token._var, token._old_value)
    # 标记此token已被使用
    token._used = True
  • 19
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Python 上下文管理器(Context Manager)是一种用于管理资源的特殊对象,它定义了在进入和离开代码块时应该执行的操作。上下文管理器通常与 `with` 语句一起使用,用于确保资源在使用完毕后被正确释放。 在 Python 中,上下文管理器可以通过实现 `__enter__` 和 `__exit__` 方法来创建。`__enter__` 方法定义了进入代码块时要执行的操作,而 `__exit__` 方法定义了离开代码块时要执行的操作。当代码块执行完毕或发生异常时,`__exit__` 方法会自动被调用。 下面是一个简单的示例,演示了如何使用上下文管理器来打开和关闭文件: ```python class FileManager: def __init__(self, filename): self.filename = filename def __enter__(self): self.file = open(self.filename, 'r') return self.file def __exit__(self, exc_type, exc_val, exc_tb): self.file.close() # 使用上下文管理器打开文件 with FileManager('example.txt') as file: data = file.read() # 在此处进行文件操作 # 文件已经自动关闭,不需要手动调用 file.close() ``` 在上述示例中,`FileManager` 类实现了 `__enter__` 和 `__exit__` 方法。在 `__enter__` 方法中,我们打开了文件并将其返回,使得在 `with` 语句块中可以使用该文件对象。当代码块执行完毕或发生异常时,`__exit__` 方法会被调用,确保文件被关闭。 上下文管理器的一个重要用途是确保资源的正确释放,比如关闭文件、释放锁等。它使得代码更加简洁、可读性更高,并且可以避免常见的资源泄漏问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值