# `contextvars` --- 上下文变量

本模块提供了相关API用于管理、存储和访问上下文相关的状态。 `ContextVar` 类用于声明 上下文变量 并与其一起使用。函数 `copy_context()` 和类 `Context` 用于管理当前上下文和异步框架中。

当在并发代码中使用时，有状态的上下文管理器应当使用上下文变量而不是 `threading.local()` 以防止它们的状态意外地泄露到其他代码中。

更多信息参见 PEP 567 。

Added in version 3.7.

## 上下文变量

class contextvars.ContextVar(name[, *, default])

  此类用于声明一个新的上下文变量，如:

      var: ContextVar[int] = ContextVar('var', default=42)

  name 参数用于内省和调试，必需。

  调用 `ContextVar.get()` 时，如果上下文中没有找到此变量的值，则返回可选的仅命名参数 default 。

  重要： 上下文变量应该在顶级模块中创建，且永远不要在闭包中创建。 `Context` 对象拥有对上下文变量的强引用，这可以让上下文变量被垃圾收集器正确回收。

  name

    上下文变量的名称，只读属性。

    Added in version 3.7.1.

  get([default])

    返回当前上下文中此上下文变量的值。

    如果当前上下文中此变量没有值，则此方法会:

    - 如果提供了 default，返回其值；或者

    - 返回上下文变量本身的默认值， 如果创建此上下文变量时提供了默认值；或者

    - 抛出 `LookupError` 异常。

  set(value)

    调用此方法设置上下文变量在当前上下文中的值。

    必选参数 value 是上下文变量的新值。

    返回一个 `Token` 对象，可通过 `ContextVar.reset()` 方法将上下文变量还原为之前某个状态。

  reset(token)

    将上下文变量重置为调用 `ContextVar.set()` 之前、创建 token 时候的状态。

    例如：

        var = ContextVar('var')

        token = var.set('new value')
        # 使用 'var' 的代码；var.get() 将返回 'new value'。
        var.reset(token)

        # 在重置调用之后 var 将不再有值，
        # 因此 var.get() 将引发一个 LookupError。

    同一个 token 不能被重复使用。

class contextvars.Token

  Token 对象是由 `ContextVar.set()` 方法返回的。 它们可以被传入 `ContextVar.reset()` 方法来恢复在对应的 set 之前状态的变量值。 同一个 token 不能多次重置上下文变量。

  var

    只读属性。指向创建此 token 的 `ContextVar` 对象。

  old_value

    一个只读属性。 会被设为在创建此令牌的 `ContextVar.set()` 方法调用之前该变量所具有的值。 如果调用之前变量没有设置值则它会指令 `Token.MISSING`。

  MISSING

    `Token.old_value` 会用到的一个标记对象。

## 手动上下文管理

contextvars.copy_context()

  返回当前上下文中 `Context` 对象的拷贝。

  以下代码片段会获取当前上下文的拷贝并打印设置到其中的所有变量及其值:

      ctx: Context = copy_context()
      print(list(ctx.items()))

  此函数具有 O(1) 复杂度，也就是说对于只包含几个上下文变量和很多上下文变量的情况运行速度是相同的。

class contextvars.Context

  `ContextVars` 与其值的映射。

  `Context()` 创建一个不包含任何值的空上下文。如果要获取当前上下文的拷贝，使用 `copy_context()` 函数。

  每个线程有它自己在用的 `Context` 对象栈。 current context 是位于当前线程的栈的顶部的 `Context` 对象。 栈中所有的 `Context` 对象都被视为是 进入过的。

  进入 一个上下文，这可以通过调用其 `run()` 方法来完成，将把该上下文推入当前线程的栈的顶部使它成为当前上下文。

  退出 当前上下文，这可以通过从传给 `run()` 方法的回调退出，通过将该上下文弹出上下文栈的顶部将当前上下文恢复至该上下文被进入之前的上下文来完成。

  由于每个线程都有它自己的上下文栈，`ContextVar` 对象的行为类似于 `threading.local()` 在不同线程中赋值时的行为。

  尝试进入一个已被进入的上下文，包括在其他线程中被进入的上下文，将会引发 `RuntimeError`。

  在退出一个上下文之后，它可以在稍后被重新进入（从任何线程）。

  任何通过 `ContextVar.set()` 方法对 `ContextVar` 值的修改都将在当前上下文中被记录。 `ContextVar.get()` 方法将返回关联到当前上下文的值。 退出一个上下文肯定会撤销在进入该上下文期间对上下文变量的任何修改（如有必要，这些值可通过重新进入相应的上下文来恢复）。

  Context 实现了 `collections.abc.Mapping` 接口。

  run(callable, *args, **kwargs)

    进入 Context，执行 `callable(*args, **kwargs)`，然后退出 Context。 返回 callable 的返回值，或者如果发生了异常则传播该异常。

    示例:

        import contextvars

        var = contextvars.ContextVar('var')
        var.set('spam')
        print(var.get())  # 'spam'

        ctx = contextvars.copy_context()

        def main():
            # 在调用 'copy_context()' 和 'ctx.run(main)' 之前
            # 'var' 被设为 'spam'，因此：
            print(var.get())  # 'spam'
            print(ctx[var])  # 'spam'

            var.set('ham')

            # 在将 'var' 设为 'ham' 之后：
            print(var.get())  # 'ham'
            print(ctx[var])  # 'ham'

        # 'main' 函数对 'var' 的任何修改
        # 都将包含在 'ctx' 中。
        ctx.run(main)

        # 'main()' 函数是在 'ctx' 上下文中运行的，
        # 因此对 'var' 的修改将包含在其中：
        print(ctx[var])  # 'ham'

        # 不过，在 'ctx' 之外，'var' 仍为 'spam'：
        print(var.get())  # 'spam'

  copy()

    返回此上下文对象的浅拷贝。

  var in context

    如果 context 中含有名称为 var 的变量，返回 `True`， 否则返回 `False`。

  context[var]

    返回名称为 var 的 `ContextVar` 变量。如果上下文对象中不包含这个变量，则抛出 `KeyError` 异常。

  get(var[, default])

    如果 var 在上下文对象中具有值则返回 var 的值。 在其他情况下返回 default。 如果未给出 default 则返回 `None`。

  iter(context)

    返回一个存储在上下文对象中的变量的迭代器。

  len(proxy)

    返回上下文对象中所设的变量的数量。

  keys()

    返回上下文对象中的所有变量的列表。

  values()

    返回上下文对象中所有变量值的列表。

  items()

    返回包含上下文对象中所有变量及其值的 2 元组的列表。

## asyncio 支持

上下文变量在 `asyncio` 中有原生的支持并且无需任何额外配置即可被使用。 例如，以下是一个简单的回显服务器，它使用上下文变量来让远程客户端的地址在处理该客户端的 Task 中可用:

    import asyncio
    import contextvars

    client_addr_var = contextvars.ContextVar('client_addr')

    def render_goodbye():
        # 可直接访问当前处理的客户端地址
        # 而无需显式地将其传给此函数。

        client_addr = client_addr_var.get()
        return f'Good bye, client @ {client_addr}\r\n'.encode()

    async def handle_request(reader, writer):
        addr = writer.transport.get_extra_info('socket').getpeername()
        client_addr_var.set(addr)

        # 现在将可以在我们所调用的任何代码中通过
        # 'client_addr_var.get()' 调用来获取客户端地址。

        while True:
            line = await reader.readline()
            print(line)
            if not line.strip():
                break

        writer.write(b'HTTP/1.1 200 OK\r\n')  # 状态行
        writer.write(b'\r\n')  # 标头
        writer.write(render_goodbye())  # 消息体
        writer.close()

    async def main():
        srv = await asyncio.start_server(
            handle_request, '127.0.0.1', 8081)

        async with srv:
            await srv.serve_forever()

    asyncio.run(main())

    # 要进行测试你可以使用 telnet 或 curl：
    #     telnet 127.0.0.1 8081
    #     curl 127.0.0.1:8081