Python利用contextvars實(shí)現(xiàn)管理上下文變量
Python 在 3.7 的時(shí)候引入了一個(gè)模塊:contextvars,從名字上很容易看出它指的是上下文變量(Context Variables),所以在介紹 contextvars 之前我們需要先了解一下什么是上下文(Context)。
Context 是一個(gè)包含了相關(guān)信息內(nèi)容的對象,舉個(gè)例子:"比如一部 13 集的動(dòng)漫,你直接點(diǎn)進(jìn)第八集,看到女主角在男主角面前流淚了"。相信此時(shí)你是不知道為什么女主角會流淚的,因?yàn)槟銢]有看前面幾集的內(nèi)容,缺失了相關(guān)的上下文信息。
所以 Context 并不是什么神奇的東西,它的作用就是攜帶一些指定的信息。
web 框架中的 request
我們以 fastapi 和 sanic 為例,看看當(dāng)一個(gè)請求過來的時(shí)候,它們是如何解析的。
#?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)發(fā)請求測試一下,看看結(jié)果是否正確。
可以看到請求都是成功的,并且對于 fastapi 和 sanic 而言,其 request 和 視圖函數(shù)是綁定在一起的。也就是在請求到來的時(shí)候,會被封裝成一個(gè) Request 對象、然后傳遞到視圖函數(shù)中。
但對于 flask 而言則不是這樣子的,我們看一下 flask 是如何接收請求參數(shù)的。
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,當(dāng)然我這里并不是在比較哪種方式好,主要是為了引出我們今天的主題。首先對于 flask 而言,如果我再定義一個(gè)視圖函數(shù)的話,那么獲取請求參數(shù)依舊是相同的方式,但是這樣問題就來了,不同的視圖函數(shù)內(nèi)部使用同一個(gè) request,難道不會發(fā)生沖突嗎?
顯然根據(jù)我們使用 flask 的經(jīng)驗(yàn)來說,答案是不會的,至于原因就是 ThreadLocal。
ThreadLocal
ThreadLocal,從名字上看可以得出它肯定是和線程相關(guān)的。沒錯(cuò),它專門用來創(chuàng)建局部變量,并且創(chuàng)建的局部變量是和線程綁定的。
import?threading
#?創(chuàng)建一個(gè)?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
????#?為不同的線程設(shè)置不同的值
????if?name?==?"one":
????????local.value?=?"ONE"
????elif?name?==?"two":
????????local.value?=?"TWO"
????#?執(zhí)行?get?函數(shù)
????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
"""可以看到兩個(gè)線程之間是互不影響的,因?yàn)槊總€(gè)線程都有自己唯一的 id,在綁定值的時(shí)候會綁定在當(dāng)前的線程中,獲取也會從當(dāng)前的線程中獲取??梢园?ThreadLocal 想象成一個(gè)字典:
{
????"one":?{"value":?"ONE"},
????"two":?{"value":?"TWO"}
}更準(zhǔn)確的說 key 應(yīng)該是線程的 id,為了直觀我們就用線程的 name 代替了,但總之在獲取的時(shí)候只會獲取綁定在該線程上的變量的值。
而 flask 內(nèi)部也是這么設(shè)計(jì)的,只不過它沒有直接用 threading.local,而是自己實(shí)現(xiàn)了一個(gè) Local 類,除了支持線程之外還支持 greenlet 的協(xié)程,那么它是怎么實(shí)現(xiàn)的呢?首先我們知道 flask 內(nèi)部存在 "請求 context" 和 "應(yīng)用 context",它們都是通過棧來維護(hù)的(兩個(gè)不同的棧)。
#?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"))
每個(gè)請求都會綁定在當(dāng)前的 Context 中,等到請求結(jié)束之后再銷毀,這個(gè)過程由框架完成,開發(fā)者只需要直接使用 request 即可。所以請求的具體細(xì)節(jié)流程可以點(diǎn)進(jìn)源碼中查看,這里我們重點(diǎn)關(guān)注一個(gè)對象:werkzeug.local.Local,也就是上面說的 Local 類,它是變量的設(shè)置和獲取的關(guān)鍵。直接看部分源碼:
#?werkzeug/local.py
class?Local(object):
????__slots__?=?("__storage__",?"__ident_func__")
????def?__init__(self):
????????#?內(nèi)部有兩個(gè)成員:__storage__?是一個(gè)字典,值就存在這里面
????????#?__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:
????????????#?根據(jù)線程?id?得到?value(一個(gè)字典)
????????????#?然后再根據(jù)?name?獲取對應(yīng)的值
????????????#?所以只會獲取綁定在當(dāng)前線程上的值
????????????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,然后將值設(shè)置在對應(yīng)的字典中
????????????#?所以只會將值設(shè)置在當(dāng)前的線程中
????????????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 內(nèi)部的邏輯其實(shí)很簡單,通過 ThreadLocal 實(shí)現(xiàn)了線程之間的隔離。每個(gè)請求都會綁定在各自的 Context 中,獲取值的時(shí)候也會從各自的 Context 中獲取,因?yàn)樗褪怯脕肀4嫦嚓P(guān)信息的(重要的是同時(shí)也實(shí)現(xiàn)了隔離)。
相應(yīng)此刻你已經(jīng)理解了上下文,但是問題來了,不管是 threading.local 也好、還是類似于 flask 自己實(shí)現(xiàn)的 Local 也罷,它們都是針對線程的。如果是使用 async def 定義的協(xié)程該怎么辦呢?如何實(shí)現(xiàn)每個(gè)協(xié)程的上下文隔離呢?所以終于引出了我們的主角:contextvars。
contextvars
該模塊提供了一組接口,可用于在協(xié)程中管理、設(shè)置、訪問局部 Context 的狀態(tài)。
import?asyncio
import?contextvars
c?=?contextvars.ContextVar("只是一個(gè)標(biāo)識,?用于調(diào)試")
async?def?get():
????#?獲取值
????return?c.get()?+?"~~~"
async?def?set_(val):
????#?設(shè)置值
????c.set(val)
????print(await?get())
async?def?main():
????coro1?=?set_("協(xié)程1")
????coro2?=?set_("協(xié)程2")
????await?asyncio.gather(coro1,?coro2)
asyncio.run(main())
"""
協(xié)程1~~~
協(xié)程2~~~
"""ContextVar 提供了兩個(gè)方法,分別是 get 和 set,用于獲取值和設(shè)置值。我們看到效果和 ThreadingLocal 類似,數(shù)據(jù)在協(xié)程之間是隔離的,不會受到彼此的影響。
但我們再仔細(xì)觀察一下,我們是在 set_ 函數(shù)中設(shè)置的值,然后在 get 函數(shù)中獲取值。可 await get() 相當(dāng)于是開啟了一個(gè)新的協(xié)程,那么意味著設(shè)置值和獲取值不是在同一個(gè)協(xié)程當(dāng)中。但即便如此,我們依舊可以獲取到希望的結(jié)果。因?yàn)?Python 的協(xié)程是無棧協(xié)程,通過 await 可以實(shí)現(xiàn)級聯(lián)調(diào)用。
我們不妨再套一層:
import?asyncio
import?contextvars
c?=?contextvars.ContextVar("只是一個(gè)標(biāo)識,?用于調(diào)試")
async?def?get1():
????return?await?get2()
async?def?get2():
????return?c.get()?+?"~~~"
async?def?set_(val):
????#?設(shè)置值
????c.set(val)
????print(await?get1())
????print(await?get2())
async?def?main():
????coro1?=?set_("協(xié)程1")
????coro2?=?set_("協(xié)程2")
????await?asyncio.gather(coro1,?coro2)
asyncio.run(main())
"""
協(xié)程1~~~
協(xié)程1~~~
協(xié)程2~~~
協(xié)程2~~~
"""我們看到不管是 await get1() 還是 await get2(),得到的都是 set_ 中設(shè)置的結(jié)果,說明它是可以嵌套的。
并且在這個(gè)過程當(dāng)中,可以重新設(shè)置值。
import?asyncio
import?contextvars
c?=?contextvars.ContextVar("只是一個(gè)標(biāo)識,?用于調(diào)試")
async?def?get1():
????c.set("重新設(shè)置")
????return?await?get2()
async?def?get2():
????return?c.get()?+?"~~~"
async?def?set_(val):
????#?設(shè)置值
????c.set(val)
????print("------------")
????print(await?get2())
????print(await?get1())
????print(await?get2())
????print("------------")
async?def?main():
????coro1?=?set_("協(xié)程1")
????coro2?=?set_("協(xié)程2")
????await?asyncio.gather(coro1,?coro2)
asyncio.run(main())
"""
------------
協(xié)程1~~~
重新設(shè)置~~~
重新設(shè)置~~~
------------
------------
協(xié)程2~~~
重新設(shè)置~~~
重新設(shè)置~~~
------------
"""先 await get2() 得到的就是 set_ 函數(shù)中設(shè)置的值,這是符合預(yù)期的。但是我們在 get1 中將值重新設(shè)置了,那么之后不管是 await get1() 還是直接 await get2(),得到的都是新設(shè)置的值。
這也說明了,一個(gè)協(xié)程內(nèi)部 await 另一個(gè)協(xié)程,另一個(gè)協(xié)程內(nèi)部 await 另另一個(gè)協(xié)程,不管套娃(await)多少次,它們獲取的值都是一樣的。并且在任意一個(gè)協(xié)程內(nèi)部都可以重新設(shè)置值,然后獲取會得到最后一次設(shè)置的值。再舉個(gè)栗子:
import?asyncio
import?contextvars
c?=?contextvars.ContextVar("只是一個(gè)標(biāo)識,?用于調(diào)試")
async?def?get1():
????return?await?get2()
async?def?get2():
????val?=?c.get()?+?"~~~"
????c.set("重新設(shè)置啦")
????return?val
async?def?set_(val):
????#?設(shè)置值
????c.set(val)
????print(await?get1())
????print(c.get())
async?def?main():
????coro?=?set_("古明地覺")
????await?coro
asyncio.run(main())
"""
古明地覺~~~
重新設(shè)置啦
"""await get1() 的時(shí)候會執(zhí)行 await get2(),然后在里面拿到 c.set 設(shè)置的值,打印 "古明地覺~~~"。但是在 get2 里面,又將值重新設(shè)置了,所以第二個(gè) print 打印的就是新設(shè)置的值。\
如果在 get 之前沒有先 set,那么會拋出一個(gè) LookupError,所以 ContextVar 支持默認(rèn)值:
import?asyncio
import?contextvars
c?=?contextvars.ContextVar("只是一個(gè)標(biāo)識,?用于調(diào)試",
???????????????????????????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 中指定默認(rèn)值之外,也可以在 get 中指定:
import?asyncio
import?contextvars
c?=?contextvars.ContextVar("只是一個(gè)標(biāo)識,?用于調(diào)試",
???????????????????????????default="哼哼")
async?def?set_(val):
????print(c.get("古明地戀"))
????c.set(val)
????print(c.get())
async?def?main():
????coro?=?set_("古明地覺")
????await?coro
asyncio.run(main())
"""
古明地戀
古明地覺
"""所以結(jié)論如下,如果在 c.set 之前使用 c.get:
- 當(dāng) ContextVar 和 get 中都沒有指定默認(rèn)值,會拋出 LookupError;
- 只要有一方設(shè)置了,那么會得到默認(rèn)值;
- 如果都設(shè)置了,那么以 get 為準(zhǔn);
如果 c.get 之前執(zhí)行了 c.set,那么無論 ContextVar 和 get 有沒有指定默認(rèn)值,獲取到的都是 c.set 設(shè)置的值。
所以總的來說還是比較好理解的,并且 ContextVar 除了可以作用在協(xié)程上面,它也可以用在線程上面。沒錯(cuò),它可以替代 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 的表現(xiàn)是一樣的,但是更建議使用 ContextVars。不過前者可以綁定任意多個(gè)值,而后者只能綁定一個(gè)值(可以通過傳遞字典的方式解決這一點(diǎn))。
c.Token
當(dāng)我們調(diào)用 c.set 的時(shí)候,其實(shí)會返回一個(gè) 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 對象有一個(gè) 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())??#?val3Token 對象還有一個(gè) old_value 屬性,它會返回上一次 set 設(shè)置的值,如果是第一次 set,那么會返回一個(gè) <Token.MISSING>。
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那么這個(gè) Token 對象有什么作用呢?從目前來看貌似沒太大用處啊,其實(shí)它最大的用處就是和 reset 搭配使用,可以對狀態(tài)進(jìn)行重置。
import?contextvars
####
c?=?contextvars.ContextVar("context_var")
token?=?c.set("val")
#?顯然是可以獲取的
print(c.get())??#?val
#?將其重置為?token?之前的狀態(tài)
#?但這個(gè)?token?是第一次?set?返回的
#?那么之前就相當(dāng)于沒有?set?了
c.reset(token)
try:
????c.get()??#?此時(shí)就會報(bào)錯(cuò)
except?LookupError:
????print("報(bào)錯(cuò)啦")??#?報(bào)錯(cuò)啦
#?但是我們可以指定默認(rèn)值
print(c.get("默認(rèn)值"))??#?默認(rèn)值contextvars.Context
它負(fù)責(zé)保存 ContextVars 對象和設(shè)置的值之間的映射,但是我們不會直接通過 contextvars.Context 來創(chuàng)建,而是通過 contentvars.copy_context 函數(shù)來創(chuàng)建。
import?contextvars
c1?=?contextvars.ContextVar("context_var1")
c1.set("val1")
c2?=?contextvars.ContextVar("context_var2")
c2.set("val2")
#?此時(shí)得到的是所有?ContextVar?對象和設(shè)置的值之間的映射
#?它實(shí)現(xiàn)了?collections.abc.Mapping?接口
#?因此我們可以像操作字典一樣操作它
context?=?contextvars.copy_context()
#?key?就是對應(yīng)的?ContextVar?對象,value?就是設(shè)置的值
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 還有一個(gè) 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?函數(shù)內(nèi)部,重新設(shè)置值
#?然后里面打印的也是新設(shè)置的值
context.run(change,?"VAL1",?"VAL2")
"""
VAL1?VAL1
VAL2?VAL2
"""
print(c1.get(),?context[c1])
print(c2.get(),?context[c2])
"""
val1?VAL1
val2?VAL2
"""我們看到 run 方法接收一個(gè) callable,如果在里面修改了 ContextVar 實(shí)例設(shè)置的值,那么對于 ContextVar 而言只會在函數(shù)內(nèi)部生效,一旦出了函數(shù),那么還是原來的值。但是對于 Context 而言,它是會受到影響的,即便出了函數(shù),也是新設(shè)置的值,因?yàn)樗苯影褍?nèi)部的字典給修改了。
小結(jié)
以上就是 contextvars 模塊的用法,在多個(gè)協(xié)程之間傳遞數(shù)據(jù)是非常方便的,并且也是并發(fā)安全的。如果你用過 Go 的話,你應(yīng)該會發(fā)現(xiàn)和 Go 在 1.7 版本引入的 context 模塊比較相似,當(dāng)然 Go 的 context 模塊功能要更強(qiáng)大一些,除了可以傳遞數(shù)據(jù)之外,對多個(gè) goroutine 的級聯(lián)管理也提供了非常清蒸的解決方案。
總之對于 contextvars 而言,它傳遞的數(shù)據(jù)應(yīng)該是多個(gè)協(xié)程之間需要共享的數(shù)據(jù),像 cookie, session, token 之類的,比如上游接收了一個(gè) token,然后不斷地向下透傳。但是不要把本應(yīng)該作為函數(shù)參數(shù)的數(shù)據(jù),也通過 contextvars 來傳遞,這樣就有點(diǎn)本末倒置了。
到此這篇關(guān)于Python利用contextvars實(shí)現(xiàn)管理上下文變量的文章就介紹到這了,更多相關(guān)Python contextvars管理變量內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Laravel+Dingo/Api 自定義響應(yīng)的實(shí)現(xiàn)
這篇文章主要介紹了Laravel+Dingo/Api 自定義響應(yīng)的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-02-02
Pandas數(shù)據(jù)分析之pandas文本處理
這篇文章主要介紹了Pandas數(shù)據(jù)分析之pandas文本處理,pandas對文本數(shù)據(jù)也有很多便捷處理方法,可以不用寫循環(huán),向量化操作運(yùn)算速度快,還可以進(jìn)行高級的正則表達(dá)式,各種復(fù)雜的邏輯篩選和匹配提取信息2022-08-08
TensorFlow Session使用的兩種方法小結(jié)
今天小編就為大家分享一篇TensorFlow Session使用的兩種方法小結(jié),具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-07-07
Python使用Socket實(shí)現(xiàn)簡單聊天程序
這篇文章主要介紹了Python使用Socket實(shí)現(xiàn)簡單聊天程序,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-02-02
python實(shí)現(xiàn)學(xué)員管理系統(tǒng)(面向?qū)ο蟀?
這篇文章主要為大家詳細(xì)介紹了python面向?qū)ο蟀鎸?shí)現(xiàn)學(xué)員管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-06-06
python實(shí)現(xiàn)微信小程序用戶登錄、模板推送
這篇文章主要為大家詳細(xì)介紹了python實(shí)現(xiàn)微信小程序用戶登錄、模板推送,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-08-08
Python實(shí)現(xiàn)提取給定網(wǎng)頁內(nèi)的所有鏈接
這篇文章主要和大家分享一個(gè)實(shí)用的Python腳本,可以實(shí)現(xiàn)從給定的網(wǎng)頁中檢索所有鏈接,并將其保存為txt文件,需要的小伙伴可以收藏一下2023-05-05
pytorch實(shí)現(xiàn)建立自己的數(shù)據(jù)集(以mnist為例)
今天小編就為大家分享一篇pytorch實(shí)現(xiàn)建立自己的數(shù)據(jù)集(以mnist為例),具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-01-01

