最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 详解Python3中的contextvars模块

    正文概述    2020-07-22   198

    什么是上下文(Context)?

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

    详解Python3中的contextvars模块

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

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

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

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

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

    # Django
    from django.http import HttpResponse
    def index(request):
        text = request.GET.get('text')
        return HttpResponse(f'Text is {text}')
    # Sanic
    from sanic import response
    app = Sanic()
    @app.route('/')
    async def index(request):
        text = request.args.get('text')
        return response.text(f'Text is {text}')

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

    from flask import Flask, request
    app = Flask(__name__)
    @app.route('/')
    def index():
        text = request.args.get('text')
        return f'Text is {text}'

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

    ThreadLocal

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

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

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

    ❯ cat threadlocal_example.py
    import random
    import threading
    local_data = threading.local()
    def show():
        name = threading.current_thread().getName()
        try:
            val = local_data.value
        except AttributeError:
            print(f'Thread {name}: No value yet')
        else:
            print(f'Thread {name}: {val}')
    def worker():
        show()
        local_data.value = random.randint(1, 100)
        show()
    for i in range(2):
        t = threading.Thread(target=worker)
        t.start()
    ❯ python threadlocal_example.py
    Thread Thread-1: No value yet
    Thread Thread-1: 78
    Thread Thread-2: No value yet
    Thread Thread-2: 64

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

    contextvars例子

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

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

    ❯ cat contextvar_example.py
    import asyncio
    import contextvars
    # 申明Context变量
    request_id = contextvars.ContextVar('Id of request')
    async def get():
        # Get Value
        print(f'Request ID (Inner): {request_id.get()}')
    async def new_coro(req_id):
        # Set Value
        request_id.set(req_id)
        await get()
        print(f'Request ID (Outer): {request_id.get()}')
    async def main():
        tasks = []
        for req_id in range(1, 5):
            tasks.append(asyncio.create_task(new_coro(req_id)))
        await asyncio.gather(*tasks)
    asyncio.run(main())
    ❯ python contextvar_example.py
    Request ID (Inner): 1
    Request ID (Outer): 1
    Request ID (Inner): 2
    Request ID (Outer): 2
    Request ID (Inner): 3
    Request ID (Outer): 3
    Request ID (Inner): 4
    Request ID (Outer): 4

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

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

    在Python 3.6使用contextvars

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

    pip install contextvars
    aiotask_context

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

    # ext.py
    import aiotask_context as context  # noqa
    # app.py
    from ext import context
    client = None
    @app.listener('before_server_start')
    async def setup_db(app, loop):
        global client
        client = aiomcache.Client(config.MEMCACHED_HOST, config.MEMCACHED_PORT, loop=loop)
        loop.set_task_factory(context.task_factory)
    @app.middleware('request')
    async def setup_context(request):
        context.set('memcache', client)
    # models/mc.py
    _memcache = None
    async def get_memcache():
        global _memcache
        if _memcache is not None:
            return _memcache
        memcache = context.get('memcache')
        _memcache = memcache
        return memcache

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

    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对象,要这么写:

    if PY36:
        import contextvars
        memcache_var = contextvars.ContextVar('memcache')
    else:
        memcache_var = 'memcache'
    try:
        memcache = context.get(memcache_var)
    except AttributeError:
        # Hack for debug mode
        memcache = None

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

    contextvars的真实例子

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

    # models/var.py
    import contextvars
    memcache_var = contextvars.ContextVar('memcache')
    # app.py
    from models.var import memcache_var
    client = None
    @app.listener('before_server_start')
    async def setup_db(app, loop):
        global client
        client = aiomcache.Client(config.MEMCACHED_HOST, config.MEMCACHED_PORT, loop=loop)
    @app.middleware('request')
    async def setup_context(request):
        memcache_var.set(client)
    # models/mc.py
    from models.var import memcache_var
    _memcache = None
    async def get_memcache():
        global _memcache
        if _memcache is not None:
            return _memcache
        memcache = memcache_var.get()
        _memcache = memcache
        return memcache

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


    起源地下载网 » 详解Python3中的contextvars模块

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元