Python 在 3.7 的时候引入了一个模块:contextvars,从名字上很容易看出它指的是上下文变量(Context Variables),所以在介绍 contextvars 之前咱们须要先理解一下什么是上下文(Context)。

Context 是一个蕴含了相干信息内容的对象,举个例子:"比方一部 13 集的动漫,你间接点进第八集,看到女主角在男主角背后流泪了"。置信此时你是不晓得为什么女主角会流泪的,因为你没有看后面几集的内容,缺失了相干的上下文信息。

所以 Context 并不是什么神奇的货色,它的作用就是携带一些指定的信息。

web 框架中的 request

咱们以 fastapi 和 sanic 为例,看看当一个申请过去的时候,它们是如何解析的。

# fastapifrom fastapi import FastAPI, Requestimport uvicornapp = FastAPI()@app.get("/index")async def index(request: Request):    name = request.query_params.get("name")    return {"name": name}uvicorn.run("__main__:app", host="127.0.0.1", port=5555)# -------------------------------------------------------# sanicfrom sanic import Sanicfrom sanic.request import Requestfrom sanic import responseapp = Sanic("sanic")@app.get("/index")async def index(request: Request):    name = request.args.get("name")    return response.json({"name": name})app.run(host="127.0.0.1", port=6666) 

发申请测试一下,看看后果是否正确。

能够看到申请都是胜利的,并且对于 fastapi 和 sanic 而言,其 request 和 视图函数是绑定在一起的。也就是在申请到来的时候,会被封装成一个 Request 对象、而后传递到视图函数中。

但对于 flask 而言则不是这样子的,咱们看一下 flask 是如何接管申请参数的。

from flask import Flask, requestapp = Flask("flask")@app.route("/index")def index():    name = request.args.get("name")    return {"name": name}app.run(host="127.0.0.1", port=7777)

咱们看到对于 flask 而言则是通过 import request 的形式,如果不需要的话就不必 import,当然我这里并不是在比拟哪种形式好,次要是为了引出咱们明天的主题。首先对于 flask 而言,如果我再定义一个视图函数的话,那么获取申请参数仍旧是雷同的形式,然而这样问题就来了,不同的视图函数外部应用同一个 request,难道不会发生冲突吗?

显然依据咱们应用 flask 的教训来说,答案是不会的,至于起因就是 ThreadLocal。

ThreadLocal

ThreadLocal,从名字上看能够得出它必定是和线程相干的。没错,它专门用来创立局部变量,并且创立的局部变量是和线程绑定的。

import threading# 创立一个 local 对象local = threading.local()def get():    name = threading.current_thread().name    # 获取绑定在 local 上的 value    value = local.value    print(f"线程: {name}, value: {value}")def set_():    name = threading.current_thread().name    # 为不同的线程设置不同的值    if name == "one":        local.value = "ONE"    elif name == "two":        local.value = "TWO"    # 执行 get 函数    get()t1 = threading.Thread(target=set_, name="one")t2 = threading.Thread(target=set_, name="two")t1.start()t2.start()"""线程 one, value: ONE线程 two, value: TWO"""

能够看到两个线程之间是互不影响的,因为每个线程都有本人惟一的 id,在绑定值的时候会绑定在以后的线程中,获取也会从以后的线程中获取。能够把 ThreadLocal 设想成一个字典:

{    "one": {"value": "ONE"},    "two": {"value": "TWO"}}

更精确的说 key 应该是线程的 id,为了直观咱们就用线程的 name 代替了,但总之在获取的时候只会获取绑定在该线程上的变量的值。

而 flask 外部也是这么设计的,只不过它没有间接用 threading.local,而是本人实现了一个 Local 类,除了反对线程之外还反对 greenlet 的协程,那么它是怎么实现的呢?首先咱们晓得 flask 外部存在 "申请 context" 和 "利用 context",它们都是通过栈来保护的(两个不同的栈)。

# flask/globals.py_request_ctx_stack = LocalStack()_app_ctx_stack = LocalStack()current_app = LocalProxy(_find_app)request = LocalProxy(partial(_lookup_req_object, "request"))session = LocalProxy(partial(_lookup_req_object, "session"))

每个申请都会绑定在以后的 Context 中,等到申请完结之后再销毁,这个过程由框架实现,开发者只须要间接应用 request 即可。所以申请的具体细节流程能够点进源码中查看,这里咱们重点关注一个对象:werkzeug.local.Local,也就是下面说的 Local 类,它是变量的设置和获取的要害。间接看局部源码:

# werkzeug/local.pyclass Local(object):    __slots__ = ("__storage__", "__ident_func__")    def __init__(self):        # 外部有两个成员:__storage__ 是一个字典,值就存在这外面        # __ident_func__ 只须要晓得它是用来获取线程 id 的即可        object.__setattr__(self, "__storage__", {})        object.__setattr__(self, "__ident_func__", get_ident)    def __call__(self, proxy):        """Create a proxy for a name."""        return LocalProxy(self, proxy)    def __release_local__(self):        self.__storage__.pop(self.__ident_func__(), None)    def __getattr__(self, name):        try:            # 依据线程 id 失去 value(一个字典)            # 而后再依据 name 获取对应的值            # 所以只会获取绑定在以后线程上的值            return self.__storage__[self.__ident_func__()][name]        except KeyError:            raise AttributeError(name)    def __setattr__(self, name, value):        ident = self.__ident_func__()        storage = self.__storage__        try:            # 将线程 id 作为 key,而后将值设置在对应的字典中            # 所以只会将值设置在以后的线程中            storage[ident][name] = value        except KeyError:            storage[ident] = {name: value}    def __delattr__(self, name):        # 删除逻辑也很简略        try:            del self.__storage__[self.__ident_func__()][name]        except KeyError:            raise AttributeError(name)

所以咱们看到 flask 外部的逻辑其实很简略,通过 ThreadLocal 实现了线程之间的隔离。每个申请都会绑定在各自的 Context 中,获取值的时候也会从各自的 Context 中获取,因为它就是用来保留相干信息的(重要的是同时也实现了隔离)。

相应此刻你曾经了解了上下文,然而问题来了,不论是 threading.local 也好、还是相似于 flask 本人实现的 Local 也罢,它们都是针对线程的。如果是应用 async def 定义的协程该怎么办呢?如何实现每个协程的上下文隔离呢?所以终于引出了咱们的配角:contextvars。

contextvars

该模块提供了一组接口,可用于在协程中治理、设置、拜访部分 Context 的状态。

import asyncioimport contextvarsc = contextvars.ContextVar("只是一个标识, 用于调试")async def get():    # 获取值    return c.get() + "~~~"async def set_(val):    # 设置值    c.set(val)    print(await get())async def main():    coro1 = set_("协程1")    coro2 = set_("协程2")    await asyncio.gather(coro1, coro2)asyncio.run(main())"""协程1~~~协程2~~~"""

ContextVar 提供了两个办法,别离是 get 和 set,用于获取值和设置值。咱们看到成果和 ThreadingLocal 相似,数据在协程之间是隔离的,不会受到彼此的影响。

但咱们再仔细观察一下,咱们是在 set_ 函数中设置的值,而后在 get 函数中获取值。可 await get() 相当于是开启了一个新的协程,那么意味着设置值和获取值不是在同一个协程当中。但即便如此,咱们仍旧能够获取到心愿的后果。因为 Python 的协程是无栈协程,通过 await 能够实现级联调用。

咱们无妨再套一层:

import asyncioimport contextvarsc = contextvars.ContextVar("只是一个标识, 用于调试")async def get1():    return await get2()async def get2():    return c.get() + "~~~"async def set_(val):    # 设置值    c.set(val)    print(await get1())    print(await get2())async def main():    coro1 = set_("协程1")    coro2 = set_("协程2")    await asyncio.gather(coro1, coro2)asyncio.run(main())"""协程1~~~协程1~~~协程2~~~协程2~~~"""

咱们看到不论是 await get1() 还是 await get2(),失去的都是 set_ 中设置的后果,阐明它是能够嵌套的。

并且在这个过程当中,能够从新设置值。

import asyncioimport contextvarsc = contextvars.ContextVar("只是一个标识, 用于调试")async def get1():    c.set("从新设置")    return await get2()async def get2():    return c.get() + "~~~"async def set_(val):    # 设置值    c.set(val)    print("------------")    print(await get2())    print(await get1())    print(await get2())    print("------------")async def main():    coro1 = set_("协程1")    coro2 = set_("协程2")    await asyncio.gather(coro1, coro2)asyncio.run(main())"""------------协程1~~~从新设置~~~从新设置~~~------------------------协程2~~~从新设置~~~从新设置~~~------------"""

先 await get2() 失去的就是 set_ 函数中设置的值,这是合乎预期的。然而咱们在 get1 中将值从新设置了,那么之后不论是 await get1() 还是间接 await get2(),失去的都是新设置的值。

这也阐明了,一个协程外部 await 另一个协程,另一个协程外部 await 另另一个协程,不论套娃(await)多少次,它们获取的值都是一样的。并且在任意一个协程外部都能够从新设置值,而后获取会失去最初一次设置的值。再举个栗子:

import asyncioimport contextvarsc = contextvars.ContextVar("只是一个标识, 用于调试")async def get1():    return await get2()async def get2():    val = c.get() + "~~~"    c.set("从新设置啦")    return valasync def set_(val):    # 设置值    c.set(val)    print(await get1())    print(c.get())async def main():    coro = set_("古明地觉")    await coroasyncio.run(main())"""古明地觉~~~从新设置啦"""

await get1() 的时候会执行 await get2(),而后在外面拿到 c.set 设置的值,打印 "古明地觉~~~"。然而在 get2 外面,又将值从新设置了,所以第二个 print 打印的就是新设置的值。\

如果在 get 之前没有先 set,那么会抛出一个 LookupError,所以 ContextVar 反对默认值:

import asyncioimport contextvarsc = contextvars.ContextVar("只是一个标识, 用于调试",                           default="哼哼")async def set_(val):    print(c.get())    c.set(val)    print(c.get())async def main():    coro = set_("古明地觉")    await coroasyncio.run(main())"""哼哼古明地觉"""

除了在 ContextVar 中指定默认值之外,也能够在 get 中指定:

import asyncioimport contextvarsc = contextvars.ContextVar("只是一个标识, 用于调试",                           default="哼哼")async def set_(val):    print(c.get("古明地恋"))    c.set(val)    print(c.get())async def main():    coro = set_("古明地觉")    await coroasyncio.run(main())"""古明地恋古明地觉"""

所以论断如下,如果在 c.set 之前应用 c.get:

  • 当 ContextVar 和 get 中都没有指定默认值,会抛出 LookupError;
  • 只有有一方设置了,那么会失去默认值;
  • 如果都设置了,那么以 get 为准;

如果 c.get 之前执行了 c.set,那么无论 ContextVar 和 get 有没有指定默认值,获取到的都是 c.set 设置的值。

所以总的来说还是比拟好了解的,并且 ContextVar 除了能够作用在协程下面,它也能够用在线程下面。没错,它能够代替 threading.local,咱们来试一下:

import threadingimport contextvarsc = contextvars.ContextVar("context_var")def get():    name = threading.current_thread().name    value = c.get()    print(f"线程 {name}, value: {value}")def set_():    name = threading.current_thread().name    if name == "one":        c.set("ONE")    elif name == "two":        c.set("TWO")    get()t1 = threading.Thread(target=set_, name="one")t2 = threading.Thread(target=set_, name="two")t1.start()t2.start()"""线程 one, value: ONE线程 two, value: TWO"""

和 threading.local 的体现是一样的,然而更倡议应用 ContextVars。不过前者能够绑定任意多个值,而后者只能绑定一个值(能够通过传递字典的形式解决这一点)。

c.Token

当咱们调用 c.set 的时候,其实会返回一个 Token 对象:

import contextvarsc = contextvars.ContextVar("context_var")token = c.set("val")print(token)"""<Token var=<ContextVar name='context_var' at 0x00..> at 0x00...>"""

Token 对象有一个 var 属性,它是只读的,会返回指向此 token 的 ContextVar 对象。

import contextvarsc = contextvars.ContextVar("context_var")token = c.set("val")print(token.var is c)  # Trueprint(token.var.get())  # valprint(    token.var.set("val2").var.set("val3").var is c)  # Trueprint(c.get())  # val3

Token 对象还有一个 old_value 属性,它会返回上一次 set 设置的值,如果是第一次 set,那么会返回一个 <Token.MISSING>。

import contextvarsc = contextvars.ContextVar("context_var")token = c.set("val")# 该 token 是第一次 c.set 所返回的# 在此之前没有 set,所以 old_value 是 <Token.MISSING>print(token.old_value)  # <Token.MISSING>token = c.set("val2")print(c.get())  # val2# 返回上一次 set 的值print(token.old_value)  # val

那么这个 Token 对象有什么作用呢?从目前来看貌似没太大用处啊,其实它最大的用途就是和 reset 搭配应用,能够对状态进行重置。

import contextvars#### c = contextvars.ContextVar("context_var")token = c.set("val")# 显然是能够获取的print(c.get())  # val# 将其重置为 token 之前的状态# 但这个 token 是第一次 set 返回的# 那么之前就相当于没有 set 了c.reset(token)try:    c.get()  # 此时就会报错except LookupError:    print("报错啦")  # 报错啦# 然而咱们能够指定默认值print(c.get("默认值"))  # 默认值

contextvars.Context

它负责保留 ContextVars 对象和设置的值之间的映射,然而咱们不会间接通过 contextvars.Context 来创立,而是通过 contentvars.copy_context 函数来创立。

import contextvarsc1 = contextvars.ContextVar("context_var1")c1.set("val1")c2 = contextvars.ContextVar("context_var2")c2.set("val2")# 此时失去的是所有 ContextVar 对象和设置的值之间的映射# 它实现了 collections.abc.Mapping 接口# 因而咱们能够像操作字典一样操作它context = contextvars.copy_context()# key 就是对应的 ContextVar 对象,value 就是设置的值print(context[c1])  # val1print(context[c2])  # val2for ctx, value in context.items():    print(ctx.get(), ctx.name, value)    """    val1 context_var1 val1    val2 context_var2 val2    """print(len(context))  # 2

除此之外,context 还有一个 run 办法:

import contextvarsc1 = contextvars.ContextVar("context_var1")c1.set("val1")c2 = contextvars.ContextVar("context_var2")c2.set("val2")context = contextvars.copy_context()def change(val1, val2):    c1.set(val1)    c2.set(val2)    print(c1.get(), context[c1])    print(c2.get(), context[c2])# 在 change 函数外部,从新设置值# 而后外面打印的也是新设置的值context.run(change, "VAL1", "VAL2")"""VAL1 VAL1VAL2 VAL2"""print(c1.get(), context[c1])print(c2.get(), context[c2])"""val1 VAL1val2 VAL2"""

咱们看到 run 办法接管一个 callable,如果在外面批改了 ContextVar 实例设置的值,那么对于 ContextVar 而言只会在函数外部失效,一旦出了函数,那么还是原来的值。然而对于 Context 而言,它是会受到影响的,即使出了函数,也是新设置的值,因为它间接把外部的字典给批改了。

小结

以上就是 contextvars 模块的用法,在多个协程之间传递数据是十分不便的,并且也是并发平安的。如果你用过 Go 的话,你应该会发现和 Go 在 1.7 版本引入的 context 模块比拟类似,当然 Go 的 context 模块性能要更弱小一些,除了能够传递数据之外,对多个 goroutine 的级联治理也提供了十分清蒸的解决方案。

总之对于 contextvars 而言,它传递的数据应该是多个协程之间须要共享的数据,像 cookie, session, token 之类的,比方上游接管了一个 token,而后一直地向下透传。然而不要把本应该作为函数参数的数据,也通过 contextvars 来传递,这样就有点轻重倒置了。

以上就是本次分享的所有内容,如果你感觉文章还不错,欢送关注公众号:Python编程学习圈,每日干货分享,发送“J”还可支付大量学习材料,内容笼罩Python电子书、教程、数据库编程、Django,爬虫,云计算等等。或是返回编程学习网,理解更多编程技术常识。