Dieser Artikel bringt Ihnen relevantes Wissen über Python. Python hat in 3.7 ein Modul eingeführt. Aus dem Namen geht leicht hervor, dass es sich auf Kontextvariablen bezieht contextvars zum Verwalten von Kontextvariablen. Ich hoffe, es wird für alle hilfreich sein.
【Verwandte Empfehlung: Python3-Video-Tutorial】
Python hat in 3.7 ein Modul eingeführt: contextvars. Aus dem Namen ist leicht ersichtlich, dass es sich auf Kontextvariablen (Context Variables) bezieht, also in der Einleitung Vorher contextvars Wir müssen verstehen, was Kontext ist.
Kontext ist ein Objekt, das relevante Informationen enthält, zum Beispiel: „In einem Anime mit 13 Folgen klickt man beispielsweise direkt in die achte Folge und sieht die Heldin vor dem Helden weinen.“ Ich glaube, Sie wissen zu diesem Zeitpunkt nicht, warum die Heldin weint, weil Sie den Inhalt der vorherigen Episoden nicht gesehen haben und relevante Kontextinformationen fehlen.
Kontext ist also keine magische Sache, seine Funktion besteht darin, bestimmte Informationen zu transportieren.
Nehmen wir Fastapi und Sanic als Beispiele, um zu sehen, wie sie eine eingehende Anfrage analysieren.
# 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)
Senden Sie eine Anfrage, um zu testen, ob das Ergebnis korrekt ist.
Sie können sehen, dass die Anfragen alle erfolgreich sind, und für Fastapi und Sanic sind ihre Anfrage- und Ansichtsfunktionen miteinander verbunden. Das heißt, wenn die Anfrage eintrifft, wird sie in ein Request-Objekt gekapselt und dann an die Ansichtsfunktion übergeben.
Aber das ist bei flask nicht der Fall. Schauen wir uns an, wie flask Anforderungsparameter empfängt.
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)
Wir sehen, dass es sich bei Flask um eine Importanforderung handelt. Wenn es nicht benötigt wird, besteht keine Notwendigkeit zum Importieren. Ich vergleiche hier nicht, welche Methode besser ist, hauptsächlich um unser heutiges Thema vorzustellen. Wenn ich für Flask eine andere Ansichtsfunktion definiere, werden die Anforderungsparameter immer noch auf die gleiche Weise abgerufen, aber dann entsteht das Problem, wenn verschiedene Ansichtsfunktionen intern dieselbe Anforderung verwenden. Gibt es dann keinen Konflikt?
Basierend auf unserer Erfahrung mit der Verwendung von Flask lautet die Antwort offensichtlich Nein, und der Grund ist ThreadLocal.
ThreadLocal, am Namen kann man erkennen, dass es definitiv mit Threads zusammenhängt. Richtig, es wird speziell zum Erstellen lokaler Variablen verwendet und die erstellten lokalen Variablen sind an Threads gebunden.
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 """
Sie können sehen, dass die beiden Threads keinen Einfluss aufeinander haben, da jeder Thread seine eigene eindeutige ID hat. Beim Binden wird der Wert an den aktuellen Thread gebunden und die Erfassung erfolgt auch vom aktuellen Thread . erhalten von. Sie können sich ThreadLocal als Wörterbuch vorstellen:
{ "one": {"value": "ONE"}, "two": {"value": "TWO"} }
Genauer gesagt sollte der Schlüssel die ID des Threads sein. Aus Gründen der Intuition verwenden wir stattdessen den Namen des Threads, aber kurz gesagt, nur die an ihn gebundenen Variablen Der Thread wird beim Erhalten des Werts abgerufen.
Flask ist intern auch auf diese Weise konzipiert, verwendet jedoch nicht direkt threading.local, sondern implementiert zusätzlich zur Unterstützung von Threads auch Greenlet-Coroutinen. Zunächst wissen wir, dass es in Flask einen „Anforderungskontext“ und einen „Anwendungskontext“ gibt, die über Stapel (zwei verschiedene Stapel) verwaltet werden.
# 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"))
Jede Anfrage wird an den aktuellen Kontext gebunden und nach Abschluss der Anfrage vernichtet. Dieser Prozess wird vom Framework abgeschlossen und Entwickler müssen die Anfrage nur direkt verwenden. Daher können die spezifischen Details des Anforderungsprozesses im Quellcode angezeigt werden. Hier konzentrieren wir uns auf ein Objekt: werkzeug.local.Local, die oben erwähnte lokale Klasse. Es ist der Schlüssel zum Festlegen und Abrufen von Variablen. Schauen Sie sich einen Teil des Quellcodes direkt an:
# 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)
Wir sehen also, dass die interne Logik von Flask eigentlich sehr einfach ist und die Isolierung zwischen Threads durch ThreadLocal erreicht wird. Jede Anfrage wird an ihren eigenen Kontext gebunden, und wenn der Wert abgerufen wird, wird er auch aus dem jeweiligen Kontext abgerufen, da er zum Speichern relevanter Informationen verwendet wird (wichtig ist, dass dadurch auch Isolation erreicht wird).
Entsprechend haben Sie den Kontext an dieser Stelle verstanden, aber hier kommt das Problem. Unabhängig davon, ob es sich um threading.local oder Local handelt, das von flask selbst implementiert wird, gelten sie alle für Threads. Was soll ich tun, wenn ich eine durch async def definierte Coroutine verwende? Wie erreicht man eine Kontextisolierung jeder Coroutine? Endlich wird unser Protagonist vorgestellt: contextvars.
Dieses Modul stellt eine Reihe von Schnittstellen bereit, die zum Verwalten, Festlegen und Zugreifen auf den Status des lokalen Kontexts in Coroutinen verwendet werden können.
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 bietet zwei Methoden, get und set, zum Abrufen und Festlegen von Werten. Wir sehen, dass der Effekt dem von ThreadingLocal ähnelt. Daten werden zwischen Coroutinen isoliert und werden nicht voneinander beeinflusst.
但我们再仔细观察一下,我们是在 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:
如果 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.set 的时候,其实会返回一个 Token 对象:
import contextvars c = contextvars.ContextVar("context_var") token = c.set("val") print(token) """ <Token var=<ContextVar name='context_var' 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 对象和设置的值之间的映射,但是我们不会直接通过 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视频教程 】
Das obige ist der detaillierte Inhalt vonWie Python Kontextvariablen verwendet, um Kontextvariablen zu verwalten. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!