229

contextvars模块到底是做什么的?

 5 years ago
source link: http://www.dongwm.com/post/137/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

在Python 3.7加入了一个新的模块contextvars,模块标题说的也非常直接:Context Variables,也就是「上下文变量」。

什么是上下文(Context)?

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()。

在Python3.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

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

before_server_start
context.set('memcache', client)
context.get('memcache')

有一点要提,在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() 就可以拿到,继而操作缓存了。

你学到了嘛?

延伸阅读


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK