
Hi, This is a third iteration of the PEP. There was some really good feedback on python-ideas and the discussion thread became hard to follow again, so I decided to update the PEP only three days after I published the previous version. Summary of the changes can be found in the "Version History" section: https://www.python.org/dev/peps/pep-0550/#version-history There are a few open questions left, namely the terminology and design of ContextKey API. On the former topic, I'm quite happy with the latest version: Execution Context, Logical Context, and Context Key. Thank you, Yury PEP: 550 Title: Execution Context Version: $Revision$ Last-Modified: $Date$ Author: Yury Selivanov <yury@magic.io> Status: Draft Type: Standards Track Content-Type: text/x-rst Created: 11-Aug-2017 Python-Version: 3.7 Post-History: 11-Aug-2017, 15-Aug-2017, 18-Aug-2017 Abstract ======== This PEP proposes a new mechanism to manage execution state--the logical environment in which a function, a thread, a generator, or a coroutine executes in. A few examples of where having a reliable state storage is required: * Context managers like decimal contexts, ``numpy.errstate``, and ``warnings.catch_warnings``; * Storing request-related data such as security tokens and request data in web applications, implementing i18n; * Profiling, tracing, and logging in complex and large code bases. The usual solution for storing state is to use a Thread-local Storage (TLS), implemented in the standard library as ``threading.local()``. Unfortunately, TLS does not work for the purpose of state isolation for generators or asynchronous code, because such code executes concurrently in a single thread. Rationale ========= Traditionally, a Thread-local Storage (TLS) is used for storing the state. However, the major flaw of using the TLS is that it works only for multi-threaded code. It is not possible to reliably contain the state within a generator or a coroutine. For example, consider the following generator:: def calculate(precision, ...): with decimal.localcontext() as ctx: # Set the precision for decimal calculations # inside this block ctx.prec = precision yield calculate_something() yield calculate_something_else() Decimal context is using a TLS to store the state, and because TLS is not aware of generators, the state can leak. If a user iterates over the ``calculate()`` generator with different precisions one by one using a ``zip()`` built-in, the above code will not work correctly. For example:: g1 = calculate(precision=100) g2 = calculate(precision=50) items = list(zip(g1, g2)) # items[0] will be a tuple of: # first value from g1 calculated with 100 precision, # first value from g2 calculated with 50 precision. # # items[1] will be a tuple of: # second value from g1 calculated with 50 precision (!!!), # second value from g2 calculated with 50 precision. An even scarier example would be using decimals to represent money in an async/await application: decimal calculations can suddenly lose precision in the middle of processing a request. Currently, bugs like this are extremely hard to find and fix. Another common need for web applications is to have access to the current request object, or security context, or, simply, the request URL for logging or submitting performance tracing data:: async def handle_http_request(request): context.current_http_request = request await ... # Invoke your framework code, render templates, # make DB queries, etc, and use the global # 'current_http_request' in that code. # This isn't currently possible to do reliably # in asyncio out of the box. These examples are just a few out of many, where a reliable way to store context data is absolutely needed. The inability to use TLS for asynchronous code has lead to proliferation of ad-hoc solutions, which are limited in scope and do not support all required use cases. Current status quo is that any library, including the standard library, that uses a TLS, will likely not work as expected in asynchronous code or with generators (see [3]_ as an example issue.) Some languages that have coroutines or generators recommend to manually pass a ``context`` object to every function, see [1]_ describing the pattern for Go. This approach, however, has limited use for Python, where we have a huge ecosystem that was built to work with a TLS-like context. Moreover, passing the context explicitly does not work at all for libraries like ``decimal`` or ``numpy``, which use operator overloading. .NET runtime, which has support for async/await, has a generic solution of this problem, called ``ExecutionContext`` (see [2]_). On the surface, working with it is very similar to working with a TLS, but the former explicitly supports asynchronous code. Goals ===== The goal of this PEP is to provide a more reliable alternative to ``threading.local()``. It should be explicitly designed to work with Python execution model, equally supporting threads, generators, and coroutines. An acceptable solution for Python should meet the following requirements: * Transparent support for code executing in threads, coroutines, and generators with an easy to use API. * Negligible impact on the performance of the existing code or the code that will be using the new mechanism. * Fast C API for packages like ``decimal`` and ``numpy``. Explicit is still better than implicit, hence the new APIs should only be used when there is no acceptable way of passing the state explicitly. Specification ============= Execution Context is a mechanism of storing and accessing data specific to a logical thread of execution. We consider OS threads, generators, and chains of coroutines (such as ``asyncio.Task``) to be variants of a logical thread. In this specification, we will use the following terminology: * **Logical Context**, or LC, is a key/value mapping that stores the context of a logical thread. * **Execution Context**, or EC, is an OS-thread-specific dynamic stack of Logical Contexts. * **Context Key**, or CK, is an object used to set and get values from the Execution Context. Please note that throughout the specification we use simple pseudo-code to illustrate how the EC machinery works. The actual algorithms and data structures that we will use to implement the PEP are discussed in the `Implementation Strategy`_ section. Context Key Object ------------------ The ``sys.new_context_key(name)`` function creates a new ``ContextKey`` object. The ``name`` parameter is a ``str`` needed to render a representation of ``ContextKey`` object for introspection and debugging purposes. ``ContextKey`` objects have the following methods and attributes: * ``.name``: read-only name; * ``.set(o)`` method: set the value to ``o`` for the context key in the execution context. * ``.get()`` method: return the current EC value for the context key. Context keys return ``None`` when the key is missing, so the method never fails. The below is an example of how context keys can be used:: my_context = sys.new_context_key('my_context') my_context.set('spam') # Later, to access the value of my_context: print(my_context.get()) Thread State and Multi-threaded code ------------------------------------ Execution Context is implemented on top of Thread-local Storage. For every thread there is a separate stack of Logical Contexts -- mappings of ``ContextKey`` objects to their values in the LC. New threads always start with an empty EC. For CPython:: PyThreadState: execution_context: ExecutionContext([ LogicalContext({ci1: val1, ci2: val2, ...}), ... ]) The ``ContextKey.get()`` and ``.set()`` methods are defined as follows (in pseudo-code):: class ContextKey: def get(self): tstate = PyThreadState_Get() for logical_context in reversed(tstate.execution_context): if self in logical_context: return logical_context[self] return None def set(self, value): tstate = PyThreadState_Get() if not tstate.execution_context: tstate.execution_context = [LogicalContext()] tstate.execution_context[-1][self] = value With the semantics defined so far, the Execution Context can already be used as an alternative to ``threading.local()``:: def print_foo(): print(ci.get() or 'nothing') ci = sys.new_context_key('ci') ci.set('foo') # Will print "foo": print_foo() # Will print "nothing": threading.Thread(target=print_foo).start() Manual Context Management ------------------------- Execution Context is generally managed by the Python interpreter, but sometimes it is desirable for the user to take the control over it. A few examples when this is needed: * running a computation in ``concurrent.futures.ThreadPoolExecutor`` with the current EC; * reimplementing generators with iterators (more on that later); * managing contexts in asynchronous frameworks (implement proper EC support in ``asyncio.Task`` and ``asyncio.loop.call_soon``.) For these purposes we add a set of new APIs (they will be used in later sections of this specification): * ``sys.new_logical_context()``: create an empty ``LogicalContext`` object. * ``sys.new_execution_context()``: create an empty ``ExecutionContext`` object. * Both ``LogicalContext`` and ``ExecutionContext`` objects are opaque to Python code, and there are no APIs to modify them. * ``sys.get_execution_context()`` function. The function returns a copy of the current EC: an ``ExecutionContext`` instance. The runtime complexity of the actual implementation of this function can be O(1), but for the purposes of this section it is equivalent to:: def get_execution_context(): tstate = PyThreadState_Get() return copy(tstate.execution_context) * ``sys.run_with_execution_context(ec: ExecutionContext, func, *args, **kwargs)`` runs ``func(*args, **kwargs)`` in the provided execution context:: def run_with_execution_context(ec, func, *args, **kwargs): tstate = PyThreadState_Get() old_ec = tstate.execution_context tstate.execution_context = ExecutionContext( ec.logical_contexts + [LogicalContext()] ) try: return func(*args, **kwargs) finally: tstate.execution_context = old_ec Any changes to Logical Context by ``func`` will be ignored. This allows to reuse one ``ExecutionContext`` object for multiple invocations of different functions, without them being able to affect each other's environment:: ci = sys.new_context_key('ci') ci.set('spam') def func(): print(ci.get()) ci.set('ham') ec = sys.get_execution_context() sys.run_with_execution_context(ec, func) sys.run_with_execution_context(ec, func) # Will print: # spam # spam * ``sys.run_with_logical_context(lc: LogicalContext, func, *args, **kwargs)`` runs ``func(*args, **kwargs)`` in the current execution context using the specified logical context. Any changes that ``func`` does to the logical context will be persisted in ``lc``. This behaviour is different from the ``run_with_execution_context()`` function, which always creates a new throw-away logical context. In pseudo-code:: def run_with_logical_context(lc, func, *args, **kwargs): tstate = PyThreadState_Get() old_ec = tstate.execution_context tstate.execution_context = ExecutionContext( old_ec.logical_contexts + [lc] ) try: return func(*args, **kwargs) finally: tstate.execution_context = old_ec Using the previous example:: ci = sys.new_context_key('ci') ci.set('spam') def func(): print(ci.get()) ci.set('ham') ec = sys.get_execution_context() lc = sys.new_logical_context() sys.run_with_logical_context(lc, func) sys.run_with_logical_context(lc, func) # Will print: # spam # ham As an example, let's make a subclass of ``concurrent.futures.ThreadPoolExecutor`` that preserves the execution context for scheduled functions:: class Executor(concurrent.futures.ThreadPoolExecutor): def submit(self, fn, *args, **kwargs): context = sys.get_execution_context() fn = functools.partial( sys.run_with_execution_context, context, fn, *args, **kwargs) return super().submit(fn) Generators ---------- Generators in Python are producers of data, and ``yield`` expressions are used to suspend/resume their execution. When generators suspend execution, their local state will "leak" to the outside code if they store it in a TLS or in a global variable:: local = threading.local() def gen(): old_x = local.x local.x = 'spam' try: yield ... yield finally: local.x = old_x The above code will not work as many Python users expect it to work. A simple ``next(gen())`` will set ``local.x`` to "spam" and it will never be reset back to its original value. One of the goals of this proposal is to provide a mechanism to isolate local state in generators. Generator Object Modifications ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ To achieve this, we make a small set of modifications to the generator object: * New ``__logical_context__`` attribute. This attribute is readable and writable for Python code. * When a generator object is instantiated its ``__logical_context__`` is initialized with an empty ``LogicalContext``. * Generator's ``.send()`` and ``.throw()`` methods are modified as follows (in pseudo-C):: if gen.__logical_context__ is not NULL: tstate = PyThreadState_Get() tstate.execution_context.push(gen.__logical_context__) try: # Perform the actual `Generator.send()` or # `Generator.throw()` call. return gen.send(...) finally: gen.__logical_context__ = tstate.execution_context.pop() else: # Perform the actual `Generator.send()` or # `Generator.throw()` call. return gen.send(...) If a generator has a non-NULL ``__logical_context__``, it will be pushed to the EC and, therefore, generators will use it to accumulate their local state. If a generator has no ``__logical_context__``, generators will will use whatever LC they are being run in. EC Semantics for Generators ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Every generator object has its own Logical Context that stores only its own local modifications of the context. When a generator is being iterated, its logical context will be put in the EC stack of the current thread. This means that the generator will be able to access keys from the surrounding context:: local = sys.new_context_key("local") global = sys.new_context_key("global") def generator(): local.set('inside gen:') while True: print(local.get(), global.get()) yield g = gen() local.set('hello') global.set('spam') next(g) local.set('world') global.set('ham') next(g) # Will print: # inside gen: spam # inside gen: ham Any changes to the EC in nested generators are invisible to the outer generator:: local = sys.new_context_key("local") def inner_gen(): local.set('spam') yield def outer_gen(): local.set('ham') yield from gen() print(local.get()) list(outer_gen()) # Will print: # ham Running generators without LC ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ If ``__logical_context__`` is set to ``None`` for a generator, it will simply use the outer Logical Context. The ``@contextlib.contextmanager`` decorator uses this mechanism to allow its generator to affect the EC:: item = sys.new_context_key('item') @contextmanager def context(x): old = item.get() item.set('x') try: yield finally: item.set(old) with context('spam'): with context('ham'): print(1, item.get()) print(2, item.get()) # Will print: # 1 ham # 2 spam Implementing Generators with Iterators ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The Execution Context API allows to fully replicate EC behaviour imposed on generators with a regular Python iterator class:: class Gen: def __init__(self): self.logical_context = sys.new_logical_context() def __iter__(self): return self def __next__(self): return sys.run_with_logical_context( self.logical_context, self._next_impl) def _next_impl(self): # Actual __next__ implementation. ... yield from in generator-based coroutines ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prior to :pep:`492`, ``yield from`` was used as one of the mechanisms to implement coroutines in Python. :pep:`492` is built on top of ``yield from`` machinery, and it is even possible to make a generator compatible with async/await code by decorating it with ``@types.coroutine`` (or ``@asyncio.coroutine``). Generators decorated with these decorators follow the Execution Context semantics described below in the `EC Semantics for Coroutines`_ section below. yield from in generators ^^^^^^^^^^^^^^^^^^^^^^^^ Another ``yield from`` use is to compose generators. Essentially, ``yield from gen()`` is a better version of ``for v in gen(): yield v`` (read more about many subtle details in :pep:`380`.) A crucial difference between ``await coro`` and ``yield value`` is that the former expression guarantees that the ``coro`` will be executed fully, while the latter is producing ``value`` and suspending the generator until it gets iterated again. Therefore, this proposal does not special case ``yield from`` expression for regular generators:: item = sys.new_context_key('item') def nested(): assert item.get() == 'outer' item.set('inner') yield def outer(): item.set('outer') yield from nested() assert item.get() == 'outer' EC Semantics for Coroutines --------------------------- Python :pep:`492` coroutines are used to implement cooperative multitasking. For a Python end-user they are similar to threads, especially when it comes to sharing resources or modifying the global state. An event loop is needed to schedule coroutines. Coroutines that are explicitly scheduled by the user are usually called Tasks. When a coroutine is scheduled, it can schedule other coroutines using an ``await`` expression. In async/await world, awaiting a coroutine is equivalent to a regular function call in synchronous code. Thus, Tasks are similar to threads. By drawing a parallel between regular multithreaded code and async/await, it becomes apparent that any modification of the execution context within one Task should be visible to all coroutines scheduled within it. Any execution context modifications, however, must not be visible to other Tasks executing within the same OS thread. Similar to generators, coroutines have the new ``__logical_context__`` attribute and same implementations of ``.send()`` and ``.throw()`` methods. The key difference is that coroutines start with ``__logical_context__`` set to ``NULL`` (generators start with an empty ``LogicalContext``.) This means that it is expected that the asynchronous library and its Task abstraction will control how exactly coroutines interact with Execution Context. Tasks ^^^^^ In asynchronous frameworks like asyncio, coroutines are run by an event loop, and need to be explicitly scheduled (in asyncio coroutines are run by ``asyncio.Task``.) To enable correct Execution Context propagation into Tasks, the asynchronous framework needs to assist the interpreter: * When ``create_task`` is called, it should capture the current execution context with ``sys.get_execution_context()`` and save it on the Task object. * The ``__logical_context__`` of the wrapped coroutine should be initialized to a new empty logical context. * When the Task object runs its coroutine object, it should execute ``.send()`` and ``.throw()`` methods within the captured execution context, using the ``sys.run_with_execution_context()`` function. For ``asyncio.Task``:: class Task: def __init__(self, coro): ... self.exec_context = sys.get_execution_context() coro.__logical_context__ = sys.new_logical_context() def _step(self, val): ... sys.run_with_execution_context( self.exec_context, self.coro.send, val) ... This makes any changes to execution context made by nested coroutine calls within a Task to be visible throughout the Task:: ci = sys.new_context_key('ci') async def nested(): ci.set('nested') async def main(): ci.set('main') print('before:', ci.get()) await nested() print('after:', ci.get()) asyncio.get_event_loop().run_until_complete(main()) # Will print: # before: main # after: nested New Tasks, started within another Task, will run in the correct execution context too:: current_request = sys.new_context_key('current_request') async def child(): print('current request:', repr(current_request.get())) async def handle_request(request): current_request.set(request) event_loop.create_task(child) run(top_coro()) # Will print: # current_request: None The above snippet will run correctly, and the ``child()`` coroutine will be able to access the current request object through the ``current_request`` Context Key. Any of the above examples would work if one the coroutines was a generator decorated with ``@asyncio.coroutine``. Event Loop Callbacks ^^^^^^^^^^^^^^^^^^^^ Similarly to Tasks, functions like asyncio's ``loop.call_soon()`` should capture the current execution context with ``sys.get_execution_context()`` and execute callbacks within it with ``sys.run_with_execution_context()``. This way the following code will work:: current_request = sys.new_context_key('current_request') def log(): request = current_request.get() print(request) async def request_handler(request): current_request.set(request) get_event_loop.call_soon(log) Asynchronous Generators ----------------------- Asynchronous Generators (AG) interact with the Execution Context similarly to regular generators. They have an ``__logical_context__`` attribute, which, similarly to regular generators, can be set to ``None`` to make them use the outer Logical Context. This is used by the new ``contextlib.asynccontextmanager`` decorator. Greenlets --------- Greenlet is an alternative implementation of cooperative scheduling for Python. Although greenlet package is not part of CPython, popular frameworks like gevent rely on it, and it is important that greenlet can be modified to support execution contexts. In a nutshell, greenlet design is very similar to design of generators. The main difference is that for generators, the stack is managed by the Python interpreter. Greenlet works outside of the Python interpreter, and manually saves some ``PyThreadState`` fields and pushes/pops the C-stack. Thus the ``greenlet`` package can be easily updated to use the new low-level `C API`_ to enable full support of EC. New APIs ======== Python ------ Python APIs were designed to completely hide the internal implementation details, but at the same time provide enough control over EC and LC to re-implement all of Python built-in objects in pure Python. 1. ``sys.new_context_key(name: str='...')``: create a ``ContextKey`` object used to access/set values in EC. 2. ``ContextKey``: * ``.name``: read-only attribute. * ``.get()``: return the current value for the key. * ``.set(o)``: set the current value in the EC for the key. 3. ``sys.get_execution_context()``: return the current ``ExecutionContext``. 4. ``sys.new_execution_context()``: create a new empty ``ExecutionContext``. 5. ``sys.new_logical_context()``: create a new empty ``LogicalContext``. 6. ``sys.run_with_execution_context(ec: ExecutionContext, func, *args, **kwargs)``. 7. ``sys.run_with_logical_context(lc:LogicalContext, func, *args, **kwargs)``. C API ----- 1. ``PyContextKey * PyContext_NewKey(char *desc)``: create a ``PyContextKey`` object. 2. ``PyObject * PyContext_GetKey(PyContextKey *)``: get the current value for the context key. 3. ``int PyContext_SetKey(PyContextKey *, PyObject *)``: set the current value for the context key. 4. ``PyLogicalContext * PyLogicalContext_New()``: create a new empty ``PyLogicalContext``. 5. ``PyLogicalContext * PyExecutionContext_New()``: create a new empty ``PyExecutionContext``. 6. ``PyExecutionContext * PyExecutionContext_Get()``: get the EC for the active thread state. 7. ``int PyExecutionContext_Set(PyExecutionContext *)``: set the passed EC object as the current for the active thread state. 8. ``int PyExecutionContext_SetWithLogicalContext(PyExecutionContext *, PyLogicalContext *)``: allows to implement ``sys.run_with_logical_context`` Python API. Implementation Strategy ======================= LogicalContext is a Weak Key Mapping ------------------------------------ Using a weak key mapping for ``LogicalContext`` implementation enables the following properties with regards to garbage collection: * ``ContextKey`` objects are strongly-referenced only from the application code, not from any of the Execution Context machinery or values they point to. This means that there are no reference cycles that could extend their lifespan longer than necessary, or prevent their garbage collection. * Values put in the Execution Context are guaranteed to be kept alive while there is a ``ContextKey`` key referencing them in the thread. * If a ``ContextKey`` is garbage collected, all of its values will be removed from all contexts, allowing them to be GCed if needed. * If a thread has ended its execution, its thread state will be cleaned up along with its ``ExecutionContext``, cleaning up all values bound to all Context Keys in the thread. ContextKey.get() Cache ---------------------- We can add three new fields to ``PyThreadState`` and ``PyInterpreterState`` structs: * ``uint64_t PyThreadState->unique_id``: a globally unique thread state identifier (we can add a counter to ``PyInterpreterState`` and increment it when a new thread state is created.) * ``uint64_t ContextKey->version``: every time the key is updated in any logical context or thread, this key will be incremented. The above two fields allow implementing a fast cache path in ``ContextKey.get()``, in pseudo-code:: class ContextKey: def set(self, value): ... # implementation self.version += 1 def get(self): tstate = PyThreadState_Get() if (self.last_tstate_id == tstate.unique_id and self.last_version == self.version): return self.last_value value = None for mapping in reversed(tstate.execution_context): if self in mapping: value = mapping[self] break self.last_value = value # borrowed ref self.last_tstate_id = tstate.unique_id self.last_version = self.version return value Note that ``last_value`` is a borrowed reference. The assumption is that if current thread and key version tests are OK, the object will be alive. This allows the CK values to be properly GCed. This is similar to the trick that decimal C implementation uses for caching the current decimal context, and will have the same performance characteristics, but available to all Execution Context users. Approach #1: Use a dict for LogicalContext ------------------------------------------ The straightforward way of implementing the proposed EC mechanisms is to create a ``WeakKeyDict`` on top of Python ``dict`` type. To implement the ``ExecutionContext`` type we can use Python ``list`` (or a custom stack implementation with some pre-allocation optimizations). This approach will have the following runtime complexity: * O(M) for ``ContextKey.get()``, where ``M`` is the number of Logical Contexts in the stack. It is important to note that ``ContextKey.get()`` will implement a cache making the operation O(1) for packages like ``decimal`` and ``numpy``. * O(1) for ``ContextKey.set()``. * O(N) for ``sys.get_execution_context()``, where ``N`` is the total number of keys/values in the current **execution** context. Approach #2: Use HAMT for LogicalContext ---------------------------------------- Languages like Clojure and Scala use Hash Array Mapped Tries (HAMT) to implement high performance immutable collections [5]_, [6]_. Immutable mappings implemented with HAMT have O(log\ :sub:`32`\ N) performance for both ``set()``, ``get()``, and ``merge()`` operations, which is essentially O(1) for relatively small mappings (read about HAMT performance in CPython in the `Appendix: HAMT Performance`_ section.) In this approach we use the same design of the ``ExecutionContext`` as in Approach #1, but we will use HAMT backed weak key Logical Context implementation. With that we will have the following runtime complexity: * O(M * log\ :sub:`32`\ N) for ``ContextKey.get()``, where ``M`` is the number of Logical Contexts in the stack, and ``N`` is the number of keys/values in the EC. The operation will essentially be O(M), because execution contexts are normally not expected to have more than a few dozen of keys/values. (``ContextKey.get()`` will have the same caching mechanism as in Approach #1.) * O(log\ :sub:`32`\ N) for ``ContextKey.set()`` where ``N`` is the number of keys/values in the current **logical** context. This will essentially be an O(1) operation most of the time. * O(log\ :sub:`32`\ N) for ``sys.get_execution_context()``, where ``N`` is the total number of keys/values in the current **execution** context. Essentially, using HAMT for Logical Contexts instead of Python dicts, allows to bring down the complexity of ``sys.get_execution_context()`` from O(N) to O(log\ :sub:`32`\ N) because of the more efficient merge algorithm. Approach #3: Use HAMT and Immutable Linked List ----------------------------------------------- We can make an alternative ``ExecutionContext`` design by using a linked list. Each ``LogicalContext`` in the ``ExecutionContext`` object will be wrapped in a linked-list node. ``LogicalContext`` objects will use an HAMT backed weak key implementation described in the Approach #2. Every modification to the current ``LogicalContext`` will produce a new version of it, which will be wrapped in a **new linked list node**. Essentially this means, that ``ExecutionContext`` is an immutable forest of ``LogicalContext`` objects, and can be safely copied by reference in ``sys.get_execution_context()`` (eliminating the expensive "merge" operation.) With this approach, ``sys.get_execution_context()`` will be a constant time **O(1) operation**. In case we decide to apply additional optimizations such as flattening ECs with too many Logical Contexts, HAMT-backed immutable mapping will have a O(log\ :sub:`32`\ N) merge complexity. Summary ------- We believe that approach #3 enables an efficient and complete Execution Context implementation, with excellent runtime performance. `ContextKey.get() Cache`_ enables fast retrieval of context keys for performance critical libraries like decimal and numpy. Fast ``sys.get_execution_context()`` enables efficient management of execution contexts in asynchronous libraries like asyncio. Design Considerations ===================== Can we fix ``PyThreadState_GetDict()``? --------------------------------------- ``PyThreadState_GetDict`` is a TLS, and some of its existing users might depend on it being just a TLS. Changing its behaviour to follow the Execution Context semantics would break backwards compatibility. PEP 521 ------- :pep:`521` proposes an alternative solution to the problem: enhance Context Manager Protocol with two new methods: ``__suspend__`` and ``__resume__``. To make it compatible with async/await, the Asynchronous Context Manager Protocol will also need to be extended with ``__asuspend__`` and ``__aresume__``. This allows to implement context managers like decimal context and ``numpy.errstate`` for generators and coroutines. The following code:: class Context: def __init__(self): self.key = new_context_key('key') def __enter__(self): self.old_x = self.key.get() self.key.set('something') def __exit__(self, *err): self.key.set(self.old_x) would become this:: local = threading.local() class Context: def __enter__(self): self.old_x = getattr(local, 'x', None) local.x = 'something' def __suspend__(self): local.x = self.old_x def __resume__(self): local.x = 'something' def __exit__(self, *err): local.x = self.old_x Besides complicating the protocol, the implementation will likely negatively impact performance of coroutines, generators, and any code that uses context managers, and will notably complicate the interpreter implementation. :pep:`521` also does not provide any mechanism to propagate state in a logical context, like storing a request object in an HTTP request handler to have better logging. Nor does it solve the leaking state problem for greenlet/gevent. Can Execution Context be implemented outside of CPython? -------------------------------------------------------- Because async/await code needs an event loop to run it, an EC-like solution can be implemented in a limited way for coroutines. Generators, on the other hand, do not have an event loop or trampoline, making it impossible to intercept their ``yield`` points outside of the Python interpreter. Should we update sys.displayhook and other APIs to use EC? ---------------------------------------------------------- APIs like redirecting stdout by overwriting ``sys.stdout``, or specifying new exception display hooks by overwriting the ``sys.displayhook`` function are affecting the whole Python process **by design**. Their users assume that the effect of changing them will be visible across OS threads. Therefore we cannot just make these APIs to use the new Execution Context. That said we think it is possible to design new APIs that will be context aware, but that is outside of the scope of this PEP. Backwards Compatibility ======================= This proposal preserves 100% backwards compatibility. Appendix: HAMT Performance ========================== While investigating possibilities of how to implement an immutable mapping in CPython, we were able to improve the efficiency of ``dict.copy()`` up to 5 times: [4]_. One caveat is that the improved ``dict.copy()`` does not resize the dict, which is a necessary thing to do when items get deleted from the dict. Which means that we can make ``dict.copy()`` faster for only dicts that don't need to be resized, and the ones that do, will use a slower version. To assess if HAMT can be used for Execution Context, we implemented it in CPython [7]_. .. figure:: pep-0550-hamt_vs_dict.png :align: center :width: 100% Figure 1. Benchmark code can be found here: [9]_. The chart illustrates the following: * HAMT displays near O(1) performance for all benchmarked dictionary sizes. * If we can use the optimized ``dict.copy()`` implementation ([4]_), the performance of immutable mapping implemented with Python ``dict`` is good up until 100 items. * A dict with an unoptimized ``dict.copy()`` becomes very slow around 100 items. .. figure:: pep-0550-lookup_hamt.png :align: center :width: 100% Figure 2. Benchmark code can be found here: [10]_. Figure 2 shows comparison of lookup costs between Python dict and an HAMT immutable mapping. HAMT lookup time is 30-40% worse than Python dict lookups on average, which is a very good result, considering how well Python dicts are optimized. Note, that according to [8]_, HAMT design can be further improved. The bottom line is that it is possible to imagine a scenario when an application has more than 100 items in the Execution Context, in which case the dict-backed implementation of an immutable mapping becomes a subpar choice. HAMT on the other hand guarantees that its ``set()``, ``get()``, and ``merge()`` operations will execute in O(log\ :sub:`32`\ ) time, which means it is a more future proof solution. Acknowledgments =============== I thank Elvis Pranskevichus and Victor Petrovykh for countless discussions around the topic and PEP proof reading and edits. Thanks to Nathaniel Smith for proposing the ``ContextKey`` design [17]_ [18]_, for pushing the PEP towards a more complete design, and coming up with the idea of having a stack of contexts in the thread state. Thanks to Nick Coghlan for numerous suggestions and ideas on the mailing list, and for coming up with a case that cause the complete rewrite of the initial PEP version [19]_. Version History =============== 1. Posted on 11-Aug-2017, view it here: [20]_. 2. Posted on 15-Aug-2017, view it here: [21]_. The fundamental limitation that caused a complete redesign of the first version was that it was not possible to implement an iterator that would interact with the EC in the same way as generators (see [19]_.) Version 2 was a complete rewrite, introducing new terminology (Local Context, Execution Context, Context Item) and new APIs. 3. Posted on 18-Aug-2017: the current version. Updates: * Local Context was renamed to Logical Context. The term "local" was ambiguous and conflicted with local name scopes. * Context Item was renamed to Context Key, see the thread with Nick Coghlan, Stefan Krah, and Yury Selivanov [22]_ for details. * Context Item get cache design was adjusted, per Nathaniel Smith's idea in [24]_. * Coroutines are created without a Logical Context; ceval loop no longer needs to special case the ``await`` expression (proposed by Nick Coghlan in [23]_.) * `Appendix: HAMT Performance`_ section was updated with more details about the proposed ``dict.copy()`` optimization and its limitations. References ========== .. [1] https://blog.golang.org/context .. [2] https://msdn.microsoft.com/en-us/library/system.threading.executioncontext.a... .. [3] https://github.com/numpy/numpy/issues/9444 .. [4] http://bugs.python.org/issue31179 .. [5] https://en.wikipedia.org/wiki/Hash_array_mapped_trie .. [6] http://blog.higher-order.net/2010/08/16/assoc-and-clojures-persistenthashmap... .. [7] https://github.com/1st1/cpython/tree/hamt .. [8] https://michael.steindorfer.name/publications/oopsla15.pdf .. [9] https://gist.github.com/1st1/9004813d5576c96529527d44c5457dcd .. [10] https://gist.github.com/1st1/dbe27f2e14c30cce6f0b5fddfc8c437e .. [11] https://github.com/1st1/cpython/tree/pep550 .. [12] https://www.python.org/dev/peps/pep-0492/#async-await .. [13] https://github.com/MagicStack/uvloop/blob/master/examples/bench/echoserver.p... .. [14] https://github.com/MagicStack/pgbench .. [15] https://github.com/python/performance .. [16] https://gist.github.com/1st1/6b7a614643f91ead3edf37c4451a6b4c .. [17] https://mail.python.org/pipermail/python-ideas/2017-August/046752.html .. [18] https://mail.python.org/pipermail/python-ideas/2017-August/046772.html .. [19] https://mail.python.org/pipermail/python-ideas/2017-August/046775.html .. [20] https://github.com/python/peps/blob/e8a06c9a790f39451d9e99e203b13b3ad73a1d01... .. [21] https://github.com/python/peps/blob/e3aa3b2b4e4e9967d28a10827eed1e9e5960c175... .. [22] https://mail.python.org/pipermail/python-ideas/2017-August/046801.html .. [23] https://mail.python.org/pipermail/python-ideas/2017-August/046790.html .. [24] https://mail.python.org/pipermail/python-ideas/2017-August/046786.html Copyright ========= This document has been placed in the public domain.

On 19 August 2017 at 06:33, Yury Selivanov <yselivanov.ml@gmail.com> wrote:
Hi,
This is a third iteration of the PEP.
There was some really good feedback on python-ideas and the discussion thread became hard to follow again, so I decided to update the PEP only three days after I published the previous version.
Summary of the changes can be found in the "Version History" section: https://www.python.org/dev/peps/pep-0550/#version-history
There are a few open questions left, namely the terminology and design of ContextKey API. On the former topic, I'm quite happy with the latest version: Execution Context, Logical Context, and Context Key.
Nice, I quite like this version of the naming scheme and the core design in general. While Guido has a point using the same noun for two different things being somewhat confusing, I think the parallel here is the one between the local scope and the lexical (nonlocal) scope for variable names - just as your lexical scope is a nested stack of local scopes in outer functions, your execution context is your current logical context plus a nested stack of outer logical contexts.
Generator Object Modifications ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
To achieve this, we make a small set of modifications to the generator object:
* New ``__logical_context__`` attribute. This attribute is readable and writable for Python code.
* When a generator object is instantiated its ``__logical_context__`` is initialized with an empty ``LogicalContext``.
* Generator's ``.send()`` and ``.throw()`` methods are modified as follows (in pseudo-C)::
if gen.__logical_context__ is not NULL: tstate = PyThreadState_Get()
tstate.execution_context.push(gen.__logical_context__)
try: # Perform the actual `Generator.send()` or # `Generator.throw()` call. return gen.send(...) finally: gen.__logical_context__ = tstate.execution_context.pop() else: # Perform the actual `Generator.send()` or # `Generator.throw()` call. return gen.send(...)
I think this pseudo-code expansion includes a few holdovers from the original visibly-immutable API design. Given the changes since then, I think this would be clearer if the first branch used sys.run_with_logical_context(), since the logical context references at the Python layer now behave like shared mutable objects, and the apparent immutability of sys.run_with_execution_context() comes from injecting a fresh logical context every time. Also +1 to the new design considerations questions that explicitly postpones consideration of any of my "What about..."" questions from python-ideas to future PEPs. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On Sat, Aug 19, 2017 at 4:17 AM, Nick Coghlan <ncoghlan@gmail.com> wrote: [..]
* Generator's ``.send()`` and ``.throw()`` methods are modified as follows (in pseudo-C)::
if gen.__logical_context__ is not NULL: tstate = PyThreadState_Get()
tstate.execution_context.push(gen.__logical_context__)
try: # Perform the actual `Generator.send()` or # `Generator.throw()` call. return gen.send(...) finally: gen.__logical_context__ = tstate.execution_context.pop() else: # Perform the actual `Generator.send()` or # `Generator.throw()` call. return gen.send(...)
I think this pseudo-code expansion includes a few holdovers from the original visibly-immutable API design.
Given the changes since then, I think this would be clearer if the first branch used sys.run_with_logical_context(), since the logical context references at the Python layer now behave like shared mutable objects, and the apparent immutability of sys.run_with_execution_context() comes from injecting a fresh logical context every time.
This is a good idea, I like it! It will indeed simplify the explanation. Yury

On Fri, 18 Aug 2017 16:33:27 -0400 Yury Selivanov <yselivanov.ml@gmail.com> wrote:
There are a few open questions left, namely the terminology and design of ContextKey API. On the former topic, I'm quite happy with the latest version: Execution Context, Logical Context, and Context Key.
I don't really like it. "Logical Context" is vague (there are lots of things called "context" in other libraries, so a bit of specificity would help avoid confusion), and it's not clear what is "logical" about it anyway. "Local Context" actually seemed better to me (as it reminded of threading.local() or the general notion of thread-local storage). Regards Antoine.

On Sat, Aug 19, 2017, 01:43 Antoine Pitrou <solipsis@pitrou.net> wrote:
On Fri, 18 Aug 2017 16:33:27 -0400 Yury Selivanov <yselivanov.ml@gmail.com> wrote:
There are a few open questions left, namely the terminology and design of ContextKey API. On the former topic, I'm quite happy with the latest version: Execution Context, Logical Context, and Context Key.
I don't really like it. "Logical Context" is vague (there are lots of things called "context" in other libraries, so a bit of specificity would help avoid confusion), and it's not clear what is "logical" about it anyway. "Local Context" actually seemed better to me (as it reminded of threading.local() or the general notion of thread-local storage).
I have to admit that I didn't even pick up on that name change. I could go either way. I do appreciate dropping ContextItem, though, because "CI" makes me think of continuous integration. And the overall shape of the API for public consumption LGTM. -brett
Regards
Antoine.
_______________________________________________ Python-Dev mailing list Python-Dev@python.org https://mail.python.org/mailman/listinfo/python-dev Unsubscribe: https://mail.python.org/mailman/options/python-dev/brett%40python.org

On 08/19/2017 01:40 AM, Antoine Pitrou wrote:
On Fri, 18 Aug 2017 16:33:27 -0400 Yury Selivanov wrote:
There are a few open questions left, namely the terminology and design of ContextKey API. On the former topic, I'm quite happy with the latest version: Execution Context, Logical Context, and Context Key.
I don't really like it. "Logical Context" is vague (there are lots of things called "context" in other libraries, so a bit of specificity would help avoid confusion), and it's not clear what is "logical" about it anyway. "Local Context" actually seemed better to me (as it reminded of threading.local() or the general notion of thread-local storage).
I am also not seeing the link between "logical" and "this local layer of environmental changes that won't be seen by those who called me". Maybe ContextLayer? Or marry the two and call it LocalContextLayer. -- ~Ethan~

The way we came to "logical context" was via "logical thread (of control)", which is distinct from OS thread. But I think we might need to search for another term... On Aug 19, 2017 11:56 AM, "Ethan Furman" <ethan@stoneleaf.us> wrote:
On 08/19/2017 01:40 AM, Antoine Pitrou wrote:
On Fri, 18 Aug 2017 16:33:27 -0400 Yury Selivanov wrote:
There are a few open questions left, namely the terminology
and design of ContextKey API. On the former topic, I'm quite happy with the latest version: Execution Context, Logical Context, and Context Key.
I don't really like it. "Logical Context" is vague (there are lots of things called "context" in other libraries, so a bit of specificity would help avoid confusion), and it's not clear what is "logical" about it anyway. "Local Context" actually seemed better to me (as it reminded of threading.local() or the general notion of thread-local storage).
I am also not seeing the link between "logical" and "this local layer of environmental changes that won't be seen by those who called me".
Maybe ContextLayer? Or marry the two and call it LocalContextLayer.
-- ~Ethan~
_______________________________________________ Python-Dev mailing list Python-Dev@python.org https://mail.python.org/mailman/listinfo/python-dev Unsubscribe: https://mail.python.org/mailman/options/python-dev/guido% 40python.org

On 20 August 2017 at 10:21, Guido van Rossum <gvanrossum@gmail.com> wrote:
The way we came to "logical context" was via "logical thread (of control)", which is distinct from OS thread. But I think we might need to search for another term...
Right. Framing it in pragmatic terms, the two entities that we're attempting to name are: 1. The mutable storage that ContextKey.set() writes to 2. The dynamic context that ContextKey.get() queries Right now, we're using ExecutionContext for the latter, and LogicalContext for the former, and I can definitely see Antoine's point that those names don't inherently convey any information about which is which. Personally, I still like the idea of moving ExecutionContext into the "mutable storage" role, and then finding some other name for the stack of execution contexts that ck.get() queries. For example, if we named the latter after what it's *for*, we could call it the DynamicQueryContext, and end up with the following invocation functions: # Replacing ExecutionContext in the current PEP DynamicQueryContext sys.get_dynamic_query_context() sys.new_dynamic_query_context() sys.run_with_dynamic_query_context() # Suggests immutability -> good! # Suggests connection to ck.get() -> good! # Replacing LogicalContext in the current PEP ExecutionContext sys.new_execution_context() sys.run_with_execution_context() __execution_context__ attribute on generators (et al) # Neutral on mutability/immutability # Neutral on ck.set()/ck.get() An alternative would be to dispense with the ExecutionContext name entirely, and instead use DynamicWriteContext and DynamicQueryContext. If we did that, I'd suggest omitting "dynamic" from the function and attribute names (while keeping it on the types), and end up with: # Replacing ExecutionContext in the current PEP DynamicQueryContext sys.get_query_context() sys.new_query_context() sys.run_with_query_context() # Suggests immutability -> good! # Suggests connection to ck.get() -> good! # Replacing LogicalContext in the current PEP DynamicWriteContext sys.new_write_context() sys.run_with_write_context() __write_context__ attribute on generators (et al) # Suggests mutability -> good! # Suggests connection to ck.set() -> good! In this variant, the phrase "execution context" could become a general term that covered *all* of the active state that a running piece of code has access to (the dynamic context, thread locals, closure variables, module globals, process globals, etc), rather than referring to any particular runtime entity. Cheers, Nick. P.S. Since we have LookupError (rather than QueryError) as the shared base exception type for KeyError and IndexError, it would also be entirely reasonable to replace "Query" in the above suggestions with "Lookup" (DynamicLookupContext, sys.get_lookup_context(), etc). That would also have the benefit of being less jargony, and more like conversational English. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On 08/19/2017 10:41 PM, Nick Coghlan wrote:
On 20 August 2017 at 10:21, Guido van Rossum wrote:
The way we came to "logical context" was via "logical thread (of control)", which is distinct from OS thread. But I think we might need to search for another term...
Right. Framing it in pragmatic terms, the two entities that we're attempting to name are:
1. The mutable storage that ContextKey.set() writes to 2. The dynamic context that ContextKey.get() queries
Right now, we're using ExecutionContext for the latter, and LogicalContext for the former, and I can definitely see Antoine's point that those names don't inherently convey any information about which is which.
[snip]
# Replacing ExecutionContext in the current PEP DynamicQueryContext sys.get_dynamic_query_context() sys.new_dynamic_query_context() sys.run_with_dynamic_query_context() # Suggests immutability -> good! # Suggests connection to ck.get() -> good!
# Replacing LogicalContext in the current PEP ExecutionContext sys.new_execution_context() sys.run_with_execution_context() __execution_context__ attribute on generators (et al) # Neutral on mutability/immutability # Neutral on ck.set()/ck.get()
[snippety snip]
# Replacing ExecutionContext in the current PEP DynamicQueryContext sys.get_query_context() sys.new_query_context() sys.run_with_query_context() # Suggests immutability -> good! # Suggests connection to ck.get() -> good!
# Replacing LogicalContext in the current PEP DynamicWriteContext sys.new_write_context() sys.run_with_write_context() __write_context__ attribute on generators (et al) # Suggests mutability -> good! # Suggests connection to ck.set() -> good!
This is just getting more confusing for me. Going back to Yury's original names for now... Relating this naming problem back to globals() and locals(), the correlation works okay for locals/LocalContext, but breaks down at the globals() level because globals() is a specific set of variables -- namely, module-level assignments, while ExecutionContext would be the equivalent of globals, nonlocals, and locals all together. A more accurate name for ExecutionContext might be ParentContext, but that would imply that the LocalContext is not included, and it is (if I finally understand what's going on, of course). So I like ExecutionContext for the stack of WhateverWeCallTheOtherContext contexts. But what do we call it? Again, if I understand what's going on, a normal, threadless, non-async, generator-bereft, plain vanilla Python program is going to have only one LocalContext no matter how many nor how deep the function call chain goes -- so in that sense Local isn't really the best word, but Context all by itself is /really/ unhelpful, and Local does imply "the most current Context Layer". Of all the names proposed so far, I think LocalContext is the best reminder of the thing that CK.key writes to. For the piled layers of LocalContexts that CK.key.get searches through, either ExecutionContext or perhaps ContextEnvironment or even ContextStack works for me (the stack portion not being an implementation detail, but a promise of how it effectively works). -- ~Ethan~

On 22 August 2017 at 09:39, Greg Ewing <greg.ewing@canterbury.ac.nz> wrote:
Ethan Furman wrote:
So I like ExecutionContext for the stack of WhateverWeCallTheOtherContext contexts. But what do we call it?
How about ExecutionContextFrame, by analogy with stack/stack frame.
My latest suggestion to Yury was to see how the PEP reads with it called ImplicitContext, such that: * the active execution context is a stack of implicit contexts * ContextKey.set() updates the innermost implicit context * Contextkey.get() reads the whole stack of active implicit contexts * by default, generators (both sync and async) would have their own implicit context, but you could make them use the context of method callers by doing "gen.__implicit_context__ = None" * by default, coroutines would use their method caller's context, but async frameworks would make sure to give top-level tasks their own independent contexts That proposal came from an initial attempt at redrafting the Abstract and Rationale sections, where it turns out that one of the things the current version of the PEP is somewhat taking for granted is that the reader already has a particular understanding of the difference between explicit state management (i.e. passing things around as function arguments and instance attributes) and implicit state management (i.e. relying on process globals and thread locals). Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On Mon, Aug 21, 2017 at 10:09 PM, Nick Coghlan <ncoghlan@gmail.com> wrote:
My latest suggestion to Yury was to see how the PEP reads with it called ImplicitContext, such that:
* the active execution context is a stack of implicit contexts * ContextKey.set() updates the innermost implicit context * Contextkey.get() reads the whole stack of active implicit contexts * by default, generators (both sync and async) would have their own implicit context, but you could make them use the context of method callers by doing "gen.__implicit_context__ = None" * by default, coroutines would use their method caller's context, but async frameworks would make sure to give top-level tasks their own independent contexts
That proposal came from an initial attempt at redrafting the Abstract and Rationale sections, where it turns out that one of the things the current version of the PEP is somewhat taking for granted is that the reader already has a particular understanding of the difference between explicit state management (i.e. passing things around as function arguments and instance attributes) and implicit state management (i.e. relying on process globals and thread locals).
I think I like ImplicitContext. Maybe we can go with this as a working title at least. I think we should also rethink the form the key framework-facing APIs will take, and how they are presented in the PEP -- I am now leaning towards explaining this from the start as an immutable linked list of immutable mappings, where the OS-thread state gets updated to a new linked list when it is changed (either by ContextKey.set or by the various stack manipulations). I think this falls under several Zen-of-Python points: EIBTI, and "If the implementation is easy to explain, it may be a good idea." -- --Guido van Rossum (python.org/~guido <http://python.org/%7Eguido>)

On Sat, 19 Aug 2017 17:21:03 -0700 Guido van Rossum <gvanrossum@gmail.com> wrote:
The way we came to "logical context" was via "logical thread (of control)", which is distinct from OS thread. But I think we might need to search for another term...
Perhaps "task context"? A "task" might be a logical thread, OS thread, or anything else that deserves a distinct set of implicit parameters. Regards Antoine.

On Sun, Aug 20, 2017, 03:08 Antoine Pitrou <solipsis@pitrou.net> wrote:
On Sat, 19 Aug 2017 17:21:03 -0700 Guido van Rossum <gvanrossum@gmail.com> wrote:
The way we came to "logical context" was via "logical thread (of control)", which is distinct from OS thread. But I think we might need to search for another term...
Perhaps "task context"? A "task" might be a logical thread, OS thread, or anything else that deserves a distinct set of implicit parameters.
Maybe this is skirting too loose to the dynamic scoping, but maybe ContextFrame? This does start to line up with frames of execution which I know is a bit low-level, but then again most people will never need to know about this corner of Python. -brett
Regards
Antoine.
_______________________________________________ Python-Dev mailing list Python-Dev@python.org https://mail.python.org/mailman/listinfo/python-dev Unsubscribe: https://mail.python.org/mailman/options/python-dev/brett%40python.org

On Sun, Aug 20, 2017, 03:08 Antoine Pitrou <solipsis@pitrou.net> wrote:
On Sat, 19 Aug 2017 17:21:03 -0700 Guido van Rossum <gvanrossum@gmail.com> wrote:
The way we came to "logical context" was via "logical thread (of control)", which is distinct from OS thread. But I think we might need to search for another term...
Perhaps "task context"? A "task" might be a logical thread, OS thread, or anything else that deserves a distinct set of implicit parameters.
I think you're on to something here, though I hesitate to use "task" because asyncio.Task is a specific implementation of it. On Sun, Aug 20, 2017 at 7:04 PM, Brett Cannon <brett@python.org> wrote:
Maybe this is skirting too loose to the dynamic scoping, but maybe ContextFrame? This does start to line up with frames of execution which I know is a bit low-level, but then again most people will never need to know about this corner of Python.
I've been thinking that the missing link here may be the execution stack. A logical thread (LT) has a "logical stack" (LS). While a thread of control is a fairly fuzzy concept (once you include things that aren't OS threads), an execution stack is a concrete object, even if not all logical threads represent their execution stack in the same way. For example, a suspended asyncio Task has a stack that is represented by a series of stack frames linked together by await (or yield from), and greenlet apparently uses a different representation again (their term is micro-thread -- maybe we could also do something with that?). Here's the result of some more thinking about this PEP that I've been doing while writing and rewriting this message (with a surprising ending). Let's say that the details of scheduling an LT and managing its mapping onto an LS is defined by a "framework". In this terminology, OS threads are a framework, as are generators, asyncio, and greenlet. There are potentially many different such frameworks. (Some others include Twisted, Tornado and concurrent.futures.ThreadPoolExecutor.) The PEP's big idea is to recognize that there are also many different, non-framework, libraries (e.g. Decimal or Flask) that need to associate some data with an LT. The PEP therefore proposes APIs that allow libraries to do this without caring about what framework is managing the LT, and vice versa (the framework doesn't have to care about how libraries use the per-LT data). The proposed APIs uses two sets of concepts: one set for the framework and one for the library. The library-facing API is simple: create a ContextKey (CK) instance as a global variable in the library, and use its get() and set() methods to access and manipulate the data for that library associated with the current logical thread (LT). Its role is similar to threading.local(), although the API and implementation are completely different, and threading.local() is tied to a specific framework (OS threads). For frameworks the API is more complicated. There are two new classes, LogicalContext (LC) and ExecutionContext (EC). The PEP gives pseudo code suggesting that LC is/contains a dict (whose items are (CK, value) pairs) and an EC is/contains a list of LCs. But in actuality that's only one possible implementation (and not the one proposed for CPython). The key idea is rather that a framework needs to be able to take the data associated with one LT and clone it as the starting point for the data associated for a new LT. This cloning is done by sys.get_execution_context(), and the PEP proposes to use a Hash Array Mapped Trie (HAMT) as the basis for the implementation of LC and EC, to make this cloning fast. IIUC it needs to be fast to match the speed with which many frameworks create and destroy their LTs. The PEP proposes a bunch of new functions in sys for frameworks to manipulate LCs and ECs and their association with the current OS-level thread. Note that OS threads are important here because in the end all frameworks build on top of them. Honestly I'm not sure we need the distinction between LC and EC. If you read carefully some of the given example code seems to confuse them. If we could get away with only a single framework-facing concept, I would be happy calling it ExecutionContext. (Another critique of the proposal I have is that it adds too many similarly-named functions to sys. But this email is already too long and I need to go to bed.) -- --Guido van Rossum (python.org/~guido <http://python.org/%7Eguido>)

On 21 August 2017 at 15:03, Guido van Rossum <guido@python.org> wrote:
Honestly I'm not sure we need the distinction between LC and EC. If you read carefully some of the given example code seems to confuse them. If we could get away with only a single framework-facing concept, I would be happy calling it ExecutionContext.
Unfortunately, I don't think we can, and that's why I tried to reframe the discussion in terms of "Where ContextKey.set() writes to" and "Where ContextKey.get() looks things up". Consider the following toy generator: def tracking_gen(): start_tracking_iterations() while True: tally_iteration() yield task_id = ContextKey("task_id") iter_counter = ContextKey("iter_counter") def start_tracking_iterations(): iter_counter.set(collection.Counter()) def tally_iteration(): current_task = task_id.get() # Set elsewhere iter_counter.get()[current_task] += 1 Now, this isn't a very *sensible* generator (since it could just use a regular object instance for tracking instead of a context variable), but nevertheless, it's one that we would expect to work, and it's one that we would expect to exhibit the following properties: 1. When tally_iteration() calls task_id.get(), we expect that to be resolved in the context calling next() on the instance, *not* the context where the generator was first created 2. When tally_iteration() calls iter_counter.get(), we expect that to be resolved in the same context where start_tracking_iterations() called iter_counter.set() This has consequences for the design in the PEP: * what we want to capture at generator creation time is the context where writes will happen, and we also want that to be the innermost context used for lookups * other than that innermost context, we want everything else to be dynamic * this means that "mutable context saved on the generator" and "entire dynamic context visible when the generator runs" aren't the same thing And hence the introduction of the LocalContext/LogicalContext terminology for the former, and the ExecutionContext terminology for the latter. It's also where the analogy with ChainMap came from (although I don't think this has made it into the PEP itself): * LogicalContext is the equivalent of the individual mappings * ExecutionContext is the equivalent of ChainMap * ContextKey.get() replaces ChainMap.__getitem__ * ContextKey.set(value) replaces ChainMap.__setitem__ * ContextKey.set(None) replaces ChainMap.__delitem__ While the context is defined conceptually as a nested chain of key:value mappings, we avoid using the mapping syntax because of the way the values can shift dynamically out from under you based on who called you - while the ChainMap analogy is hopefully helpful to understanding, we don't want people taking it too literally or things will become more confusing rather than less. Despite that risk, taking the analogy further is where the DynamicWriteContext + DynamicLookupContext terminology idea came from: * like ChainMap.new_child(), adjusting the DynamicWriteContext changes what ck.set() affects, and also sets the innermost context for ck.get() * like using a different ChainMap, adjusting the DynamicLookupContext changes what ck.get() can see (unlike ChainMap, it also isolates ck.set() by default) I'll also note that the first iteration of the PEP didn't really make this distinction, and it caused a problem that Nathaniel pointed out: generators would "snapshot" their entire dynamic context when first created, and then never adjust it for external changes between iterations. This meant that if you adjusted something like the decimal context outside the generator after creating it, it would ignore those changes - instead of having the problem of changes inside the generator leaking out, we instead had the problem of changes outside the generator *not* making their way in, even if you wanted them to. Due to that heritage, fixing some of the examples could easily have been missed in the v2 rewrite that introduced the distinction between the two kinds of context.
(Another critique of the proposal I have is that it adds too many similarly-named functions to sys. But this email is already too long and I need to go to bed.)
If it helps any, one of the ideas that has come up is to put all of the proposed context manipulation APIs in contextlib rather than in sys, and I think that's a reasonable idea (I don't think any of us actually like the notion of adding that many new subsystem specific APIs directly to sys). Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On Mon, Aug 21, 2017 at 7:12 AM, Nick Coghlan <ncoghlan@gmail.com> wrote:
On 21 August 2017 at 15:03, Guido van Rossum <guido@python.org> wrote:
Honestly I'm not sure we need the distinction between LC and EC. If you read carefully some of the given example code seems to confuse them. If we could get away with only a single framework-facing concept, I would be happy calling it ExecutionContext.
Unfortunately, I don't think we can, and that's why I tried to reframe the discussion in terms of "Where ContextKey.set() writes to" and "Where ContextKey.get() looks things up".
Consider the following toy generator:
def tracking_gen(): start_tracking_iterations() while True: tally_iteration() yield
task_id = ContextKey("task_id") iter_counter = ContextKey("iter_counter")
def start_tracking_iterations(): iter_counter.set(collection.Counter())
def tally_iteration(): current_task = task_id.get() # Set elsewhere iter_counter.get()[current_task] += 1
Now, this isn't a very *sensible* generator (since it could just use a regular object instance for tracking instead of a context variable), but nevertheless, it's one that we would expect to work, and it's one that we would expect to exhibit the following properties:
1. When tally_iteration() calls task_id.get(), we expect that to be resolved in the context calling next() on the instance, *not* the context where the generator was first created 2. When tally_iteration() calls iter_counter.get(), we expect that to be resolved in the same context where start_tracking_iterations() called iter_counter.set()
This has consequences for the design in the PEP:
* what we want to capture at generator creation time is the context where writes will happen, and we also want that to be the innermost context used for lookups * other than that innermost context, we want everything else to be dynamic * this means that "mutable context saved on the generator" and "entire dynamic context visible when the generator runs" aren't the same thing
And hence the introduction of the LocalContext/LogicalContext terminology for the former, and the ExecutionContext terminology for the latter.
OK, this is a sensible explanation. I think the PEP would benefit from including some version of it early on (though perhaps shortened a bit).
It's also where the analogy with ChainMap came from (although I don't think this has made it into the PEP itself):
* LogicalContext is the equivalent of the individual mappings * ExecutionContext is the equivalent of ChainMap * ContextKey.get() replaces ChainMap.__getitem__ * ContextKey.set(value) replaces ChainMap.__setitem__ * ContextKey.set(None) replaces ChainMap.__delitem__
While the context is defined conceptually as a nested chain of key:value mappings, we avoid using the mapping syntax because of the way the values can shift dynamically out from under you based on who called you - while the ChainMap analogy is hopefully helpful to understanding, we don't want people taking it too literally or things will become more confusing rather than less.
Agreed. However now I am confused as to how the HAMT fits in. Yury says somewhere that the HAMT will be used for the EC and then cloning the EC is just returning a pointer to the same EC. But even if I interpret that as making a new EC containing a pointer to the same underlying HAMT, I don't see how that will preserve the semantics that different logical threads, running interleaved (like different generators being pumped alternatingly), will see updates to LCs that are lower on the stack of LCs in the EC. (I see this with the stack-of-dicts version, but not with the immutable HAMT inplementation.)
Despite that risk, taking the analogy further is where the DynamicWriteContext + DynamicLookupContext terminology idea came from:
* like ChainMap.new_child(), adjusting the DynamicWriteContext changes what ck.set() affects, and also sets the innermost context for ck.get() * like using a different ChainMap, adjusting the DynamicLookupContext changes what ck.get() can see (unlike ChainMap, it also isolates ck.set() by default)
Here I'm lost again. In the PEP's pseudo code, your first bullet seems to be the operation "push a new LC on the stack of the current EC". Does the second bullet just mean "switch to a different EC"?
I'll also note that the first iteration of the PEP didn't really make this distinction, and it caused a problem that Nathaniel pointed out: generators would "snapshot" their entire dynamic context when first created, and then never adjust it for external changes between iterations. This meant that if you adjusted something like the decimal context outside the generator after creating it, it would ignore those changes - instead of having the problem of changes inside the generator leaking out, we instead had the problem of changes outside the generator *not* making their way in, even if you wanted them to.
OK, this really needs to be made very clear early in the PEP. Maybe this final sentence provides the key requirement: changes outside the generator should make it into the generator when next() is invoked, unless the generator itself has made an override; but changes inside the generator should not leak out through next().
Due to that heritage, fixing some of the examples could easily have been missed in the v2 rewrite that introduced the distinction between the two kinds of context.
At this point I would like to suggest that maybe you and/or Nathaniel could volunteer as co-authors for the PEP. You could then also help Yury clean up his grammar (e.g. adding "the" in various places) and improve the general structure of the PEP.
(Another critique of the proposal I have is that it adds too many similarly-named functions to sys. But this email is already too long and I need to go to bed.)
If it helps any, one of the ideas that has come up is to put all of the proposed context manipulation APIs in contextlib rather than in sys, and I think that's a reasonable idea (I don't think any of us actually like the notion of adding that many new subsystem specific APIs directly to sys).
I don't think it belongs in contextlib. That module is about contexts for use in with-statements; here we are not particularly concerned with those but with manipulating state that is associated with a logical thread. I think it makes more sense to add a new module for this purpose. I also think that some of the framework-facing APIs should probably be methods rather than functions. -- --Guido van Rossum (python.org/~guido)

On Mon, Aug 21, 2017 at 3:10 PM, Guido van Rossum <guido@python.org> wrote: [..]
Agreed. However now I am confused as to how the HAMT fits in. Yury says somewhere that the HAMT will be used for the EC and then cloning the EC is just returning a pointer to the same EC. But even if I interpret that as making a new EC containing a pointer to the same underlying HAMT, I don't see how that will preserve the semantics that different logical threads, running interleaved (like different generators being pumped alternatingly), will see updates to LCs that are lower on the stack of LCs in the EC. (I see this with the stack-of-dicts version, but not with the immutable HAMT inplementation.)
Few important things (using the current PEP 550 terminology): * ExecutionContext is a *dynamic* stack of LogicalContexts. * LCs do not reference other LCs. * ContextKey.set() can only modify the *top* LC in the stack. If LC is a mutable mapping: # EC = [LC1, LC2, LC3, LC4({a: b, foo: bar})] a.set(c) # LC4 = EC.top() # LC4[a] = c # EC = [LC1, LC2, LC3, LC4({a: c, foo: bar})] If LC are implemented with immutable mappings: # EC = [LC1, LC2, LC3, LC4({a: b, foo: bar})] a.set(c) # LC4 = EC.pop() # LC4_1 = LC4.copy() # LC4_1[a] = c # EC.push(LC4_1) # EC = [LC1, LC2, LC3, LC4_1({a: c, foo: bar})] Any code that uses EC will not see any difference, because it can only work with the top LC. Back to generators. Generators have their own empty LCs when created to store their *local* EC modifications. When a generator is *being* iterated, it pushes its LC to the EC. When the iteration step is finished, it pops its LC from the EC. If you have nested generators, they will dynamically build a stack of their LCs while they are iterated. Therefore, generators *naturally* control the stack of EC. We can't execute two generators simultaneously in one thread (we can only iterate them one by one), so the top LC always belongs to the current generator that is being iterated: def nested_gen(): # EC = [outer_LC, gen1_LC, nested_gen_LC] yield # EC = [outer_LC, gen1_LC, nested_gen_LC] yield def gen1(): # EC = [outer_LC, gen1_LC] n = nested_gen() yield # EC = [outer_LC, gen1_LC] next(n) # EC = [outer_LC, gen1_LC] yield next(n) # EC = [outer_LC, gen1_LC] def gen2(): # EC = [outer_LC, gen2_LC] yield # EC = [outer_LC, gen2_LC] yield g1 = gen1() g2 = gen2() next(g1) next(g2) next(g1) next(g2) HAMT is a way to efficiently implement immutable mappings with O(log32 N) set operation, that's it. If we implement immutable mappings using regular dicts and copy, set() would be O(log N). [..]
I'll also note that the first iteration of the PEP didn't really make this distinction, and it caused a problem that Nathaniel pointed out: generators would "snapshot" their entire dynamic context when first created, and then never adjust it for external changes between iterations. This meant that if you adjusted something like the decimal context outside the generator after creating it, it would ignore those changes - instead of having the problem of changes inside the generator leaking out, we instead had the problem of changes outside the generator *not* making their way in, even if you wanted them to.
OK, this really needs to be made very clear early in the PEP. Maybe this final sentence provides the key requirement: changes outside the generator should make it into the generator when next() is invoked, unless the generator itself has made an override; but changes inside the generator should not leak out through next().
It's covered here with two examples: https://www.python.org/dev/peps/pep-0550/#ec-semantics-for-generators Yury

On Mon, Aug 21, 2017 at 12:50 PM, Yury Selivanov <yselivanov.ml@gmail.com> wrote:
Few important things (using the current PEP 550 terminology):
* ExecutionContext is a *dynamic* stack of LogicalContexts. * LCs do not reference other LCs. * ContextKey.set() can only modify the *top* LC in the stack.
If LC is a mutable mapping:
# EC = [LC1, LC2, LC3, LC4({a: b, foo: bar})]
a.set(c) # LC4 = EC.top() # LC4[a] = c
# EC = [LC1, LC2, LC3, LC4({a: c, foo: bar})]
If LC are implemented with immutable mappings:
# EC = [LC1, LC2, LC3, LC4({a: b, foo: bar})]
a.set(c) # LC4 = EC.pop() # LC4_1 = LC4.copy() # LC4_1[a] = c # EC.push(LC4_1)
# EC = [LC1, LC2, LC3, LC4_1({a: c, foo: bar})]
Any code that uses EC will not see any difference, because it can only work with the top LC.
OK, good. This makes more sense, especially if I read "the EC" as shorthand for the EC stored in the current thread's per-thread state. The immutable mapping (if used) is used for the LC, not for the EC, and in this case cloning an EC would simply make a shallow copy of its underlying list -- whereas without the immutable mapping, cloning the EC would also require making shallow copies of each LC. And I guess the linked-list implementation (Approach #3 in the PEP) makes EC cloning an O(1) operation. Note that there is a lot of hand-waving and shorthand in this explanation, but I think I finally follow the design. It is going to be a big task to write this up in a didactic way -- the current PEP needs a fair amount of help in that sense. (If you want to become a better writer, I've recently enjoyed reading Steven Pinker's *The Sense of Style*: The Thinking Person's Guide to Writing in the 21st Century. Amongst other fascinating topics, it explains why so often what we think is clearly written can cause so much confusion.)
Back to generators. Generators have their own empty LCs when created to store their *local* EC modifications.
When a generator is *being* iterated, it pushes its LC to the EC. When the iteration step is finished, it pops its LC from the EC. If you have nested generators, they will dynamically build a stack of their LCs while they are iterated.
Therefore, generators *naturally* control the stack of EC. We can't execute two generators simultaneously in one thread (we can only iterate them one by one), so the top LC always belongs to the current generator that is being iterated:
def nested_gen(): # EC = [outer_LC, gen1_LC, nested_gen_LC] yield # EC = [outer_LC, gen1_LC, nested_gen_LC] yield
def gen1(): # EC = [outer_LC, gen1_LC] n = nested_gen() yield # EC = [outer_LC, gen1_LC] next(n) # EC = [outer_LC, gen1_LC] yield next(n) # EC = [outer_LC, gen1_LC]
def gen2(): # EC = [outer_LC, gen2_LC] yield # EC = [outer_LC, gen2_LC] yield
g1 = gen1() g2 = gen2()
next(g1) next(g2) next(g1) next(g2)
This, combined with your later clarification:
In the current version of the PEP, generators are initialized with an empty LogicalContext. When they are being iterated (started or resumed), their LogicalContext is pushed to the EC. When the iteration is stopped (or paused), they pop their LC from the EC.
makes it clear how the proposal works for generators. There's an important piece that I hadn't figured out from Nick's generator example, because I had mistakenly assumed that something *would* be captured at generator create time. It's a reasonable mistake to make, I think -- the design space here is just huge and there are many variations that don't affect typical code but do differ in edge cases. Your clear statement "nothing needs to be captured" is helpful to avoid this misunderstanding.
HAMT is a way to efficiently implement immutable mappings with O(log32 N) set operation, that's it. If we implement immutable mappings using regular dicts and copy, set() would be O(log N).
This sounds like abuse of the O() notation. Mathematically O(log N) and O(log32 N) surely must be equivalent, since log32 N is just K*(log N) for some constant K (about 0.288539), and the constant disappears in the O(), as O(K*f(N)) and O(f(N)) are equivalent. Now, I'm happy to hear that a HAMT-based implementation is faster than a dict+copy-based implementation, but I don't think your use of O() makes sense here.
[..]
I'll also note that the first iteration of the PEP didn't really make this distinction, and it caused a problem that Nathaniel pointed out: generators would "snapshot" their entire dynamic context when first created, and then never adjust it for external changes between iterations. This meant that if you adjusted something like the decimal context outside the generator after creating it, it would ignore those changes - instead of having the problem of changes inside the generator leaking out, we instead had the problem of changes outside the generator *not* making their way in, even if you wanted them to.
OK, this really needs to be made very clear early in the PEP. Maybe this final sentence provides the key requirement: changes outside the generator should make it into the generator when next() is invoked, unless the generator itself has made an override; but changes inside the generator should not leak out through next().
It's covered here with two examples: https://www.python.org/dev/peps/pep-0550/#ec-semantics-for-generators
I think what's missing is the fact that this is one of the key motivating reasons for the design (starting with v2 of the PEP). When I encountered that section I just skimmed it, assuming it was mostly just showing how to apply the given semantics to generators. I also note some issues with the use of tense here -- it's a bit confusing to follow which parts of the text refer to defects of the current (pre-PEP) situation and which parts refer to how the proposal would solve these defects. -- --Guido van Rossum (python.org/~guido <http://python.org/%7Eguido>)

On Mon, Aug 21, 2017 at 8:06 PM, Guido van Rossum <guido@python.org> wrote:
On Mon, Aug 21, 2017 at 12:50 PM, Yury Selivanov <yselivanov.ml@gmail.com> wrote:
Few important things (using the current PEP 550 terminology):
* ExecutionContext is a *dynamic* stack of LogicalContexts. * LCs do not reference other LCs. * ContextKey.set() can only modify the *top* LC in the stack.
If LC is a mutable mapping:
# EC = [LC1, LC2, LC3, LC4({a: b, foo: bar})]
a.set(c) # LC4 = EC.top() # LC4[a] = c
# EC = [LC1, LC2, LC3, LC4({a: c, foo: bar})]
If LC are implemented with immutable mappings:
# EC = [LC1, LC2, LC3, LC4({a: b, foo: bar})]
a.set(c) # LC4 = EC.pop() # LC4_1 = LC4.copy() # LC4_1[a] = c # EC.push(LC4_1)
# EC = [LC1, LC2, LC3, LC4_1({a: c, foo: bar})]
Any code that uses EC will not see any difference, because it can only work with the top LC.
OK, good. This makes more sense, especially if I read "the EC" as shorthand for the EC stored in the current thread's per-thread state.
That's exactly what I meant by "the EC".
The immutable mapping (if used) is used for the LC, not for the EC, and in this case cloning an EC would simply make a shallow copy of its underlying list -- whereas without the immutable mapping, cloning the EC would also require making shallow copies of each LC. And I guess the linked-list implementation (Approach #3 in the PEP) makes EC cloning an O(1) operation.
All correct.
Note that there is a lot of hand-waving and shorthand in this explanation, but I think I finally follow the design. It is going to be a big task to write this up in a didactic way -- the current PEP needs a fair amount of help in that sense.
Elvis Pranskevichus (our current What's New editor and my colleague) offered me to help with the PEP. He's now working on a partial rewrite. I've been working on this PEP for about a month now and at this point it makes it difficult for me to dump this knowledge in a nice and readable way (in any language that I know, FWIW).
(If you want to become a better writer, I've recently enjoyed reading Steven Pinker's The Sense of Style: The Thinking Person's Guide to Writing in the 21st Century. Amongst other fascinating topics, it explains why so often what we think is clearly written can cause so much confusion.)
Will definitely check it out, thank you!
Back to generators. Generators have their own empty LCs when created to store their *local* EC modifications.
When a generator is *being* iterated, it pushes its LC to the EC. When the iteration step is finished, it pops its LC from the EC. If you have nested generators, they will dynamically build a stack of their LCs while they are iterated.
Therefore, generators *naturally* control the stack of EC. We can't execute two generators simultaneously in one thread (we can only iterate them one by one), so the top LC always belongs to the current generator that is being iterated:
def nested_gen(): # EC = [outer_LC, gen1_LC, nested_gen_LC] yield # EC = [outer_LC, gen1_LC, nested_gen_LC] yield
def gen1(): # EC = [outer_LC, gen1_LC] n = nested_gen() yield # EC = [outer_LC, gen1_LC] next(n) # EC = [outer_LC, gen1_LC] yield next(n) # EC = [outer_LC, gen1_LC]
def gen2(): # EC = [outer_LC, gen2_LC] yield # EC = [outer_LC, gen2_LC] yield
g1 = gen1() g2 = gen2()
next(g1) next(g2) next(g1) next(g2)
This, combined with your later clarification:
In the current version of the PEP, generators are initialized with an empty LogicalContext. When they are being iterated (started or resumed), their LogicalContext is pushed to the EC. When the iteration is stopped (or paused), they pop their LC from the EC.
makes it clear how the proposal works for generators. There's an important piece that I hadn't figured out from Nick's generator example, because I had mistakenly assumed that something *would* be captured at generator create time. It's a reasonable mistake to make,
Yeah, it is very subtle.
HAMT is a way to efficiently implement immutable mappings with O(log32 N) set operation, that's it. If we implement immutable mappings using regular dicts and copy, set() would be O(log N).
This sounds like abuse of the O() notation. Mathematically O(log N) and O(log32 N) surely must be equivalent, since log32 N is just K*(log N) for some constant K (about 0.288539), and the constant disappears in the O(), as O(K*f(N)) and O(f(N)) are equivalent. Now, I'm happy to hear that a HAMT-based implementation is faster than a dict+copy-based implementation, but I don't think your use of O() makes sense here.
I made a typo there: when implementing an immutable mapping with Python dicts, setting a key is an O(N) operation (not O(log N)): we need to make a shallow copy of a dict and then add an item to it. (the PEP doesn't have this typo) With HAMT, set() is O(log32 N): https://github.com/python/peps/blob/master/pep-0550-hamt_vs_dict.png Yury

On Mon, Aug 21, 2017 at 8:06 PM, Guido van Rossum <guido@python.org> wrote: [..]
OK, this really needs to be made very clear early in the PEP. Maybe this final sentence provides the key requirement: changes outside the generator should make it into the generator when next() is invoked, unless the generator itself has made an override; but changes inside the generator should not leak out through next().
It's covered here with two examples: https://www.python.org/dev/peps/pep-0550/#ec-semantics-for-generators
I think what's missing is the fact that this is one of the key motivating reasons for the design (starting with v2 of the PEP). When I encountered that section I just skimmed it, assuming it was mostly just showing how to apply the given semantics to generators. I also note some issues with the use of tense here -- it's a bit confusing to follow which parts of the text refer to defects of the current (pre-PEP) situation and which parts refer to how the proposal would solve these defects.
I see. The proposal always uses present tense to describe things it adds, and I now see that this is indeed very confusing. This needs to be fixed. Yury

On Mon, Aug 21, 2017 at 5:12 PM, Nick Coghlan <ncoghlan@gmail.com> wrote:
On 21 August 2017 at 15:03, Guido van Rossum <guido@python.org> wrote:
Honestly I'm not sure we need the distinction between LC and EC. If you read carefully some of the given example code seems to confuse them. If we could get away with only a single framework-facing concept, I would be happy calling it ExecutionContext.
Unfortunately, I don't think we can, and that's why I tried to reframe the discussion in terms of "Where ContextKey.set() writes to" and "Where ContextKey.get() looks things up".
Consider the following toy generator:
def tracking_gen(): start_tracking_iterations() while True: tally_iteration() yield
task_id = ContextKey("task_id") iter_counter = ContextKey("iter_counter")
def start_tracking_iterations(): iter_counter.set(collection.Counter())
def tally_iteration(): current_task = task_id.get() # Set elsewhere iter_counter.get()[current_task] += 1
Now, this isn't a very *sensible* generator (since it could just use a regular object instance for tracking instead of a context variable), but nevertheless, it's one that we would expect to work, and it's one that we would expect to exhibit the following properties:
1. When tally_iteration() calls task_id.get(), we expect that to be resolved in the context calling next() on the instance, *not* the context where the generator was first created 2. When tally_iteration() calls iter_counter.get(), we expect that to be resolved in the same context where start_tracking_iterations() called iter_counter.set()
This has consequences for the design in the PEP:
* what we want to capture at generator creation time is the context where writes will happen, and we also want that to be the innermost context used for lookups
I don't get it. How is this a consequence of the above two points? And why do we need to capture something (a "context") at generator creation time? -- Koos
* other than that innermost context, we want everything else to be dynamic * this means that "mutable context saved on the generator" and "entire dynamic context visible when the generator runs" aren't the same thing
And hence the introduction of the LocalContext/LogicalContext terminology for the former, and the ExecutionContext terminology for the latter.
[...]
-- + Koos Zevenhoven + http://twitter.com/k7hoven +

On Mon, Aug 21, 2017 at 5:14 PM, Koos Zevenhoven <k7hoven@gmail.com> wrote: [..]
This has consequences for the design in the PEP:
* what we want to capture at generator creation time is the context where writes will happen, and we also want that to be the innermost context used for lookups
I don't get it. How is this a consequence of the above two points? And why do we need to capture something (a "context") at generator creation time?
We don't need to "capture" anything when a generator is created (it was something that PEP 550 version 1 was doing). In the current version of the PEP, generators are initialized with an empty LogicalContext. When they are being iterated (started or resumed), their LogicalContext is pushed to the EC. When the iteration is stopped (or paused), they pop their LC from the EC. Yury

On Tue, Aug 22, 2017 at 12:25 AM, Yury Selivanov <yselivanov.ml@gmail.com> wrote:
On Mon, Aug 21, 2017 at 5:14 PM, Koos Zevenhoven <k7hoven@gmail.com> wrote: [..]
This has consequences for the design in the PEP:
* what we want to capture at generator creation time is the context where writes will happen, and we also want that to be the innermost context used for lookups
I don't get it. How is this a consequence of the above two points? And why do we need to capture something (a "context") at generator creation time?
We don't need to "capture" anything when a generator is created (it was something that PEP 550 version 1 was doing).
Ok, good.
In the current version of the PEP, generators are initialized with an empty LogicalContext. When they are being iterated (started or resumed), their LogicalContext is pushed to the EC. When the iteration is stopped (or paused), they pop their LC from the EC.
Another quick one before I go: Do we really need to push and pop a LC on each next() call, even if it most likely will never be touched? -- Koos -- + Koos Zevenhoven + http://twitter.com/k7hoven +

On Mon, Aug 21, 2017 at 5:39 PM, Koos Zevenhoven <k7hoven@gmail.com> wrote: [..]
In the current version of the PEP, generators are initialized with an empty LogicalContext. When they are being iterated (started or resumed), their LogicalContext is pushed to the EC. When the iteration is stopped (or paused), they pop their LC from the EC.
Another quick one before I go: Do we really need to push and pop a LC on each next() call, even if it most likely will never be touched?
Yes, otherwise it will be hard to maintain the consistency of the stack. There will be an optimization: if the LC is empty, we will push NULL to the stack, thus avoiding the cost of allocating an object. I measured the overhead -- generators will become 0.5-1% slower in microbenchmarks, but only when they do pretty much nothing. If a generator contains more Python code than a bare "yield" expression, the overhead will be harder to detect. Yury

On Tue, Aug 22, 2017 at 12:44 AM, Yury Selivanov <yselivanov.ml@gmail.com> wrote:
On Mon, Aug 21, 2017 at 5:39 PM, Koos Zevenhoven <k7hoven@gmail.com> wrote: [..]
In the current version of the PEP, generators are initialized with an empty LogicalContext. When they are being iterated (started or resumed), their LogicalContext is pushed to the EC. When the iteration is stopped (or paused), they pop their LC from the EC.
Another quick one before I go: Do we really need to push and pop a LC on each next() call, even if it most likely will never be touched?
Yes, otherwise it will be hard to maintain the consistency of the stack.
There will be an optimization: if the LC is empty, we will push NULL to the stack, thus avoiding the cost of allocating an object.
But if LCs are immutable, there needs to be only one empty-LC instance. That would avoid special-casing NULL in code.
-- Koos
I measured the overhead -- generators will become 0.5-1% slower in microbenchmarks, but only when they do pretty much nothing. If a generator contains more Python code than a bare "yield" expression, the overhead will be harder to detect.
-- + Koos Zevenhoven + http://twitter.com/k7hoven +

On Tue, Aug 22, 2017 at 2:06 AM, Koos Zevenhoven <k7hoven@gmail.com> wrote: [..]
There will be an optimization: if the LC is empty, we will push NULL to the stack, thus avoiding the cost of allocating an object.
But if LCs are immutable, there needs to be only one empty-LC instance. That would avoid special-casing NULL in code.
Yes, this is true. Yury

Hi Yury, On 08/18/2017 10:33 PM, Yury Selivanov wrote:
* ``.get()`` method: return the current EC value for the context key. Context keys return ``None`` when the key is missing, so the method never fails. Is the difference between `Key not found` and `value is None` important here?
Thanks, --francis
participants (10)
-
Antoine Pitrou
-
Brett Cannon
-
Ethan Furman
-
francismb
-
Greg Ewing
-
Guido van Rossum
-
Guido van Rossum
-
Koos Zevenhoven
-
Nick Coghlan
-
Yury Selivanov