python在比較新的版本,3.7這個(gè)版本中,引入了一個(gè)比較新的模塊contextvars,從名字上來看的話,它是形容為上下變量的,下文就給大家詳細(xì)的解答下,關(guān)于這方面的內(nèi)容。
Python在3.7的時(shí)候引入了一個(gè)模塊:contextvars,從名字上很容易看出它指的是上下文變量(Context Variables),所以在介紹contextvars之前我們需要先了解一下什么是上下文(Context)。
Context是一個(gè)包含了相關(guān)信息內(nèi)容的對(duì)象,舉個(gè)例子:"比如一部25集的電視劇,直接快進(jìn)到第24集,看到女主角在男主角面前流淚了"。相信此時(shí)你是不知道為什么女主角會(huì)流淚的,因?yàn)槟銢]有看前面幾集的內(nèi)容,缺失了相關(guān)的上下文信息。
所以Context并不是什么神奇的東西,它的作用就是攜帶一些指定的信息。
web框架中的request
我們以fastapi和sanic為例,看看當(dāng)一個(gè)請(qǐng)求過來的時(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ā)請(qǐng)求測(cè)試一下,看看結(jié)果是否正確。
可以看到請(qǐng)求都是成功的,并且對(duì)于fastapi和sanic而言,其request和視圖函數(shù)是綁定在一起的。也就是在請(qǐng)求到來的時(shí)候,會(huì)被封裝成一個(gè)Request對(duì)象、然后傳遞到視圖函數(shù)中。
但對(duì)于flask而言則不是這樣子的,我們看一下flask是如何接收請(qǐng)求參數(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)
我們看到對(duì)于flask而言則是通過import request的方式,如果不需要的話就不用import,當(dāng)然我這里并不是在比較哪種方式好,主要是為了引出我們今天的主題。首先對(duì)于flask而言,如果我再定義一個(gè)視圖函數(shù)的話,那么獲取請(qǐng)求參數(shù)依舊是相同的方式,但是這樣問題就來了,不同的視圖函數(shù)內(nèi)部使用同一個(gè)request,難道不會(huì)發(fā)生沖突嗎?
顯然根據(jù)我們使用flask的經(jīng)驗(yàn)來說,答案是不會(huì)的,至于原因就是ThreadLocal。
ThreadLocal
ThreadLocal,從名字上看可以得出它肯定是和線程相關(guān)的。沒錯(cuò),它專門用來創(chuàng)建局部變量,并且創(chuàng)建的局部變量是和線程綁定的。
import threading #創(chuàng)建一個(gè)local對(duì)象 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í)行g(shù)et函數(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í)候會(huì)綁定在當(dāng)前的線程中,獲取也會(huì)從當(dāng)前的線程中獲取??梢园裈hreadLocal想象成一個(gè)字典:
{ "one":{"value":"ONE"}, "two":{"value":"TWO"} }
更準(zhǔn)確的說key應(yīng)該是線程的id,為了直觀我們就用線程的name代替了,但總之在獲取的時(shí)候只會(huì)獲取綁定在該線程上的變量的值。
而flask內(nèi)部也是這么設(shè)計(jì)的,只不過它沒有直接用threading.local,而是自己實(shí)現(xiàn)了一個(gè)Local類,除了支持線程之外還支持greenlet的協(xié)程,那么它是怎么實(shí)現(xiàn)的呢?首先我們知道flask內(nèi)部存在"請(qǐng)求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è)請(qǐng)求都會(huì)綁定在當(dāng)前的Context中,等到請(qǐng)求結(jié)束之后再銷毀,這個(gè)過程由框架完成,開發(fā)者只需要直接使用request即可。所以請(qǐng)求的具體細(xì)節(jié)流程可以點(diǎn)進(jìn)源碼中查看,這里我們重點(diǎn)關(guān)注一個(gè)對(duì)象: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):
所以我們看到flask內(nèi)部的邏輯其實(shí)很簡(jiǎn)單,通過ThreadLocal實(shí)現(xiàn)了線程之間的隔離。每個(gè)請(qǐng)求都會(huì)綁定在各自的Context中,獲取值的時(shí)候也會(huì)從各自的Context中獲取,因?yàn)樗褪怯脕肀4嫦嚓P(guān)信息的(重要的是同時(shí)也實(shí)現(xiàn)了隔離)。
相應(yīng)此刻你已經(jīng)理解了上下文,但是問題來了,不管是threading.local也好、還是類似于flask自己實(shí)現(xiàn)的Local也罷,它們都是針對(duì)線程的。如果是使用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)識(shí),用于調(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é)程之間是隔離的,不會(huì)受到彼此的影響。
但我們?cè)僮屑?xì)觀察一下,我們是在set_函數(shù)中設(shè)置的值,然后在get函數(shù)中獲取值。可await get()相當(dāng)于是開啟了一個(gè)新的協(xié)程,那么意味著設(shè)置值和獲取值不是在同一個(gè)協(xié)程當(dāng)中。但即便如此,我們依舊可以獲取到希望的結(jié)果。因?yàn)镻ython的協(xié)程是無棧協(xié)程,通過await可以實(shí)現(xiàn)級(jí)聯(lián)調(diào)用。
我們不妨再套一層:
import asyncio import contextvars c=contextvars.ContextVar("只是一個(gè)標(biāo)識(shí),用于調(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)識(shí),用于調(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ù)期的。但是我們?cè)趃et1中將值重新設(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è)置值,然后獲取會(huì)得到最后一次設(shè)置的值。再舉個(gè)栗子:
import asyncio import contextvars c=contextvars.ContextVar("只是一個(gè)標(biāo)識(shí),用于調(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í)候會(huì)執(zhí)行await get2(),然后在里面拿到c.set設(shè)置的值,打印"古明地覺~~~"。但是在get2里面,又將值重新設(shè)置了,所以第二個(gè)print打印的就是新設(shè)置的值。
如果在get之前沒有先set,那么會(huì)拋出一個(gè)LookupError,所以ContextVar支持默認(rèn)值:
import asyncio import contextvars c=contextvars.ContextVar("只是一個(gè)標(biāo)識(shí),用于調(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)識(shí),用于調(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)值,會(huì)拋出LookupError;
只要有一方設(shè)置了,那么會(huì)得到默認(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))。 當(dāng)我們調(diào)用c.set的時(shí)候,其實(shí)會(huì)返回一個(gè)Token對(duì)象: import contextvars c=contextvars.ContextVar("context_var") token=c.set("val") print(token) """ <Token var=<ContextVar name='context_var'at 0x00..>at 0x00...> """
Token對(duì)象還有一個(gè)old_value屬性,它會(huì)返回上一次set設(shè)置的值,如果是第一次set,那么會(huì)返回一個(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對(duì)象有什么作用呢?從目前來看貌似沒太大用處啊,其實(shí)它最大的用處就是和reset搭配使用,可以對(duì)狀態(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í)就會(huì)報(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對(duì)象和設(shè)置的值之間的映射,但是我們不會(huì)直接通過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對(duì)象和設(shè)置的值之間的映射 #它實(shí)現(xiàn)了collections.abc.Mapping接口 #因此我們可以像操作字典一樣操作它 context=contextvars.copy_context() #key就是對(duì)應(yīng)的ContextVar對(duì)象,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è)置的值,那么對(duì)于ContextVar而言只會(huì)在函數(shù)內(nèi)部生效,一旦出了函數(shù),那么還是原來的值。但是對(duì)于Context而言,它是會(huì)受到影響的,即便出了函數(shù),也是新設(shè)置的值,因?yàn)樗苯影褍?nèi)部的字典給修改了。
小結(jié)
以上就是contextvars模塊的用法,在多個(gè)協(xié)程之間傳遞數(shù)據(jù)是非常方便的,并且也是并發(fā)安全的。如果你用過Go的話,你應(yīng)該會(huì)發(fā)現(xiàn)和Go在1.7版本引入的context模塊比較相似,當(dāng)然Go的context模塊功能要更強(qiáng)大一些,除了可以傳遞數(shù)據(jù)之外,對(duì)多個(gè)goroutine的級(jí)聯(lián)管理也提供了非常清蒸的解決方案。
總之對(duì)于contextvars而言,它傳遞的數(shù)據(jù)應(yīng)該是多個(gè)協(xié)程之間需要共享的數(shù)據(jù),像cookie,session,token之類的,比如上游接收了一個(gè)token,然后不斷地向下透?jìng)?。但是不要把本?yīng)該作為函數(shù)參數(shù)的數(shù)據(jù),也通過contextvars來傳遞,這樣就有點(diǎn)本末倒置了。
關(guān)于contextvars的內(nèi)容就為大家介紹到這里了,希望可以為各位讀者帶來幫助。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/127743.html
摘要:正確的思路是等概率隨機(jī)只取出共個(gè)數(shù),每個(gè)數(shù)出現(xiàn)的概率也是相等的隨機(jī)輸出把一段代碼改成,并增加單元測(cè)試。代碼本身很簡(jiǎn)單,即使沒學(xué)過也能看懂,改后的代碼如下但是對(duì)于單元測(cè)試則僅限于聽過的地步,需要用到,好像也有別的模塊。 在拉勾上投了十幾個(gè)公司,大部分都被標(biāo)記為不合適,有兩個(gè)給了面試機(jī)會(huì),其中一個(gè)自己覺得肯定不會(huì)去的,也就沒有去面試,另一個(gè)經(jīng)歷了一輪電話面加一輪現(xiàn)場(chǎng)筆試和面試,在此記錄一下...
摘要:引上下文管理器太極生兩儀,兩儀為陰陽(yáng)。而最常用的則是,即上下文管理器使用上下文管理器用之后的文件讀寫會(huì)變成我們看到用了之后,代碼沒有了創(chuàng)建,也沒有了釋放。實(shí)現(xiàn)上下文管理器我們先感性地對(duì)進(jìn)行猜測(cè)。現(xiàn)實(shí)一個(gè)上下文管理器就是這么簡(jiǎn)單。 Python有什么好學(xué)的這句話可不是反問句,而是問句哦。 主要是煎魚覺得太多的人覺得Python的語法較為簡(jiǎn)單,寫出來的代碼只要符合邏輯,不需要太多的學(xué)習(xí)即可...
摘要:一個(gè)典型的上下文管理器類如下處理異常正如方法名明確告訴我們的,方法負(fù)責(zé)進(jìn)入上下的準(zhǔn)備工作,如果有需要可以返回一個(gè)值,這個(gè)值將會(huì)被賦值給中的??偨Y(jié)都是關(guān)于上下文管理器的內(nèi)容,與協(xié)程關(guān)系不大。 Part 1 傳送門 David Beazley 的博客 PPT 下載地址 在 Part 1 我們已經(jīng)介紹了生成器的定義和生成器的操作,現(xiàn)在讓我們開始使用生成器。Part 2 主要描述了如...
摘要:接下來,我們將注入到函數(shù)的字節(jié)碼中。首先我們來看一下幀的參數(shù)所能提供的信息,如下所示當(dāng)前幀將執(zhí)行的當(dāng)前的操作中的字節(jié)碼字符串的索引經(jīng)過我們的處理我們可以得知之后要被執(zhí)行的操作碼,這對(duì)我們聚合數(shù)據(jù)并展示是相當(dāng)有用的。 原文鏈接: Understanding Python execution from inside: A Python assembly tracer 以下為譯文 最近...
摘要:前言羅子雄如何成為一名優(yōu)秀設(shè)計(jì)師董明偉工程師的入門和進(jìn)階董明偉基于自己實(shí)踐講的知乎為新人提供了很多實(shí)用建議,他推薦的羅子雄如何成為一名優(yōu)秀設(shè)計(jì)師的演講講的非常好,總結(jié)了設(shè)計(jì)師從入門到提高的優(yōu)秀實(shí)踐。 前言 羅子雄:如何成為一名優(yōu)秀設(shè)計(jì)師 董明偉:Python 工程師的入門和進(jìn)階 董明偉基于自己實(shí)踐講的知乎live為Python新人提供了很多實(shí)用建議,他推薦的羅子雄:如何成為一名優(yōu)秀...
閱讀 919·2023-01-14 11:38
閱讀 891·2023-01-14 11:04
閱讀 750·2023-01-14 10:48
閱讀 2039·2023-01-14 10:34
閱讀 956·2023-01-14 10:24
閱讀 833·2023-01-14 10:18
閱讀 506·2023-01-14 10:09
閱讀 583·2023-01-14 10:02