本篇文章為大家帶來了關於Python的相關知識,Python 在3.7 的時候引入了一個模組:contextvars,從名字上很容易看出它指的是上下文變量,下面就來跟大家詳細講講如何使用contextvars實現管理上下文變量,希望對大家有幫助。
【相關推薦:Python3影片教學 】
Python 在3.7 的時候引入了一個模組:contextvars,從名字上很容易看出它指的是上下文變數(Context Variables),所以在介紹contextvars 之前我們需要先了解什麼是上下文(Context)。
Context 是一個包含了相關資訊內容的對象,舉個例子:"例如一部 13 集的動漫,你直接點進第八集,看到女主角在男主角面前流淚了"。相信此時你是不知道為什麼女主角會流淚的,因為你沒有看前面幾集的內容,缺少了相關的上下文資訊。
所以 Context 並不是什麼神奇的東西,它的作用就是攜帶一些指定的資訊。
我們以 fastapi 和 sanic 為例,看看當一個請求過來的時候,它們是如何解析的。
# fastapi from fastapi import FastAPI, Request import uvicorn app = 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) # ------------------------------------------------------- # sanic from sanic import Sanic from sanic.request import Request from sanic import response app = 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, request app = 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,從名字上看可以得到它肯定是和執行緒相關的。沒錯,它專門用來創建局部變量,並且創建的局部變量是和線程綁定的。
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.py class 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。
該模組提供了一組接口,可用於在協程中管理、設定、存取局部 Context 的狀態。
import asyncio import contextvars c = 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 asyncio import contextvars c = 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 asyncio import contextvars c = 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 asyncio import contextvars c = contextvars.ContextVar("只是一个标识, 用于调试") async def get1(): return await get2() async def get2(): val = c.get() + "~~~" c.set("重新设置啦") return val async def set_(val): # 设置值 c.set(val) print(await get1()) print(c.get()) async def main(): coro = set_("古明地觉") await coro asyncio.run(main()) """ 古明地觉~~~ 重新设置啦 """
await get1() 的时候会执行 await get2(),然后在里面拿到 c.set 设置的值,打印 "古明地觉~~~"。但是在 get2 里面,又将值重新设置了,所以第二个 print 打印的就是新设置的值。\
如果在 get 之前没有先 set,那么会抛出一个 LookupError,所以 ContextVar 支持默认值:
import asyncio import contextvars c = contextvars.ContextVar("只是一个标识, 用于调试", default="哼哼") async def set_(val): print(c.get()) c.set(val) print(c.get()) async def main(): coro = set_("古明地觉") await coro asyncio.run(main()) """ 哼哼 古明地觉 """
除了在 ContextVar 中指定默认值之外,也可以在 get 中指定:
import asyncio import contextvars c = contextvars.ContextVar("只是一个标识, 用于调试", default="哼哼") async def set_(val): print(c.get("古明地恋")) c.set(val) print(c.get()) async def main(): coro = set_("古明地觉") await coro asyncio.run(main()) """ 古明地恋 古明地觉 """
所以结论如下,如果在 c.set 之前使用 c.get:
如果 c.get 之前执行了 c.set,那么无论 ContextVar 和 get 有没有指定默认值,获取到的都是 c.set 设置的值。
所以总的来说还是比较好理解的,并且 ContextVar 除了可以作用在协程上面,它也可以用在线程上面。没错,它可以替代 threading.local,我们来试一下:
import threading import contextvars c = 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.set 的时候,其实会返回一个 Token 对象:
import contextvars c = 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 contextvars c = contextvars.ContextVar("context_var") token = c.set("val") print(token.var is c) # True print(token.var.get()) # val print( token.var.set("val2").var.set("val3").var is c ) # True print(c.get()) # val3
Token 对象还有一个 old_value 属性,它会返回上一次 set 设置的值,如果是第一次 set,那么会返回一个
import contextvars c = 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 对象和设置的值之间的映射,但是我们不会直接通过 contextvars.Context 来创建,而是通过 contentvars.copy_context 函数来创建。
import contextvars c1 = 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]) # val1 print(context[c2]) # val2 for 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 contextvars c1 = 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 VAL1 VAL2 VAL2 """ print(c1.get(), context[c1]) print(c2.get(), context[c2]) """ val1 VAL1 val2 VAL2 """
我们看到 run 方法接收一个 callable,如果在里面修改了 ContextVar 实例设置的值,那么对于 ContextVar 而言只会在函数内部生效,一旦出了函数,那么还是原来的值。但是对于 Context 而言,它是会受到影响的,即便出了函数,也是新设置的值,因为它直接把内部的字典给修改了。
以上就是 contextvars 模块的用法,在多个协程之间传递数据是非常方便的,并且也是并发安全的。如果你用过 Go 的话,你应该会发现和 Go 在 1.7 版本引入的 context 模块比较相似,当然 Go 的 context 模块功能要更强大一些,除了可以传递数据之外,对多个 goroutine 的级联管理也提供了非常清蒸的解决方案。
总之对于 contextvars 而言,它传递的数据应该是多个协程之间需要共享的数据,像 cookie, session, token 之类的,比如上游接收了一个 token,然后不断地向下透传。但是不要把本应该作为函数参数的数据,也通过 contextvars 来传递,这样就有点本末倒置了。
【相关推荐:Python3视频教程 】
以上是Python怎麼利用contextvars實作管理上下文變數的詳細內容。更多資訊請關注PHP中文網其他相關文章!