成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

怎么使用python contextvs實(shí)現(xiàn)管理上下文

89542767 / 433人閱讀


  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)容的對象,舉個(gè)例子:"比如一部25集的電視劇,直接快進(jìn)到第24集,看到女主角在男主角面前流淚了"。相信此時(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í)行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í)候會綁定在當(dāng)前的線程中,獲取也會從當(dāng)前的線程中獲取??梢园裈hreadLocal想象成一個(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):

 

  所以我們看到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ù)中獲取值??蒩wait get()相當(dāng)于是開啟了一個(gè)新的協(xié)程,那么意味著設(shè)置值和獲取值不是在同一個(gè)協(xié)程當(dāng)中。但即便如此,我們依舊可以獲取到希望的結(jié)果。因?yàn)镻ython的協(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))。
  當(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è)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)于contextvars的內(nèi)容就為大家介紹到這里了,希望可以為各位讀者帶來幫助。


文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/127743.html

相關(guān)文章

  • 【FAILED】記一次Python后端開發(fā)面試的經(jīng)歷

    摘要:正確的思路是等概率隨機(jī)只取出共個(gè)數(shù),每個(gè)數(shù)出現(xiàn)的概率也是相等的隨機(jī)輸出把一段代碼改成,并增加單元測試。代碼本身很簡單,即使沒學(xué)過也能看懂,改后的代碼如下但是對于單元測試則僅限于聽過的地步,需要用到,好像也有別的模塊。 在拉勾上投了十幾個(gè)公司,大部分都被標(biāo)記為不合適,有兩個(gè)給了面試機(jī)會,其中一個(gè)自己覺得肯定不會去的,也就沒有去面試,另一個(gè)經(jīng)歷了一輪電話面加一輪現(xiàn)場筆試和面試,在此記錄一下...

    kohoh_ 評論0 收藏0
  • Python有什么好學(xué)的》之下文管理

    摘要:引上下文管理器太極生兩儀,兩儀為陰陽。而最常用的則是,即上下文管理器使用上下文管理器用之后的文件讀寫會變成我們看到用了之后,代碼沒有了創(chuàng)建,也沒有了釋放。實(shí)現(xiàn)上下文管理器我們先感性地對進(jìn)行猜測?,F(xiàn)實(shí)一個(gè)上下文管理器就是這么簡單。 Python有什么好學(xué)的這句話可不是反問句,而是問句哦。 主要是煎魚覺得太多的人覺得Python的語法較為簡單,寫出來的代碼只要符合邏輯,不需要太多的學(xué)習(xí)即可...

    qpwoeiru96 評論0 收藏0
  • 生成器進(jìn)化到協(xié)程 Part 2

    摘要:一個(gè)典型的上下文管理器類如下處理異常正如方法名明確告訴我們的,方法負(fù)責(zé)進(jìn)入上下的準(zhǔn)備工作,如果有需要可以返回一個(gè)值,這個(gè)值將會被賦值給中的。總結(jié)都是關(guān)于上下文管理器的內(nèi)容,與協(xié)程關(guān)系不大。 Part 1 傳送門 David Beazley 的博客 PPT 下載地址 在 Part 1 我們已經(jīng)介紹了生成器的定義和生成器的操作,現(xiàn)在讓我們開始使用生成器。Part 2 主要描述了如...

    fuyi501 評論0 收藏0
  • [譯] 從底層理解 Python 的執(zhí)行

    摘要:接下來,我們將注入到函數(shù)的字節(jié)碼中。首先我們來看一下幀的參數(shù)所能提供的信息,如下所示當(dāng)前幀將執(zhí)行的當(dāng)前的操作中的字節(jié)碼字符串的索引經(jīng)過我們的處理我們可以得知之后要被執(zhí)行的操作碼,這對我們聚合數(shù)據(jù)并展示是相當(dāng)有用的。 原文鏈接: Understanding Python execution from inside: A Python assembly tracer 以下為譯文 最近...

    wmui 評論0 收藏0
  • 如何成為一名優(yōu)秀的程序員

    摘要:前言羅子雄如何成為一名優(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)秀...

    keelii 評論0 收藏0

發(fā)表評論

0條評論

最新活動(dòng)
閱讀需要支付1元查看
<