Movatterモバイル変換


[0]ホーム

URL:


Following system colour schemeSelected dark colour schemeSelected light colour scheme

Python Enhancement Proposals

PEP 567 – Context Variables

Author:
Yury Selivanov <yury at edgedb.com>
Status:
Final
Type:
Standards Track
Created:
12-Dec-2017
Python-Version:
3.7
Post-History:
12-Dec-2017, 28-Dec-2017, 16-Jan-2018

Table of Contents

Abstract

This PEP proposes a newcontextvars module and a set of newCPython C APIs to support context variables. This concept issimilar to thread-local storage (TLS), but, unlike TLS, it also allowscorrectly keeping track of values per asynchronous task, e.g.asyncio.Task.

This proposal is a simplified version ofPEP 550. The keydifference is that this PEP is concerned only with solving the casefor asynchronous tasks, not for generators. There are no proposedmodifications to any built-in types or to the interpreter.

This proposal is not strictly related to Python Context Managers.Although it does provide a mechanism that can be used by ContextManagers to store their state.

API Design and Implementation Revisions

InPython 3.7.1 the signatures of all context variablesC APIs werechanged to usePyObject* pointers insteadofPyContext*,PyContextVar*, andPyContextToken*,e.g.:

//in3.7.0:PyContext*PyContext_New(void);//in3.7.1+:PyObject*PyContext_New(void);

See[6] for more details. TheC API section of this PEP wasupdated to reflect the change.

Rationale

Thread-local variables are insufficient for asynchronous tasks thatexecute concurrently in the same OS thread. Any context manager thatsaves and restores a context value usingthreading.local() willhave its context values bleed to other code unexpectedly when usedin async/await code.

A few examples where having a working context local storage forasynchronous code is desirable:

  • Context managers likedecimal contexts andnumpy.errstate.
  • Request-related data, such as security tokens and requestdata in web applications, language context forgettext, etc.
  • Profiling, tracing, and logging in large code bases.

Introduction

The PEP proposes a new mechanism for managing context variables.The key classes involved in this mechanism arecontextvars.Contextandcontextvars.ContextVar. The PEP also proposes some policiesfor using the mechanism around asynchronous tasks.

The proposed mechanism for accessing context variables uses theContextVar class. A module (such asdecimal) that wishes touse the new mechanism should:

  • declare a module-global variable holding aContextVar toserve as a key;
  • access the current value via theget() method on thekey variable;
  • modify the current value via theset() method on thekey variable.

The notion of “current value” deserves special consideration:different asynchronous tasks that exist and execute concurrentlymay have different values for the same key. This idea is well knownfrom thread-local storage but in this case the locality of the value isnot necessarily bound to a thread. Instead, there is the notion of the“currentContext” which is stored in thread-local storage.Manipulation of the current context is the responsibility of thetask framework, e.g. asyncio.

AContext is a mapping ofContextVar objects to their values.TheContext itself exposes theabc.Mapping interface(notabc.MutableMapping!), so it cannot be modified directly.To set a new value for a context variable in aContext object,the user needs to:

  • make theContext object “current” using theContext.run()method;
  • useContextVar.set() to set a new value for the contextvariable.

TheContextVar.get() method looks for the variable in the currentContext object usingself as a key.

It is not possible to get a direct reference to the currentContextobject, but it is possible to obtain a shallow copy of it using thecontextvars.copy_context() function. This ensures that thecaller ofContext.run() is the sole owner of itsContextobject.

Specification

A new standard library modulecontextvars is added with thefollowing APIs:

  1. Thecopy_context()->Context function is used to get a copy ofthe currentContext object for the current OS thread.
  2. TheContextVar class to declare and access context variables.
  3. TheContext class encapsulates context state. Every OS threadstores a reference to its currentContext instance.It is not possible to control that reference directly.Instead, theContext.run(callable,*args,**kwargs) method isused to run Python code in another context.

contextvars.ContextVar

TheContextVar class has the following constructor signature:ContextVar(name,*,default=_NO_DEFAULT). Thename parameteris used for introspection and debug purposes, and is exposedas a read-onlyContextVar.name attribute. Thedefaultparameter is optional. Example:

# Declare a context variable 'var' with the default value 42.var=ContextVar('var',default=42)

(The_NO_DEFAULT is an internal sentinel object used todetect if the default value was provided.)

ContextVar.get(default=_NO_DEFAULT) returns a value forthe context variable for the currentContext:

# Get the value of `var`.var.get()

If there is no value for the variable in the current context,ContextVar.get() will:

  • return the value of thedefault argument of theget() method,if provided; or
  • return the default value for the context variable, if provided; or
  • raise aLookupError.

ContextVar.set(value)->Token is used to set a new value forthe context variable in the currentContext:

# Set the variable 'var' to 1 in the current context.var.set(1)

ContextVar.reset(token) is used to reset the variable in thecurrent context to the value it had before theset() operationthat created thetoken (or to remove the variable if it wasnot set):

# Assume: var.get(None) is None# Set 'var' to 1:token=var.set(1)try:# var.get() == 1finally:var.reset(token)# After reset: var.get(None) is None,# i.e. 'var' was removed from the current context.

TheContextVar.reset() method raises:

  • aValueError if it is called with a token object createdby another variable;
  • aValueError if the currentContext object does not matchthe one where the token object was created;
  • aRuntimeError if the token object has already been used onceto reset the variable.

contextvars.Token

contextvars.Token is an opaque object that should be used torestore theContextVar to its previous value, or to remove it fromthe context if the variable was not set before. It can be createdonly by callingContextVar.set().

For debug and introspection purposes it has:

  • a read-only attributeToken.var pointing to the variablethat created the token;
  • a read-only attributeToken.old_value set to the value thevariable had before theset() call, or toToken.MISSINGif the variable wasn’t set before.

contextvars.Context

Context object is a mapping of context variables to values.

Context() creates an empty context. To get a copy of the currentContext for the current OS thread, use thecontextvars.copy_context() method:

ctx=contextvars.copy_context()

To run Python code in someContext, useContext.run()method:

ctx.run(function)

Any changes to any context variables thatfunction causes willbe contained in thectx context:

var=ContextVar('var')var.set('spam')defmain():# '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() raises aRuntimeError when called on the samecontext object from more than one OS thread, or when calledrecursively.

Context.copy() returns a shallow copy of the context object.

Context objects implement thecollections.abc.Mapping ABC.This can be used to introspect contexts:

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])

Note that all Mapping methods, includingContext.__getitem__ andContext.get, ignore default values for context variables(i.e.ContextVar.default). This means that for a variablevarthat was created with a default value and was not set in thecontext:

  • context[var] raises aKeyError,
  • varincontext returnsFalse,
  • the variable isn’t included incontext.items(), etc.

asyncio

asyncio usesLoop.call_soon(),Loop.call_later(),andLoop.call_at() to schedule the asynchronous execution of afunction.asyncio.Task usescall_soon() to run thewrapped coroutine.

We modifyLoop.call_{at,later,soon} andFuture.add_done_callback() to accept the new optionalcontextkeyword-only argument, which defaults to the current context:

defcall_soon(self,callback,*args,context=None):ifcontextisNone:context=contextvars.copy_context()# ... some time latercontext.run(callback,*args)

Tasks in asyncio need to maintain their own context that they inheritfrom the point they were created at.asyncio.Task is modifiedas follows:

classTask: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)...

Implementation

This section explains high-level implementation details inpseudo-code. Some optimizations are omitted to keep this sectionshort and clear.

TheContext mapping is implemented using an immutable dictionary.This allows for a O(1) implementation of thecopy_context()function. The reference implementation implements the immutabledictionary using Hash Array Mapped Tries (HAMT); seePEP 550for analysis of HAMT performance[1].

For the purposes of this section, we implement an immutable dictionaryusing a copy-on-write approach and the built-in dict type:

class_ContextData:def__init__(self):self._mapping=dict()def__getitem__(self,key):returnself._mapping[key]def__contains__(self,key):returnkeyinself._mappingdef__len__(self):returnlen(self._mapping)def__iter__(self):returniter(self._mapping)defset(self,key,value):copy=_ContextData()copy._mapping=self._mapping.copy()copy._mapping[key]=valuereturncopydefdelete(self,key):copy=_ContextData()copy._mapping=self._mapping.copy()delcopy._mapping[key]returncopy

Every OS thread has a reference to the currentContext object:

classPyThreadState:context:Context

contextvars.Context is a wrapper around_ContextData:

classContext(collections.abc.Mapping):_data:_ContextData_prev_context:Optional[Context]def__init__(self):self._data=_ContextData()self._prev_context=Nonedefrun(self,callable,*args,**kwargs):ifself._prev_contextisnotNone:raiseRuntimeError(f'cannot enter context:{self} is already entered')ts:PyThreadState=PyThreadState_Get()self._prev_context=ts.contexttry:ts.context=selfreturncallable(*args,**kwargs)finally:ts.context=self._prev_contextself._prev_context=Nonedefcopy(self):new=Context()new._data=self._datareturnnew# Implement abstract Mapping.__getitem__def__getitem__(self,var):returnself._data[var]# Implement abstract Mapping.__contains__def__contains__(self,var):returnvarinself._data# Implement abstract Mapping.__len__def__len__(self):returnlen(self._data)# Implement abstract Mapping.__iter__def__iter__(self):returniter(self._data)# The rest of the Mapping methods are implemented# by collections.abc.Mapping.

contextvars.copy_context() is implemented as follows:

defcopy_context():ts:PyThreadState=PyThreadState_Get()returnts.context.copy()

contextvars.ContextVar interacts withPyThreadState.contextdirectly:

classContextVar:def__init__(self,name,*,default=_NO_DEFAULT):self._name=nameself._default=default@propertydefname(self):returnself._namedefget(self,default=_NO_DEFAULT):ts:PyThreadState=PyThreadState_Get()try:returnts.context[self]exceptKeyError:passifdefaultisnot_NO_DEFAULT:returndefaultifself._defaultisnot_NO_DEFAULT:returnself._defaultraiseLookupErrordefset(self,value):ts:PyThreadState=PyThreadState_Get()data:_ContextData=ts.context._datatry:old_value=data[self]exceptKeyError:old_value=Token.MISSINGupdated_data=data.set(self,value)ts.context._data=updated_datareturnToken(ts.context,self,old_value)defreset(self,token):iftoken._used:raiseRuntimeError("Token has already been used once")iftoken._varisnotself:raiseValueError("Token was created by a different ContextVar")ts:PyThreadState=PyThreadState_Get()iftoken._contextisnotts.context:raiseValueError("Token was created in a different Context")iftoken._old_valueisToken.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

Note that the in the reference implementation,ContextVar.get()has an internal cache for the most recent value, which allows tobypass a hash lookup. This is similar to the optimization thedecimal module implements to retrieve its context fromPyThreadState_GetDict(). SeePEP 550 which explains theimplementation of the cache in great detail.

TheToken class is implemented as follows:

classToken:MISSING=object()def__init__(self,context,var,old_value):self._context=contextself._var=varself._old_value=old_valueself._used=False@propertydefvar(self):returnself._var@propertydefold_value(self):returnself._old_value

Summary of the New APIs

Python API

  1. A newcontextvars module withContextVar,Context,andToken classes, and acopy_context() function.
  2. asyncio.Loop.call_at(),asyncio.Loop.call_later(),asyncio.Loop.call_soon(), andasyncio.Future.add_done_callback() run callback functions inthe context they were called in. A newcontext keyword-onlyparameter can be used to specify a custom context.
  3. asyncio.Task is modified internally to maintain its owncontext.

C API

  1. PyObject*PyContextVar_New(char*name,PyObject*default):create aContextVar object. Thedefault argument can beNULL, which means that the variable has no default value.
  2. intPyContextVar_Get(PyObject*,PyObject*default_value,PyObject**value):return-1 if an error occurs during the lookup,0 otherwise.If a value for the context variable is found, it will be set to thevalue pointer. Otherwise,value will be set todefault_value when it is notNULL. Ifdefault_value isNULL,value will be set to the default value of thevariable, which can beNULL too.value is always a newreference.
  3. PyObject*PyContextVar_Set(PyObject*,PyObject*):set the value of the variable in the current context.
  4. PyContextVar_Reset(PyObject*,PyObject*):reset the value of the context variable.
  5. PyObject*PyContext_New(): create a new empty context.
  6. PyObject*PyContext_Copy(PyObject*): return a shallowcopy of the passed context object.
  7. PyObject*PyContext_CopyCurrent(): get a copy of the currentcontext.
  8. intPyContext_Enter(PyObject*) andintPyContext_Exit(PyObject*) allow to set and restorethe context for the current OS thread. It is required to alwaysrestore the previous context:
    PyObject*old_ctx=PyContext_Copy();if(old_ctx==NULL)gotoerror;if(PyContext_Enter(new_ctx))gotoerror;//runsomecodeif(PyContext_Exit(old_ctx))gotoerror;

Rejected Ideas

Replicating threading.local() interface

Please refer toPEP 550 where this topic is covered in detail:[2].

Replacing Token with ContextVar.unset()

The Token API allows to get around having aContextVar.unset()method, which is incompatible with chained contexts design ofPEP 550. Future compatibility withPEP 550 is desiredin case there is demand to support context variables in generatorsand asynchronous generators.

The Token API also offers better usability: the user does not haveto special-case absence of a value. Compare:

token=cv.set(new_value)try:# cv.get() is new_valuefinally:cv.reset(token)

with:

_deleted=object()old=cv.get(default=_deleted)try:cv.set(blah)# codefinally:ifoldis_deleted:cv.unset()else:cv.set(old)

Having Token.reset() instead of ContextVar.reset()

Nathaniel Smith suggested to implement theContextVar.reset()method directly on theToken class, so instead of:

token=var.set(value)# ...var.reset(token)

we would write:

token=var.set(value)# ...token.reset()

HavingToken.reset() would make it impossible for a user toattempt to reset a variable with a token object created by anothervariable.

This proposal was rejected for the reason ofContextVar.reset()being clearer to the human reader of the code which variable isbeing reset.

Making Context objects picklable

Proposed by Antoine Pitrou, this could enable transparentcross-process use ofContext objects, so theOffloading execution to other threads example would work withaProcessPoolExecutor too.

Enabling this is problematic because of the following reasons:

  1. ContextVar objects do not have__module__ and__qualname__ attributes, making straightforward picklingofContext objects impossible. This is solvable by modifyingthe API to either auto detect the module where a context variableis defined, or by adding a new keyword-only “module” parametertoContextVar constructor.
  2. Not all context variables refer to picklable objects. Making aContextVar picklable must be an opt-in.

Given the time frame of the Python 3.7 release schedule it was decidedto defer this proposal to Python 3.8.

Making Context a MutableMapping

Making theContext class implement theabc.MutableMappinginterface would mean that it is possible to set and unset variablesusingContext[var]=value anddelContext[var] operations.

This proposal was deferred to Python 3.8+ because of the following:

  1. If in Python 3.8 it is decided that generators should supportcontext variables (seePEP 550 andPEP 568), thenContextwould be transformed into a chain-map of context variables mappings(as every generator would have its own mapping). That would makemutation operations likeContext.__delitem__ confusing, asthey would operate only on the topmost mapping of the chain.
  2. Having a single way of mutating the context(ContextVar.set() andContextVar.reset() methods) makesthe API more straightforward.

    For example, it would be non-obvious why the below code fragmentdoes not work as expected:

    var=ContextVar('var')ctx=copy_context()ctx[var]='value'print(ctx[var])# Prints 'value'print(var.get())# Raises a LookupError

    While the following code would work:

    ctx=copy_context()deffunc():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. IfContext was mutable it would mean that context variablescould be mutated separately (or concurrently) from the code thatruns within the context. That would be similar to obtaining areference to a running Python frame object and modifying itsf_locals from another OS thread. Having one single way toassign values to context variables makes contexts conceptuallysimpler and more predictable, while keeping the door open forfuture performance optimizations.

Having initial values for ContextVars

Nathaniel Smith proposed to have a requiredinitial_valuekeyword-only argument for theContextVar constructor.

The main argument against this proposal is that for some typesthere is simply no sensible “initial value” exceptNone.E.g. consider a web framework that stores the current HTTPrequest object in a context variable. With the current semanticsit is possible to create a context variable without a default value:

# Framework:current_request:ContextVar[Request]= \ContextVar('current_request')# Later, while handling an HTTP request:request:Request=current_request.get()# Work with the 'request' object:returnrequest.method

Note that in the above example there is no need to check ifrequest isNone. It is simply expected that the frameworkalways sets thecurrent_request variable, or it is a bug (inwhich casecurrent_request.get() would raise aLookupError).

If, however, we had a required initial value, we would haveto guard againstNone values explicitly:

# 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:ifrequestisNone:raiseRuntimeError# Work with the 'request' object:returnrequest.method

Moreover, we can loosely compare context variables to regularPython variables and tothreading.local() objects. Bothof them raise errors on failed lookups (NameError andAttributeError respectively).

Backwards Compatibility

This proposal preserves 100% backwards compatibility.

Libraries that usethreading.local() to store context-relatedvalues, currently work correctly only for synchronous code. Switchingthem to use the proposed API will keep their behavior for synchronouscode unmodified, but will automatically enable support forasynchronous code.

Examples

Converting code that uses threading.local()

A typical code fragment that usesthreading.local() usuallylooks like the following:

classPrecisionStorage(threading.local):# Subclass threading.local to specify a default value.value=0.0precision=PrecisionStorage()# To set a new precision:precision.value=0.5# To read the current precision:print(precision.value)

Such code can be converted to use thecontextvars module:

precision=contextvars.ContextVar('precision',default=0.0)# To set a new precision:precision.set(0.5)# To read the current precision:print(precision.get())

Offloading execution to other threads

It is possible to run code in a separate OS thread using a copyof the current thread context:

executor=ThreadPoolExecutor()current_context=contextvars.copy_context()executor.submit(current_context.run,some_function)

Reference Implementation

The reference implementation can be found here:[3].See also issue 32436[4].

Acceptance

PEP 567 was accepted by Guido on Monday, January 22, 2018[5].The reference implementation was merged on the same day.

References

[1]
PEP 550
[2]
PEP 550
[3]
https://github.com/python/cpython/pull/5027
[4]
https://bugs.python.org/issue32436
[5]
https://mail.python.org/pipermail/python-dev/2018-January/151878.html
[6]
https://bugs.python.org/issue34762

Acknowledgments

I thank Guido van Rossum, Nathaniel Smith, Victor Stinner,Elvis Pranskevichus, Alyssa Coghlan, Antoine Pitrou, INADA Naoki,Paul Moore, Eric Snow, Greg Ewing, and many others for their feedback,ideas, edits, criticism, code reviews, and discussions aroundthis PEP.

Copyright

This document has been placed in the public domain.


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

Last modified:2025-02-01 08:59:27 GMT


[8]ページ先頭

©2009-2025 Movatter.jp