python在比較新的版本,3.7這個版本中,引入了一個比較新的模塊contextvars,從名字上來看的話,它是形容為上下變量的,下文就給大家詳細的解答下,關于這方面的內容。
Python在3.7的時候引入了一個模塊:contextvars,從名字上很容易看出它指的是上下文變量(Context Variables),所以在介紹contextvars之前我們需要先了解一下什么是上下文(Context)。
Context是一個包含了相關信息內容的對象,舉個例子:"比如一部25集的電視劇,直接快進到第24集,看到女主角在男主角面前流淚了"。相信此時你是不知道為什么女主角會流淚的,因為你沒有看前面幾集的內容,缺失了相關的上下文信息。
所以Context并不是什么神奇的東西,它的作用就是攜帶一些指定的信息。
web框架中的request
我們以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
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):
所以我們看到flask內部的邏輯其實很簡單,通過ThreadLocal實現了線程之間的隔離。每個請求都會綁定在各自的Context中,獲取值的時候也會從各自的Context中獲取,因為它就是用來保存相關信息的(重要的是同時也實現了隔離)。
相應此刻你已經理解了上下文,但是問題來了,不管是threading.local也好、還是類似于flask自己實現的Local也罷,它們都是針對線程的。如果是使用async def定義的協程該怎么辦呢?如何實現每個協程的上下文隔離呢?所以終于引出了我們的主角:contextvars。
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:
當ContextVar和get中都沒有指定默認值,會拋出LookupError;
只要有一方設置了,那么會得到默認值;
如果都設置了,那么以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對象還有一個old_value屬性,它會返回上一次set設置的值,如果是第一次set,那么會返回一個<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 那么這個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 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來傳遞,這樣就有點本末倒置了。
關于contextvars的內容就為大家介紹到這里了,希望可以為各位讀者帶來幫助。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/127743.html
摘要:正確的思路是等概率隨機只取出共個數,每個數出現的概率也是相等的隨機輸出把一段代碼改成,并增加單元測試。代碼本身很簡單,即使沒學過也能看懂,改后的代碼如下但是對于單元測試則僅限于聽過的地步,需要用到,好像也有別的模塊。 在拉勾上投了十幾個公司,大部分都被標記為不合適,有兩個給了面試機會,其中一個自己覺得肯定不會去的,也就沒有去面試,另一個經歷了一輪電話面加一輪現場筆試和面試,在此記錄一下...
摘要:引上下文管理器太極生兩儀,兩儀為陰陽。而最常用的則是,即上下文管理器使用上下文管理器用之后的文件讀寫會變成我們看到用了之后,代碼沒有了創建,也沒有了釋放。實現上下文管理器我們先感性地對進行猜測。現實一個上下文管理器就是這么簡單。 Python有什么好學的這句話可不是反問句,而是問句哦。 主要是煎魚覺得太多的人覺得Python的語法較為簡單,寫出來的代碼只要符合邏輯,不需要太多的學習即可...
摘要:一個典型的上下文管理器類如下處理異常正如方法名明確告訴我們的,方法負責進入上下的準備工作,如果有需要可以返回一個值,這個值將會被賦值給中的。總結都是關于上下文管理器的內容,與協程關系不大。 Part 1 傳送門 David Beazley 的博客 PPT 下載地址 在 Part 1 我們已經介紹了生成器的定義和生成器的操作,現在讓我們開始使用生成器。Part 2 主要描述了如...
摘要:接下來,我們將注入到函數的字節碼中。首先我們來看一下幀的參數所能提供的信息,如下所示當前幀將執行的當前的操作中的字節碼字符串的索引經過我們的處理我們可以得知之后要被執行的操作碼,這對我們聚合數據并展示是相當有用的。 原文鏈接: Understanding Python execution from inside: A Python assembly tracer 以下為譯文 最近...
摘要:前言羅子雄如何成為一名優秀設計師董明偉工程師的入門和進階董明偉基于自己實踐講的知乎為新人提供了很多實用建議,他推薦的羅子雄如何成為一名優秀設計師的演講講的非常好,總結了設計師從入門到提高的優秀實踐。 前言 羅子雄:如何成為一名優秀設計師 董明偉:Python 工程師的入門和進階 董明偉基于自己實踐講的知乎live為Python新人提供了很多實用建議,他推薦的羅子雄:如何成為一名優秀...
閱讀 911·2023-01-14 11:38
閱讀 878·2023-01-14 11:04
閱讀 740·2023-01-14 10:48
閱讀 1982·2023-01-14 10:34
閱讀 942·2023-01-14 10:24
閱讀 819·2023-01-14 10:18
閱讀 499·2023-01-14 10:09
閱讀 572·2023-01-14 10:02