资讯专栏INFORMATION COLUMN

怎么使用python contextvs实现管理上下文

89542767 / 406人阅读


  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的内容就为大家介绍到这里了,希望可以为各位读者带来帮助。


文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/127743.html

相关文章

  • 【FAILED】记一次Python后端开发面试的经历

    摘要:正确的思路是等概率随机只取出共个数,每个数出现的概率也是相等的随机输出把一段代码改成,并增加单元测试。代码本身很简单,即使没学过也能看懂,改后的代码如下但是对于单元测试则仅限于听过的地步,需要用到,好像也有别的模块。 在拉勾上投了十几个公司,大部分都被标记为不合适,有两个给了面试机会,其中一个自己觉得肯定不会去的,也就没有去面试,另一个经历了一轮电话面加一轮现场笔试和面试,在此记录一下...

    kohoh_ 评论0 收藏0
  • Python有什么好学的》之下文管理

    摘要:引上下文管理器太极生两仪,两仪为阴阳。而最常用的则是,即上下文管理器使用上下文管理器用之后的文件读写会变成我们看到用了之后,代码没有了创建,也没有了释放。实现上下文管理器我们先感性地对进行猜测。现实一个上下文管理器就是这么简单。 Python有什么好学的这句话可不是反问句,而是问句哦。 主要是煎鱼觉得太多的人觉得Python的语法较为简单,写出来的代码只要符合逻辑,不需要太多的学习即可...

    qpwoeiru96 评论0 收藏0
  • 生成器进化到协程 Part 2

    摘要:一个典型的上下文管理器类如下处理异常正如方法名明确告诉我们的,方法负责进入上下的准备工作,如果有需要可以返回一个值,这个值将会被赋值给中的。总结都是关于上下文管理器的内容,与协程关系不大。 Part 1 传送门 David Beazley 的博客 PPT 下载地址 在 Part 1 我们已经介绍了生成器的定义和生成器的操作,现在让我们开始使用生成器。Part 2 主要描述了如...

    fuyi501 评论0 收藏0
  • [译] 从底层理解 Python 的执行

    摘要:接下来,我们将注入到函数的字节码中。首先我们来看一下帧的参数所能提供的信息,如下所示当前帧将执行的当前的操作中的字节码字符串的索引经过我们的处理我们可以得知之后要被执行的操作码,这对我们聚合数据并展示是相当有用的。 原文链接: Understanding Python execution from inside: A Python assembly tracer 以下为译文 最近...

    wmui 评论0 收藏0
  • 如何成为一名优秀的程序员

    摘要:前言罗子雄如何成为一名优秀设计师董明伟工程师的入门和进阶董明伟基于自己实践讲的知乎为新人提供了很多实用建议,他推荐的罗子雄如何成为一名优秀设计师的演讲讲的非常好,总结了设计师从入门到提高的优秀实践。 前言 罗子雄:如何成为一名优秀设计师 董明伟:Python 工程师的入门和进阶 董明伟基于自己实践讲的知乎live为Python新人提供了很多实用建议,他推荐的罗子雄:如何成为一名优秀...

    keelii 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<