目錄
web 框架中的 request
ThreadLocal
contextvars
c.Token
contextvars.Context
小结
首頁 後端開發 Python教學 Python怎麼利用contextvars實作管理上下文變數

Python怎麼利用contextvars實作管理上下文變數

Aug 01, 2022 pm 02:52 PM
python

本篇文章為大家帶來了關於Python的相關知識,Python 在3.7 的時候引入了一個模組:contextvars,從名字上很容易看出它指的是上下文變量,下面就來跟大家詳細講講如何使用contextvars實現管理上下文變量,希望對大家有幫助。

Python怎麼利用contextvars實作管理上下文變數

【相關推薦:Python3影片教學

Python 在3.7 的時候引入了一個模組:contextvars,從名字上很容易看出它指的是上下文變數(Context Variables),所以在介紹contextvars 之前我們需要先了解什麼是上下文(Context)。

Context 是一個包含了相關資訊內容的對象,舉個例子:"例如一部 13 集的動漫,你直接點進第八集,看到女主角在男主角面前流淚了"。相信此時你是不知道為什麼女主角會流淚的,因為你沒有看前面幾集的內容,缺少了相關的上下文資訊。

所以 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):
        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)

所以我們看到 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.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,那么会返回一个

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怎麼利用contextvars實作管理上下文變數的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

如何用PHP結合AI實現文本糾錯 PHP語法檢測與優化 如何用PHP結合AI實現文本糾錯 PHP語法檢測與優化 Jul 25, 2025 pm 08:57 PM

要實現PHP結合AI進行文本糾錯與語法優化,需按以下步驟操作:1.選擇適合的AI模型或API,如百度、騰訊API或開源NLP庫;2.通過PHP的curl或Guzzle調用API並處理返回結果;3.在應用中展示糾錯信息並允許用戶選擇是否採納;4.使用php-l和PHP_CodeSniffer進行語法檢測與代碼優化;5.持續收集反饋並更新模型或規則以提升效果。選擇AIAPI時應重點評估準確率、響應速度、價格及對PHP的支持。代碼優化應遵循PSR規範、合理使用緩存、避免循環查詢、定期審查代碼,並藉助X

PHP調用AI智能語音助手 PHP語音交互系統搭建 PHP調用AI智能語音助手 PHP語音交互系統搭建 Jul 25, 2025 pm 08:45 PM

用戶語音輸入通過前端JavaScript的MediaRecorderAPI捕獲並發送至PHP後端;2.PHP將音頻保存為臨時文件後調用STTAPI(如Google或百度語音識別)轉換為文本;3.PHP將文本發送至AI服務(如OpenAIGPT)獲取智能回复;4.PHP再調用TTSAPI(如百度或Google語音合成)將回復轉為語音文件;5.PHP將語音文件流式返回前端播放,完成交互。整個流程由PHP主導數據流轉與錯誤處理,確保各環節無縫銜接。

成品python大片在線觀看入口 python免費成品網站大全 成品python大片在線觀看入口 python免費成品網站大全 Jul 23, 2025 pm 12:36 PM

本文為您精選了多個頂級的Python“成品”項目網站與高水平“大片”級學習資源入口。無論您是想尋找開發靈感、觀摩學習大師級的源代碼,還是系統性地提昇實戰能力,這些平台都是不容錯過的寶庫,能幫助您快速成長為Python高手。

用於量子機學習的Python 用於量子機學習的Python Jul 21, 2025 am 02:48 AM

要入門量子機器學習(QML),首選工具是Python,需安裝PennyLane、Qiskit、TensorFlowQuantum或PyTorchQuantum等庫;接著通過運行示例熟悉流程,如使用PennyLane構建量子神經網絡;然後按照數據集準備、數據編碼、構建參數化量子線路、經典優化器訓練等步驟實現模型;實戰中應避免一開始就追求復雜模型,關注硬件限制,採用混合模型結構,並持續參考最新文獻和官方文檔以跟進發展。

如何用PHP開發商品推薦模塊 PHP推薦算法與用戶行為分析 如何用PHP開發商品推薦模塊 PHP推薦算法與用戶行為分析 Jul 23, 2025 pm 07:00 PM

收集用戶行為數據需通過PHP記錄瀏覽、搜索、購買等信息至數據庫,並清洗分析以挖掘興趣偏好;2.推薦算法選擇應根據數據特徵決定:基於內容、協同過濾、規則或混合推薦;3.協同過濾在PHP中可實現為計算用戶餘弦相似度、選K近鄰、加權預測評分並推薦高分商品;4.性能評估用準確率、召回率、F1值及CTR、轉化率並通過A/B測試驗證效果;5.冷啟動問題可通過商品屬性、用戶註冊信息、熱門推薦和專家評價緩解;6.性能優化手段包括緩存推薦結果、異步處理、分佈式計算與SQL查詢優化,從而提升推薦效率與用戶體驗。

如何加入Python的字符串列表 如何加入Python的字符串列表 Jul 18, 2025 am 02:15 AM

在Python中,使用join()方法合併字符串需注意以下要點:1.使用str.join()方法,調用時前面的字符串作為連接符,括號裡的可迭代對象包含要連接的字符串;2.確保列表中的元素都是字符串,若含非字符串類型需先轉換;3.處理嵌套列表時需先展平結構再連接。

Python網絡刮擦教程 Python網絡刮擦教程 Jul 21, 2025 am 02:39 AM

掌握Python網絡爬蟲需抓住三個核心步驟:1.使用requests發起請求,通過get方法獲取網頁內容,注意設置headers、處理異常及遵守robots.txt;2.利用BeautifulSoup或XPath提取數據,前者適合簡單解析,後者更靈活適用於復雜結構;3.針對動態加載內容使用Selenium模擬瀏覽器操作,雖速度較慢但能應對複雜頁面,也可嘗試尋找網站API接口提高效率。

如何用PHP開發AI智能表單系統 PHP智能表單設計與分析 如何用PHP開發AI智能表單系統 PHP智能表單設計與分析 Jul 25, 2025 pm 05:54 PM

選擇合適的PHP框架需根據項目需求綜合考慮:Laravel適合快速開發,提供EloquentORM和Blade模板引擎,便於數據庫操作和動態表單渲染;Symfony更靈活,適合複雜系統;CodeIgniter輕量,適用於對性能要求較高的簡單應用。 2.確保AI模型準確性需從高質量數據訓練、合理選擇評估指標(如準確率、召回率、F1值)、定期性能評估與模型調優入手,並通過單元測試和集成測試保障代碼質量,同時持續監控輸入數據以防止數據漂移。 3.保護用戶隱私需採取多項措施:對敏感數據進行加密存儲(如AES

See all articles