不懂Python3中contextvars模块是什么?其实想解决这个问题也不难,下面让小编带着大家一起学习怎么去解决,希望大家阅读完这篇文章后大所收获。

什么是上下文(Context)?

Context Variables,也就是「上下文变量」。

Context是一个包含了相关环境内容的对象。这不是什么很高深的设计,其实和我们的日常生活也是息息相关的。

举个比较实时的例子,权力的游戏第八季刚开播,如果你没看过前七季,不了解过去的剧情、人物关系、过去的种种主线副线发展,去看第八季第一集是完全看不懂的,因为你缺失了这个美剧的上下文。

上下文就带着这些信息,如果有一人非常了解过去的那些剧情甚至看过原著,Ta可以把那些第八季能关联到的故事、剧情搞一个视频剪辑(上下文对象),那么你不需要把过去完整的七季完整看一遍,可能花一个小时看看这个视频(获得上下文对象),就能继续看第八季(完成之后的操作)。

Flask的设计中就包含了Context(下面不再说上下文,而统一用Context)。这个设计有什么用呢?简单地说:可以在一些场景下隐式地传递变量

我们看一下Django和Sanic怎么传递请求对象Request:

#Djangofromdjango.httpimportHttpResponsedefindex(request):text=request.GET.get('text')returnHttpResponse(f'Textis{text}')#Sanicfromsanicimportresponseapp=Sanic()@app.route('/')asyncdefindex(request):text=request.args.get('text')returnresponse.text(f'Textis{text}')

这2个框架都有一个问题:视图函数上要显式的传递request(请求对象)。我们再看看Flask的效果:

fromflaskimportFlask,requestapp=Flask(__name__)@app.route('/')defindex():text=request.args.get('text')returnf'Textis{text}'

在Flask中,request是import进来使用的(不需要就不用import),和视图解耦了。这种设计下,不需要像Django/Sanic那样把参数传来传去。

ThreadLocal

Flask怎么实现的呢?这就引出了ThreadLocal(本地线程)对象,看名字可以知道它是线程安全的,是单个线程自己的局部变量。Flask的实现中并没有直接用Python的ThreadLocal,而是自己实现了一个Local类,除了支持线程还支持了Greenlet的协程。

Q: 那为什么不用全局变量呢? A: 由于存在GIL,全局变量的修改必须加锁,会影响效率

先看一下线程库中ThreadLocal的例子:

❯catthreadlocal_example.pyimportrandomimportthreadinglocal_data=threading.local()defshow():name=threading.current_thread().getName()try:val=local_data.valueexceptAttributeError:print(f'Thread{name}:Novalueyet')else:print(f'Thread{name}:{val}')defworker():show()local_data.value=random.randint(1,100)show()foriinrange(2):t=threading.Thread(target=worker)t.start()❯pythonthreadlocal_example.pyThreadThread-1:NovalueyetThreadThread-1:78ThreadThread-2:NovalueyetThreadThread-2:64

可以感受到2个线程的状态互不影响。回到Flask,请求Context在内部作为一个栈来维护(应用Context在另外一个栈)。每个访问Flask的请求,会绑定到当前的Context,等请求结束后再销毁。维护的过程由框架实现,开发者不需要关心,你只需要用flask.request就可以了,这样就提高了接口的可读性和扩展性。

contextvars例子

threading.local的隔离效果很好,但是他是针对线程的,隔离线程之间的数据状态。但是现在有了asyncio,怎么办?

biu~ 我们回到contextvars,这个模块提供了一组接口,可用于管理、储存、访问局部Context的状态。我们看个例子:

❯catcontextvar_example.pyimportasyncioimportcontextvars#申明Context变量request_id=contextvars.ContextVar('Idofrequest')asyncdefget():#GetValueprint(f'RequestID(Inner):{request_id.get()}')asyncdefnew_coro(req_id):#SetValuerequest_id.set(req_id)awaitget()print(f'RequestID(Outer):{request_id.get()}')asyncdefmain():tasks=[]forreq_idinrange(1,5):tasks.append(asyncio.create_task(new_coro(req_id)))awaitasyncio.gather(*tasks)asyncio.run(main())❯pythoncontextvar_example.pyRequestID(Inner):1RequestID(Outer):1RequestID(Inner):2RequestID(Outer):2RequestID(Inner):3RequestID(Outer):3RequestID(Inner):4RequestID(Outer):4

可以看到在数据状态协程之间互不影响。注意上面contextvars.ContextVar的传入的第一个参数(name)值是一个字符串,它主要是用来标识和调试的,并不一定要用一个单词或者用下划线连起来。

注意,这个模块不仅仅给aio加入Context的支持,也用来替代threading.local()。

在Python 3.6使用contextvars

contextvars实现了PEP 567, 如果在Python3.6想使用可以用MagicStack/contextvars这个向后移植库,它和标准库都是同一个作者写的,可以放心使用。用之前你需要安装它:

pipinstallcontextvarsaiotask_context

在Sanic里面request确实没有用Context,那在aio体系里面怎么用呢?原来我会使用一个独立的库aiotask_context,在我的技术博客项目中就有用到,我简化一下这部分的代码(延伸阅读3的commit):

#ext.pyimportaiotask_contextascontext#noqa#app.pyfromextimportcontextclient=None@app.listener('before_server_start')asyncdefsetup_db(app,loop):globalclientclient=aiomcache.Client(config.MEMCACHED_HOST,config.MEMCACHED_PORT,loop=loop)loop.set_task_factory(context.task_factory)@app.middleware('request')asyncdefsetup_context(request):context.set('memcache',client)#models/mc.py_memcache=Noneasyncdefget_memcache():global_memcacheif_memcacheisnotNone:return_memcachememcache=context.get('memcache')_memcache=memcachereturnmemcache

按执行过程,我解释一下:

app.py默认client是None,在before_server_start中会设置初始化一个aiomcache.Client,用global设置给client

每次请求,通过context.set('memcache', client)把client设置到Context里面

在实际业务中,直接用context.get('memcache')获取这个client。整个逻辑中见不到client传来传去,也不需要给request设置额外的属性

有一点要提,在Python 3.6, context接受的参数必须是ContextVar对象,要这么写:

ifPY36:importcontextvarsmemcache_var=contextvars.ContextVar('memcache')else:memcache_var='memcache'try:memcache=context.get(memcache_var)exceptAttributeError:#Hackfordebugmodememcache=None

这里捕获了AttributeError,主要是在ipython中调试,由于没有启动Sanic所以没有设置上下文,所以需要异常处理一下。

contextvars的真实例子

接着替换成contextvars(延伸阅读链接4的commit):

#models/var.pyimportcontextvarsmemcache_var=contextvars.ContextVar('memcache')#app.pyfrommodels.varimportmemcache_varclient=None@app.listener('before_server_start')asyncdefsetup_db(app,loop):globalclientclient=aiomcache.Client(config.MEMCACHED_HOST,config.MEMCACHED_PORT,loop=loop)@app.middleware('request')asyncdefsetup_context(request):memcache_var.set(client)#models/mc.pyfrommodels.varimportmemcache_var_memcache=Noneasyncdefget_memcache():global_memcacheif_memcacheisnotNone:return_memcachememcache=memcache_var.get()_memcache=memcachereturnmemcache

在这种模式下,memcache(Redis)等实例对象不需要放在request对象里面,也不需要传来传去,而是放在一个上下文中,需要时直接通过memcache_var.get()就可以拿到,继而操作缓存了。

感谢你能够认真阅读完这篇文章,希望小编分享Python3中contextvars模块是什么内容对大家有帮助,同时也希望大家多多支持亿速云,关注亿速云行业资讯频道,遇到问题就找亿速云,详细的解决方法等着你来学习!