Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

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

Inhaltsverzeichnis

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 und numpy.errstate.
  • Anforderungsbezogene Daten, wie z. B. Sicherheitstoken und Anforderungsdaten in Webanwendungen, Sprachkontext für gettext usw.
  • 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 ContextVar als 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 Methode Context.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

  1. Die Funktion copy_context() -> Context wird verwendet, um eine Kopie des aktuellen Context-Objekts für den aktuellen OS-Thread zu erhalten.
  2. Die Klasse ContextVar zur Deklaration und zum Zugriff auf Kontextvariablen.
  3. Die Klasse Context kapselt den Kontextzustand. Jeder OS-Thread speichert eine Referenz auf seine aktuelle Context-Instanz. Diese Referenz kann nicht direkt gesteuert werden. Stattdessen wird die Methode Context.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 LookupError auslö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 aktuelle Context-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 dem set()-Aufruf hatte, oder auf Token.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 einen KeyError aus,
  • var in context gibt False zurü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

  1. Ein neues Modul contextvars mit den Klassen ContextVar, Context und Token sowie einer Funktion copy_context().
  2. asyncio.Loop.call_at(), asyncio.Loop.call_later(), asyncio.Loop.call_soon() und asyncio.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.
  3. asyncio.Task wird intern geändert, um seinen eigenen Kontext zu verwalten.

C API

  1. PyObject * PyContextVar_New(char *name, PyObject *default): Erstellt ein ContextVar-Objekt. Das Argument *default* kann NULL sein, was bedeutet, dass die Variable keinen Standardwert hat.
  2. int PyContextVar_Get(PyObject *, PyObject *default_value, PyObject **value): Gibt -1 zurück, wenn während der Suche ein Fehler auftritt, andernfalls 0. Wenn ein Wert für die Kontextvariable gefunden wird, wird er auf den value-Zeiger gesetzt. Andernfalls wird *value* auf *default_value* gesetzt, wenn es nicht NULL ist. Wenn default_value NULL ist, wird *value* auf den Standardwert der Variablen gesetzt, der ebenfalls NULL sein kann. *value* ist immer eine neue Referenz.
  3. PyObject * PyContextVar_Set(PyObject *, PyObject *): Setzt den Wert der Variablen im aktuellen Kontext.
  4. PyContextVar_Reset(PyObject *, PyObject *): Setzt den Wert der Kontextvariablen zurück.
  5. PyObject * PyContext_New(): Erstellt einen neuen leeren Kontext.
  6. PyObject * PyContext_Copy(PyObject *): Gibt eine flache Kopie des übergebenen Kontextobjekts zurück.
  7. PyObject * PyContext_CopyCurrent(): Gibt eine Kopie des aktuellen Kontexts zurück.
  8. int PyContext_Enter(PyObject *) und int PyContext_Exit(PyObject *) ermöglichen das Setzen und Wiederherstellen des Kontexts für den aktuellen OS-Thread. Es ist erforderlich, den vorherigen Kontext immer wiederherzustellen
    PyObject *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

  1. ContextVar-Objekte haben keine Attribute __module__ und __qualname__, was ein direktes Pickling von Context-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 von ContextVar hinzugefügt wird.
  2. Nicht alle Kontextvariablen verweisen auf pickelbare Objekte. Das Pickelbar machen einer ContextVar muss 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

  1. Wenn in Python 3.8 beschlossen wird, dass Generatoren Kontextvariablen unterstützen sollen (siehe PEP 550 und PEP 568), dann würde Context in eine Kette von Kontextvariablen-Zuordnungen umgewandelt werden (da jeder Generator seine eigene Zuordnung hätte). Dies würde Mutationsoperationen wie Context.__delitem__ verwirrend machen, da sie nur auf der obersten Zuordnung der Kette operieren würden.
  2. Ein einziger Weg zur Mutation des Kontexts (ContextVar.set() und ContextVar.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)
    
  3. Wenn Context verä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 seiner f_locals von 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.


Quelle: https://github.com/python/peps/blob/main/peps/pep-0567.rst

Zuletzt geändert: 2025-02-01 08:59:27 GMT