PEP 567 – Kontextvariablen
- Autor:
- Yury Selivanov <yury at edgedb.com>
- Status:
- Final
- Typ:
- Standards Track
- Erstellt:
- 12. Dez 2017
- Python-Version:
- 3.7
- Post-History:
- 12. Dez 2017, 28. Dez 2017, 16. Jan 2018
Zusammenfassung
Dieser PEP schlägt ein neues Modul contextvars und eine Reihe neuer CPython C-APIs zur Unterstützung von Kontextvariablen vor. Dieses Konzept ähnelt dem Thread-lokalen Speicher (TLS), aber im Gegensatz zu TLS wird auch das korrekte Nachverfolgen von Werten pro asynchroner Aufgabe, z. B. asyncio.Task, ermöglicht.
Dieser Vorschlag ist eine vereinfachte Version von PEP 550. Der Hauptunterschied besteht darin, dass dieser PEP sich nur mit der Lösung des Falls für asynchrone Aufgaben befasst, nicht für Generatoren. Es sind keine Änderungen an integrierten Typen oder am Interpreter vorgesehen.
Dieser Vorschlag steht nicht in direktem Zusammenhang mit Python-Kontextmanagern. Er bietet jedoch einen Mechanismus, der von Kontextmanagern zum Speichern ihres Zustands verwendet werden kann.
API-Design und Implementierungsrevisionen
In Python 3.7.1 wurden die Signaturen aller Kontextvariablen-C-APIs geändert, um PyObject *-Zeiger anstelle von PyContext *, PyContextVar * und PyContextToken * zu verwenden, z. B.:
// in 3.7.0:
PyContext *PyContext_New(void);
// in 3.7.1+:
PyObject *PyContext_New(void);
Weitere Details finden Sie unter [6]. Der Abschnitt C API dieses PEP wurde aktualisiert, um die Änderung widerzuspiegeln.
Begründung
Thread-lokale Variablen sind für asynchrone Aufgaben, die im selben OS-Thread gleichzeitig ausgeführt werden, unzureichend. Jeder Kontextmanager, der einen Kontextwert mit threading.local() speichert und wiederherstellt, wird seine Kontextwerte unerwartet an anderen Code weitergeben, wenn er in Async/Await-Code verwendet wird.
Einige Beispiele, bei denen ein funktionierender kontextlokaler Speicher für asynchronen Code wünschenswert ist
- Kontextmanager wie
decimal-Kontexte undnumpy.errstate. - Anforderungsbezogene Daten, wie z. B. Sicherheitstoken und Anforderungsdaten in Webanwendungen, Sprachkontext für
gettextusw. - Profiling, Tracing und Logging in großen Codebasen.
Einleitung
Der PEP schlägt einen neuen Mechanismus für die Verwaltung von Kontextvariablen vor. Die wichtigsten Klassen, die an diesem Mechanismus beteiligt sind, sind contextvars.Context und contextvars.ContextVar. Der PEP schlägt auch einige Richtlinien für die Verwendung des Mechanismus im Umfeld von asynchronen Aufgaben vor.
Der vorgeschlagene Mechanismus für den Zugriff auf Kontextvariablen verwendet die Klasse ContextVar. Ein Modul (wie z. B. decimal), das den neuen Mechanismus verwenden möchte, sollte
- eine modulweite globale Variable deklarieren, die eine
ContextVarals Schlüssel hält; - auf den aktuellen Wert über die Methode
get()der Schlüsselvariable zugreifen; - den aktuellen Wert über die Methode
set()der Schlüsselvariable ändern.
Der Begriff „aktueller Wert“ verdient besondere Aufmerksamkeit: Verschiedene asynchrone Aufgaben, die gleichzeitig existieren und ausgeführt werden, können unterschiedliche Werte für denselben Schlüssel haben. Diese Idee ist aus dem Thread-lokalen Speicher bekannt, aber in diesem Fall ist die Lokalität des Werts nicht unbedingt an einen Thread gebunden. Stattdessen gibt es den Begriff des „aktuellen Context“, der im Thread-lokalen Speicher gespeichert ist. Die Manipulation des aktuellen Kontexts liegt in der Verantwortung des Task-Frameworks, z. B. asyncio.
Ein Context ist eine Zuordnung von ContextVar-Objekten zu ihren Werten. Der Context selbst exponiert die abc.Mapping-Schnittstelle (nicht abc.MutableMapping!), sodass er nicht direkt geändert werden kann. Um einen neuen Wert für eine Kontextvariable in einem Context-Objekt festzulegen, muss der Benutzer
- das
Context-Objekt mithilfe der MethodeContext.run()„aktuell“ machen; - verwenden Sie
ContextVar.set(), um einen neuen Wert für die Kontextvariable festzulegen.
Die Methode ContextVar.get() sucht mithilfe von self als Schlüssel nach der Variablen im aktuellen Context-Objekt.
Es ist nicht möglich, eine direkte Referenz auf den aktuellen Context-Objekt zu erhalten, aber es ist möglich, eine flache Kopie davon mithilfe der Funktion contextvars.copy_context() zu erhalten. Dies stellt sicher, dass der *Aufrufer* von Context.run() der alleinige Besitzer seines Context-Objekts ist.
Spezifikation
Ein neues Modul der Standardbibliothek contextvars wird mit den folgenden APIs hinzugefügt
- Die Funktion
copy_context() -> Contextwird verwendet, um eine Kopie des aktuellenContext-Objekts für den aktuellen OS-Thread zu erhalten. - Die Klasse
ContextVarzur Deklaration und zum Zugriff auf Kontextvariablen. - Die Klasse
Contextkapselt den Kontextzustand. Jeder OS-Thread speichert eine Referenz auf seine aktuelleContext-Instanz. Diese Referenz kann nicht direkt gesteuert werden. Stattdessen wird die MethodeContext.run(callable, *args, **kwargs)verwendet, um Python-Code in einem anderen Kontext auszuführen.
contextvars.ContextVar
Die Klasse ContextVar hat die folgende Konstruktorsignatur: ContextVar(name, *, default=_NO_DEFAULT). Der Parameter name wird für Introspektions- und Debuggingzwecke verwendet und als schreibgeschütztes Attribut ContextVar.name exponiert. Der Parameter default ist optional. Beispiel
# Declare a context variable 'var' with the default value 42.
var = ContextVar('var', default=42)
(Das _NO_DEFAULT ist ein internes Sentinel-Objekt, das verwendet wird, um zu erkennen, ob ein Standardwert bereitgestellt wurde.)
ContextVar.get(default=_NO_DEFAULT) gibt einen Wert für die Kontextvariable für den aktuellen Context zurück
# Get the value of `var`.
var.get()
Wenn kein Wert für die Variable im aktuellen Kontext vorhanden ist, wird ContextVar.get()
- den Wert des *default*-Arguments der Methode
get()zurückgeben, falls angegeben; oder - den Standardwert für die Kontextvariable zurückgeben, falls angegeben; oder
- einen
LookupErrorauslösen.
ContextVar.set(value) -> Token wird verwendet, um einen neuen Wert für die Kontextvariable im aktuellen Context festzulegen
# Set the variable 'var' to 1 in the current context.
var.set(1)
ContextVar.reset(token) wird verwendet, um die Variable im aktuellen Kontext auf den Wert zurückzusetzen, den sie vor der set()-Operation hatte, die das token erstellt hat (oder um die Variable zu entfernen, wenn sie nicht gesetzt war)
# Assume: var.get(None) is None
# Set 'var' to 1:
token = var.set(1)
try:
# var.get() == 1
finally:
var.reset(token)
# After reset: var.get(None) is None,
# i.e. 'var' was removed from the current context.
Die Methode ContextVar.reset() löst aus
- einen
ValueError, wenn sie mit einem Token-Objekt aufgerufen wird, das von einer anderen Variablen erstellt wurde; - einen
ValueError, wenn der aktuelleContext-Objekt nicht mit dem übereinstimmt, in dem das Token-Objekt erstellt wurde; - einen
RuntimeError, wenn das Token-Objekt bereits einmal zum Zurücksetzen der Variablen verwendet wurde.
contextvars.Token
contextvars.Token ist ein opakes Objekt, das verwendet werden sollte, um die ContextVar auf ihren vorherigen Wert wiederherzustellen oder sie aus dem Kontext zu entfernen, wenn die Variable zuvor nicht gesetzt war. Es kann nur durch Aufrufen von ContextVar.set() erstellt werden.
Für Debugging- und Introspektionszwecke verfügt es über
- ein schreibgeschütztes Attribut
Token.var, das auf die Variable zeigt, die das Token erstellt hat; - ein schreibgeschütztes Attribut
Token.old_value, das auf den Wert gesetzt ist, den die Variable vor demset()-Aufruf hatte, oder aufToken.MISSING, wenn die Variable zuvor nicht gesetzt war.
contextvars.Context
Context-Objekt ist eine Zuordnung von Kontextvariablen zu Werten.
Context() erstellt einen leeren Kontext. Um eine Kopie des aktuellen Context für den aktuellen OS-Thread zu erhalten, verwenden Sie die Methode contextvars.copy_context()
ctx = contextvars.copy_context()
Um Python-Code in einem bestimmten Context auszuführen, verwenden Sie die Methode Context.run()
ctx.run(function)
Alle Änderungen an Kontextvariablen, die function bewirkt, werden im ctx-Kontext enthalten sein
var = ContextVar('var')
var.set('spam')
def main():
# 'var' was set to 'spam' before
# calling 'copy_context()' and 'ctx.run(main)', so:
# var.get() == ctx[var] == 'spam'
var.set('ham')
# Now, after setting 'var' to 'ham':
# var.get() == ctx[var] == 'ham'
ctx = copy_context()
# Any changes that the 'main' function makes to 'var'
# will be contained in 'ctx'.
ctx.run(main)
# The 'main()' function was run in the 'ctx' context,
# so changes to 'var' are contained in it:
# ctx[var] == 'ham'
# However, outside of 'ctx', 'var' is still set to 'spam':
# var.get() == 'spam'
Context.run() löst eine RuntimeError aus, wenn sie auf dasselbe Kontextobjekt von mehr als einem OS-Thread aufgerufen wird oder wenn sie rekursiv aufgerufen wird.
Context.copy() gibt eine flache Kopie des Kontextobjekts zurück.
Context-Objekte implementieren die collections.abc.Mapping ABC. Dies kann zur Introspektion von Kontexten verwendet werden
ctx = contextvars.copy_context()
# Print all context variables and their values in 'ctx':
print(ctx.items())
# Print the value of 'some_variable' in context 'ctx':
print(ctx[some_variable])
Beachten Sie, dass alle Mapping-Methoden, einschließlich Context.__getitem__ und Context.get, Standardwerte für Kontextvariablen ignorieren (d. h. ContextVar.default). Das bedeutet, dass für eine Variable *var*, die mit einem Standardwert erstellt und nicht im *Kontext* gesetzt wurde
context[var]löst einenKeyErroraus,var in contextgibtFalsezurück,- die Variable ist nicht in
context.items()usw. enthalten.
asyncio
asyncio verwendet Loop.call_soon(), Loop.call_later() und Loop.call_at(), um die asynchrone Ausführung einer Funktion zu planen. asyncio.Task verwendet call_soon(), um die umschlossene Coroutine auszuführen.
Wir ändern Loop.call_{at,later,soon} und Future.add_done_callback() so, dass sie das neue optionale Schlüsselwortargument *context* akzeptieren, das standardmäßig den aktuellen Kontext hat
def call_soon(self, callback, *args, context=None):
if context is None:
context = contextvars.copy_context()
# ... some time later
context.run(callback, *args)
Tasks in asyncio müssen ihren eigenen Kontext beibehalten, den sie von dem Punkt an erben, an dem sie erstellt wurden. asyncio.Task wird wie folgt geändert
class Task:
def __init__(self, coro):
...
# Get the current context snapshot.
self._context = contextvars.copy_context()
self._loop.call_soon(self._step, context=self._context)
def _step(self, exc=None):
...
# Every advance of the wrapped coroutine is done in
# the task's context.
self._loop.call_soon(self._step, context=self._context)
...
Implementierung
Dieser Abschnitt erklärt Implementierungsdetails auf hoher Ebene in Pseudocode. Einige Optimierungen sind weggelassen, um diesen Abschnitt kurz und klar zu halten.
Die Zuordnung Context wird mithilfe eines unveränderlichen Wörterbuchs implementiert. Dies ermöglicht eine O(1)-Implementierung der Funktion copy_context(). Die Referenzimplementierung implementiert das unveränderliche Wörterbuch mithilfe von Hash Array Mapped Tries (HAMTs); siehe PEP 550 für eine Analyse der HAMT-Leistung [1].
Für die Zwecke dieses Abschnitts implementieren wir ein unveränderliches Wörterbuch mithilfe eines Copy-on-Write-Ansatzes und des integrierten dict-Typs
class _ContextData:
def __init__(self):
self._mapping = dict()
def __getitem__(self, key):
return self._mapping[key]
def __contains__(self, key):
return key in self._mapping
def __len__(self):
return len(self._mapping)
def __iter__(self):
return iter(self._mapping)
def set(self, key, value):
copy = _ContextData()
copy._mapping = self._mapping.copy()
copy._mapping[key] = value
return copy
def delete(self, key):
copy = _ContextData()
copy._mapping = self._mapping.copy()
del copy._mapping[key]
return copy
Jeder OS-Thread hat eine Referenz auf das aktuelle Context-Objekt
class PyThreadState:
context: Context
contextvars.Context ist ein Wrapper um _ContextData
class Context(collections.abc.Mapping):
_data: _ContextData
_prev_context: Optional[Context]
def __init__(self):
self._data = _ContextData()
self._prev_context = None
def run(self, callable, *args, **kwargs):
if self._prev_context is not None:
raise RuntimeError(
f'cannot enter context: {self} is already entered')
ts: PyThreadState = PyThreadState_Get()
self._prev_context = ts.context
try:
ts.context = self
return callable(*args, **kwargs)
finally:
ts.context = self._prev_context
self._prev_context = None
def copy(self):
new = Context()
new._data = self._data
return new
# Implement abstract Mapping.__getitem__
def __getitem__(self, var):
return self._data[var]
# Implement abstract Mapping.__contains__
def __contains__(self, var):
return var in self._data
# Implement abstract Mapping.__len__
def __len__(self):
return len(self._data)
# Implement abstract Mapping.__iter__
def __iter__(self):
return iter(self._data)
# The rest of the Mapping methods are implemented
# by collections.abc.Mapping.
contextvars.copy_context() wird wie folgt implementiert
def copy_context():
ts: PyThreadState = PyThreadState_Get()
return ts.context.copy()
contextvars.ContextVar interagiert direkt mit PyThreadState.context
class ContextVar:
def __init__(self, name, *, default=_NO_DEFAULT):
self._name = name
self._default = default
@property
def name(self):
return self._name
def get(self, default=_NO_DEFAULT):
ts: PyThreadState = PyThreadState_Get()
try:
return ts.context[self]
except KeyError:
pass
if default is not _NO_DEFAULT:
return default
if self._default is not _NO_DEFAULT:
return self._default
raise LookupError
def set(self, value):
ts: PyThreadState = PyThreadState_Get()
data: _ContextData = ts.context._data
try:
old_value = data[self]
except KeyError:
old_value = Token.MISSING
updated_data = data.set(self, value)
ts.context._data = updated_data
return Token(ts.context, self, old_value)
def reset(self, token):
if token._used:
raise RuntimeError("Token has already been used once")
if token._var is not self:
raise ValueError(
"Token was created by a different ContextVar")
ts: PyThreadState = PyThreadState_Get()
if token._context is not ts.context:
raise ValueError(
"Token was created in a different Context")
if token._old_value is Token.MISSING:
ts.context._data = ts.context._data.delete(token._var)
else:
ts.context._data = ts.context._data.set(token._var,
token._old_value)
token._used = True
Beachten Sie, dass in der Referenzimplementierung ContextVar.get() einen internen Cache für den zuletzt verwendeten Wert hat, der es ermöglicht, einen Hash-Lookup zu umgehen. Dies ähnelt der Optimierung, die das Modul decimal implementiert, um seinen Kontext aus PyThreadState_GetDict() abzurufen. Siehe PEP 550, der die Implementierung des Caches sehr detailliert erklärt.
Die Klasse Token wird wie folgt implementiert
class Token:
MISSING = object()
def __init__(self, context, var, old_value):
self._context = context
self._var = var
self._old_value = old_value
self._used = False
@property
def var(self):
return self._var
@property
def old_value(self):
return self._old_value
Zusammenfassung der neuen APIs
Python API
- Ein neues Modul
contextvarsmit den KlassenContextVar,ContextundTokensowie einer Funktioncopy_context(). asyncio.Loop.call_at(),asyncio.Loop.call_later(),asyncio.Loop.call_soon()undasyncio.Future.add_done_callback()führen Callback-Funktionen im Kontext aus, in dem sie aufgerufen wurden. Ein neues Schlüsselwortargument *context* kann verwendet werden, um einen benutzerdefinierten Kontext anzugeben.asyncio.Taskwird intern geändert, um seinen eigenen Kontext zu verwalten.
C API
PyObject * PyContextVar_New(char *name, PyObject *default): Erstellt einContextVar-Objekt. Das Argument *default* kannNULLsein, was bedeutet, dass die Variable keinen Standardwert hat.int PyContextVar_Get(PyObject *, PyObject *default_value, PyObject **value): Gibt-1zurück, wenn während der Suche ein Fehler auftritt, andernfalls0. Wenn ein Wert für die Kontextvariable gefunden wird, wird er auf denvalue-Zeiger gesetzt. Andernfalls wird *value* auf *default_value* gesetzt, wenn es nichtNULList. Wenndefault_valueNULList, wird *value* auf den Standardwert der Variablen gesetzt, der ebenfallsNULLsein kann. *value* ist immer eine neue Referenz.PyObject * PyContextVar_Set(PyObject *, PyObject *): Setzt den Wert der Variablen im aktuellen Kontext.PyContextVar_Reset(PyObject *, PyObject *): Setzt den Wert der Kontextvariablen zurück.PyObject * PyContext_New(): Erstellt einen neuen leeren Kontext.PyObject * PyContext_Copy(PyObject *): Gibt eine flache Kopie des übergebenen Kontextobjekts zurück.PyObject * PyContext_CopyCurrent(): Gibt eine Kopie des aktuellen Kontexts zurück.int PyContext_Enter(PyObject *)undint PyContext_Exit(PyObject *)ermöglichen das Setzen und Wiederherstellen des Kontexts für den aktuellen OS-Thread. Es ist erforderlich, den vorherigen Kontext immer wiederherzustellenPyObject *old_ctx = PyContext_Copy(); if (old_ctx == NULL) goto error; if (PyContext_Enter(new_ctx)) goto error; // run some code if (PyContext_Exit(old_ctx)) goto error;
Abgelehnte Ideen
Replikation der threading.local()-Schnittstelle
Siehe PEP 550, wo dieses Thema ausführlich behandelt wird: [2].
Ersetzen von Token durch ContextVar.unset()
Die Token-API ermöglicht es, auf eine ContextVar.unset()-Methode zu verzichten, die mit dem Design von verketteten Kontexten in PEP 550 inkompatibel ist. Zukünftige Kompatibilität mit PEP 550 ist gewünscht, falls Bedarf besteht, Kontextvariablen in Generatoren und asynchronen Generatoren zu unterstützen.
Die Token-API bietet auch eine bessere Benutzerfreundlichkeit: Der Benutzer muss die Abwesenheit eines Werts nicht speziell behandeln. Vergleichen Sie
token = cv.set(new_value)
try:
# cv.get() is new_value
finally:
cv.reset(token)
mit
_deleted = object()
old = cv.get(default=_deleted)
try:
cv.set(blah)
# code
finally:
if old is _deleted:
cv.unset()
else:
cv.set(old)
Haben von Token.reset() anstelle von ContextVar.reset()
Nathaniel Smith schlug vor, die Methode ContextVar.reset() direkt auf der Klasse Token zu implementieren, sodass anstelle von
token = var.set(value)
# ...
var.reset(token)
wir schreiben würden
token = var.set(value)
# ...
token.reset()
Das Vorhandensein von Token.reset() würde es einem Benutzer unmöglich machen, zu versuchen, eine Variable mit einem Token-Objekt zurückzusetzen, das von einer anderen Variablen erstellt wurde.
Dieser Vorschlag wurde aus dem Grund abgelehnt, dass ContextVar.reset() für den menschlichen Leser des Codes klarer ist, welche Variable zurückgesetzt wird.
Context-Objekte pickelbar machen
Vorgeschlagen von Antoine Pitrou, könnte dies die transparente übergreifende Prozessnutzung von Context-Objekten ermöglichen, sodass das Beispiel Auslagerung der Ausführung auf andere Threads auch mit einem ProcessPoolExecutor funktionieren würde.
Die Ermöglichung dessen ist aus folgenden Gründen problematisch
ContextVar-Objekte haben keine Attribute__module__und__qualname__, was ein direktes Pickling vonContext-Objekten unmöglich macht. Dies ist lösbar, indem die API modifiziert wird, um entweder das Modul, in dem eine Kontextvariable definiert ist, automatisch zu erkennen, oder indem ein neuer schlüsselwortbasierter Parameter „module“ zum Konstruktor vonContextVarhinzugefügt wird.- Nicht alle Kontextvariablen verweisen auf pickelbare Objekte. Das Pickelbar machen einer
ContextVarmuss eine Opt-in-Funktion sein.
Angesichts des Zeitrahmens des Python 3.7-Releaseplans wurde beschlossen, diesen Vorschlag auf Python 3.8 zu verschieben.
Context zu einem MutableMapping machen
Die Klasse Context dazu zu bringen, die Schnittstelle abc.MutableMapping zu implementieren, würde bedeuten, dass Variablen mit Operationen wie Context[var] = value und del Context[var] gesetzt und entfernt werden könnten.
Dieser Vorschlag wurde aus folgenden Gründen auf Python 3.8+ verschoben
- Wenn in Python 3.8 beschlossen wird, dass Generatoren Kontextvariablen unterstützen sollen (siehe PEP 550 und PEP 568), dann würde
Contextin eine Kette von Kontextvariablen-Zuordnungen umgewandelt werden (da jeder Generator seine eigene Zuordnung hätte). Dies würde Mutationsoperationen wieContext.__delitem__verwirrend machen, da sie nur auf der obersten Zuordnung der Kette operieren würden. - Ein einziger Weg zur Mutation des Kontexts (
ContextVar.set()undContextVar.reset()-Methoden) macht die API geradliniger.Zum Beispiel wäre es nicht offensichtlich, warum das folgende Codefragment nicht wie erwartet funktioniert
var = ContextVar('var') ctx = copy_context() ctx[var] = 'value' print(ctx[var]) # Prints 'value' print(var.get()) # Raises a LookupError
Während der folgende Code funktionieren würde
ctx = copy_context() def func(): ctx[var] = 'value' # Contrary to the previous example, this would work # because 'func()' is running within 'ctx'. print(ctx[var]) print(var.get()) ctx.run(func)
- Wenn
Contextveränderbar wäre, würde das bedeuten, dass Kontextvariablen separat (oder gleichzeitig) von dem Code mutiert werden könnten, der innerhalb des Kontexts ausgeführt wird. Das wäre ähnlich wie das Erhalten einer Referenz auf ein laufendes Python-Frame-Objekt und das Ändern seinerf_localsvon einem anderen OS-Thread aus. Ein einziger Weg zur Zuweisung von Werten an Kontextvariablen macht Kontexte konzeptionell einfacher und vorhersagbarer, während die Tür für zukünftige Leistungsoptimierungen offen bleibt.
Initialwerte für ContextVars haben
Nathaniel Smith schlug vor, ein obligatorisches schlüsselwortbasiertes Argument initial_value für den Konstruktor von ContextVar zu haben.
Das Hauptargument gegen diesen Vorschlag ist, dass es für einige Typen einfach keinen sinnvollen „Anfangswert“ gibt außer None. Z. B. betrachten Sie ein Webframework, das das aktuelle HTTP-Anforderungsobjekt in einer Kontextvariablen speichert. Mit den aktuellen Semantiken ist es möglich, eine Kontextvariable ohne Standardwert zu erstellen
# Framework:
current_request: ContextVar[Request] = \
ContextVar('current_request')
# Later, while handling an HTTP request:
request: Request = current_request.get()
# Work with the 'request' object:
return request.method
Beachten Sie, dass im obigen Beispiel keine Notwendigkeit besteht, zu prüfen, ob request None ist. Es wird einfach erwartet, dass das Framework immer die Variable current_request setzt, oder es ist ein Fehler (in diesem Fall würde current_request.get() einen LookupError auslösen).
Wenn wir jedoch einen obligatorischen Anfangswert hätten, müssten wir explizit gegen None-Werte schützen
# Framework:
current_request: ContextVar[Optional[Request]] = \
ContextVar('current_request', initial_value=None)
# Later, while handling an HTTP request:
request: Optional[Request] = current_request.get()
# Check if the current request object was set:
if request is None:
raise RuntimeError
# Work with the 'request' object:
return request.method
Darüber hinaus können wir Kontextvariablen locker mit regulären Python-Variablen und threading.local()-Objekten vergleichen. Beide lösen bei fehlgeschlagenen Suchen Fehler aus (NameError bzw. AttributeError).
Abwärtskompatibilität
Dieser Vorschlag bewahrt 100% Rückwärtskompatibilität.
Bibliotheken, die threading.local() zum Speichern kontextbezogener Werte verwenden, funktionieren derzeit nur für synchronen Code korrekt. Der Wechsel zur Verwendung der vorgeschlagenen API wird ihr Verhalten für synchronen Code unverändert lassen, aber automatisch die Unterstützung für asynchronen Code aktivieren.
Beispiele
Konvertierung von Code, der threading.local() verwendet
Ein typischer Codeausschnitt, der threading.local() verwendet, sieht normalerweise so aus
class PrecisionStorage(threading.local):
# Subclass threading.local to specify a default value.
value = 0.0
precision = PrecisionStorage()
# To set a new precision:
precision.value = 0.5
# To read the current precision:
print(precision.value)
Solcher Code kann konvertiert werden, um das Modul contextvars zu verwenden
precision = contextvars.ContextVar('precision', default=0.0)
# To set a new precision:
precision.set(0.5)
# To read the current precision:
print(precision.get())
Auslagerung der Ausführung auf andere Threads
Es ist möglich, Code in einem separaten OS-Thread auszuführen, indem eine Kopie des aktuellen Thread-Kontexts verwendet wird
executor = ThreadPoolExecutor()
current_context = contextvars.copy_context()
executor.submit(current_context.run, some_function)
Referenzimplementierung
Die Referenzimplementierung finden Sie hier: [3]. Siehe auch Issue 32436 [4].
Akzeptanz
PEP 567 wurde von Guido am Montag, 22. Januar 2018 angenommen [5]. Die Referenzimplementierung wurde am selben Tag zusammengeführt.
Referenzen
Danksagungen
Ich danke Guido van Rossum, Nathaniel Smith, Victor Stinner, Elvis Pranskevichus, Alyssa Coghlan, Antoine Pitrou, INADA Naoki, Paul Moore, Eric Snow, Greg Ewing und vielen anderen für ihr Feedback, ihre Ideen, Bearbeitungen, Kritik, Code-Reviews und Diskussionen rund um diesen PEP.
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0567.rst
Zuletzt geändert: 2025-02-01 08:59:27 GMT