>  기사  >  백엔드 개발  >  Python이 contextvar를 사용하여 컨텍스트 변수를 관리하는 방법

Python이 contextvar를 사용하여 컨텍스트 변수를 관리하는 방법

WBOY
WBOY앞으로
2022-08-01 14:52:572455검색

이 기사에서는 Python에 대한 관련 지식을 제공합니다. Python은 3.7에서 contextvars라는 모듈을 도입했습니다. 이름에서 컨텍스트 변수를 참조하는 것을 쉽게 알 수 있습니다. contextvars를 사용하여 컨텍스트 변수를 관리하는 것이 모든 사람에게 도움이 되기를 바랍니다.

Python이 contextvar를 사용하여 컨텍스트 변수를 관리하는 방법

[관련 추천: Python3 동영상 튜토리얼 ]

Python은 3.7에서 모듈을 도입했습니다: contextvars 이름에서 컨텍스트 변수(Context Variables)를 참조한다는 것을 쉽게 알 수 있으므로 소개에서 Before를 참조하세요. contextvars 우리는 context가 무엇인지 이해해야 합니다.

Context는 관련 정보가 포함된 개체입니다. 예를 들어 "13개 에피소드로 구성된 애니메이션에서 8번째 에피소드를 클릭하면 주인공 앞에서 우는 여주인공을 볼 수 있습니다." 지금 시점에서 여주인공이 왜 울고 있는지 모르실 거라 생각합니다. 이전 에피소드의 콘텐츠를 시청하지 않았고 관련 맥락 정보가 누락되었기 때문입니다.

그래서 컨텍스트는 마술적인 것이 아니며 그 기능은 특정 정보를 전달하는 것입니다.

웹 프레임워크의 요청

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의 경우 해당 요청과 보기 기능이 함께 바인딩되어 있습니다. 즉, 요청이 오면 요청 객체로 캡슐화되어 뷰 함수에 전달됩니다.

하지만 플라스크의 경우에는 그렇지 않습니다. 플라스크가 요청 매개변수를 받는 방법을 살펴보겠습니다.

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는 가져오기 요청을 통해 이루어지는 것으로 알고 있습니다. 필요하지 않으면 가져올 필요가 없습니다. 물론 여기서는 어떤 방법이 더 나은지 비교하는 것이 아니라 주로 오늘 주제를 소개하기 위한 것입니다. 우선, 플라스크의 경우 다른 뷰 함수를 정의하면 여전히 요청 매개변수를 같은 방식으로 가져오는데, 서로 다른 뷰 함수가 내부적으로 동일한 요청을 사용하면 문제가 발생하지 않나요?

분명히 플라스크 사용 경험에 따르면 대답은 '아니오'이며 그 이유는 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"}
}

더 정확하게 말하면 키는 스레드의 ID여야 합니다. 직관을 위해 스레드 이름을 대신 사용하지만 간단히 말해서 스레드에 바인딩된 변수만 사용합니다. 스레드는 값을 얻을 때 획득됩니다.

Flask도 내부적으로 이런 방식으로 설계되어 있지만 threading.local을 직접 사용하지 않고 대신 스레드 지원 외에도 greenlet 코루틴도 지원합니다. 우선, 우리는 플라스크 내부에 스택(두 개의 다른 스택)을 통해 유지되는 "요청 컨텍스트"와 "애플리케이션 컨텍스트"가 있다는 것을 알고 있습니다.

# 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"))

각 요청은 현재 컨텍스트에 바인딩되며 요청이 완료된 후 삭제됩니다. 이 프로세스는 프레임워크에 의해 완료되며 개발자는 요청을 직접 사용하기만 하면 됩니다. 따라서 요청 프로세스의 구체적인 세부 사항은 소스 코드에서 볼 수 있습니다. 여기서는 위에서 언급한 Local 클래스인 werkzeug.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):
        try:
            # 根据线程 id 得到 value(一个字典)
            # 然后再根据 name 获取对应的值
            # 所以只会获取绑定在当前线程上的值
            return self.__storage__[self.__ident_func__()][name]
        except KeyError:
            raise AttributeError(name)

    def __setattr__(self, name, value):
        ident = self.__ident_func__()
        storage = self.__storage__
        try:
            # 将线程 id 作为 key,然后将值设置在对应的字典中
            # 所以只会将值设置在当前的线程中
            storage[ident][name] = value
        except KeyError:
            storage[ident] = {name: value}

    def __delattr__(self, name):
        # 删除逻辑也很简单
        try:
            del self.__storage__[self.__ident_func__()][name]
        except KeyError:
            raise AttributeError(name)

그래서 우리는 플라스크의 내부 로직이 실제로 매우 간단하고 스레드 간의 격리가 ThreadLocal을 통해 달성된다는 것을 알 수 있습니다. 각 요청은 자체 컨텍스트에 바인딩되며, 값을 얻을 때 관련 정보를 저장하는 데 사용되므로(중요하게도 격리도 달성함) 해당 컨텍스트에서도 값을 가져옵니다.

따라서 이 시점에서 컨텍스트를 이해했지만 여기서 문제가 발생합니다. threading.local이든 플라스크 자체에 의해 구현된 로컬이든 모두 스레드용입니다. async def로 정의된 코루틴을 사용하는 경우 어떻게 해야 합니까? 각 코루틴의 컨텍스트 격리를 달성하는 방법은 무엇입니까? 드디어 우리의 주인공이 소개됩니다: contextvars.

contextvars

이 모듈은 코루틴에서 로컬 컨텍스트의 상태를 관리, 설정 및 액세스하는 데 사용할 수 있는 인터페이스 세트를 제공합니다.

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.Token

当我们调用 c.set 的时候,其实会返回一个 Token 对象:

import contextvars

c = contextvars.ContextVar("context_var")
token = c.set("val")
print(token)
"""
<Token var=<ContextVar name=&#39;context_var&#39; at 0x00..> at 0x00...>
"""

Token 对象有一个 var 属性,它是只读的,会返回指向此 token 的 ContextVar 对象。

import contextvars

c = contextvars.ContextVar("context_var")
token = c.set("val")

print(token.var is c)  # True
print(token.var.get())  # val

print(
    token.var.set("val2").var.set("val3").var is c
)  # True
print(c.get())  # val3

Token 对象还有一个 old_value 属性,它会返回上一次 set 设置的值,如果是第一次 set,那么会返回一个 0b605b4cd64c40ca4e894460fc6a4c96。

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 来传递,这样就有点本末倒置了。

【相关推荐:Python3视频教程

위 내용은 Python이 contextvar를 사용하여 컨텍스트 변수를 관리하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 jb51.net에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제