2017-09-08 17:55:50 -04:00
|
|
|
PEP: 558
|
|
|
|
Title: Defined semantics for locals()
|
2023-10-11 08:05:51 -04:00
|
|
|
Author: Alyssa Coghlan <ncoghlan@gmail.com>
|
2019-04-23 19:04:37 -04:00
|
|
|
BDFL-Delegate: Nathaniel J. Smith
|
2022-02-27 17:46:36 -05:00
|
|
|
Discussions-To: python-dev@python.org
|
2024-04-19 00:06:29 -04:00
|
|
|
Status: Withdrawn
|
2017-09-08 17:55:50 -04:00
|
|
|
Type: Standards Track
|
|
|
|
Content-Type: text/x-rst
|
2021-02-09 11:54:26 -05:00
|
|
|
Created: 08-Sep-2017
|
2023-03-11 02:49:43 -05:00
|
|
|
Python-Version: 3.13
|
2022-03-09 11:04:44 -05:00
|
|
|
Post-History: 08-Sep-2017, 22-May-2019, 30-May-2019, 30-Dec-2019, 18-Jul-2021,
|
|
|
|
26-Aug-2021
|
2017-09-08 17:55:50 -04:00
|
|
|
|
2024-04-19 00:06:29 -04:00
|
|
|
PEP Withdrawal
|
|
|
|
==============
|
2023-03-11 02:49:43 -05:00
|
|
|
|
2024-04-19 00:06:29 -04:00
|
|
|
In December 2021, this PEP and :pep:`667` converged on a common definition of the
|
|
|
|
proposed changes to the Python level semantics of the ``locals()`` builtin (as
|
|
|
|
documented in the PEP text below), with the only remaining differences being
|
|
|
|
in the proposed C API changes and various internal implementation details.
|
2023-03-11 02:49:43 -05:00
|
|
|
|
2024-04-19 00:06:29 -04:00
|
|
|
Of those remaining differences, the most significant one was that :pep:`667`
|
|
|
|
at the time still proposed an immediate backwards compatibility break for the
|
|
|
|
``PyEval_GetLocals()`` API as soon as the PEP was accepted and implemented.
|
2023-03-11 02:49:43 -05:00
|
|
|
|
2024-04-19 00:06:29 -04:00
|
|
|
:pep:`667` has since been changed to propose a generous deprecation period for
|
|
|
|
the ``PyEval_GetLocals()`` API, continuing to support it in parallel with the
|
|
|
|
improved semantics offered by the new ``PyEval_GetFrameLocals()`` API.
|
2023-03-11 02:49:43 -05:00
|
|
|
|
2024-04-19 00:06:29 -04:00
|
|
|
Any remaining C API design concerns relate to new informational APIs that can be
|
|
|
|
added at a later date if they are deemed necessary, and any potential concerns
|
|
|
|
about the exact performance characteristics of the frame locals view implementation
|
|
|
|
are outweighed by the availability of a viable reference implementation.
|
2023-03-11 02:49:43 -05:00
|
|
|
|
2024-04-19 00:06:29 -04:00
|
|
|
Accordingly, this PEP has been withdrawn in favour of proceeding with :pep:`667`.
|
2017-09-08 17:55:50 -04:00
|
|
|
|
2024-08-04 23:54:25 -04:00
|
|
|
Note: while implementing :pep:`667` it became apparent that the rationale for and impact
|
|
|
|
of ``locals()`` being updated to return independent snapshots in
|
|
|
|
:term:`optimized scopes <py3.13:optimized scope>` was not entirely clear in either PEP.
|
|
|
|
The Motivation and Rationale sections in this PEP have been updated accordingly (since those
|
|
|
|
aspects are equally applicable to the accepted :pep:`667`).
|
|
|
|
|
2017-09-08 17:55:50 -04:00
|
|
|
Abstract
|
|
|
|
========
|
|
|
|
|
|
|
|
The semantics of the ``locals()`` builtin have historically been underspecified
|
|
|
|
and hence implementation dependent.
|
|
|
|
|
2021-06-27 03:37:01 -04:00
|
|
|
This PEP proposes formally standardising on the behaviour of the CPython 3.10
|
2017-09-08 17:55:50 -04:00
|
|
|
reference implementation for most execution scopes, with some adjustments to the
|
2017-09-17 03:40:13 -04:00
|
|
|
behaviour at function scope to make it more predictable and independent of the
|
2017-09-08 17:55:50 -04:00
|
|
|
presence or absence of tracing functions.
|
|
|
|
|
2020-02-16 07:10:19 -05:00
|
|
|
In addition, it proposes that the following functions be added to the stable
|
2022-08-17 01:26:55 -04:00
|
|
|
Python C API/ABI:
|
|
|
|
|
|
|
|
.. code-block:: c
|
2020-02-16 07:10:19 -05:00
|
|
|
|
2021-07-21 08:28:21 -04:00
|
|
|
typedef enum {
|
|
|
|
PyLocals_UNDEFINED = -1,
|
|
|
|
PyLocals_DIRECT_REFERENCE = 0,
|
2021-08-26 05:26:13 -04:00
|
|
|
PyLocals_SHALLOW_COPY = 1,
|
|
|
|
_PyLocals_ENSURE_32BIT_ENUM = 2147483647
|
2021-07-21 08:28:21 -04:00
|
|
|
} PyLocals_Kind;
|
|
|
|
|
|
|
|
PyLocals_Kind PyLocals_GetKind();
|
2020-02-16 07:10:19 -05:00
|
|
|
PyObject * PyLocals_Get();
|
|
|
|
PyObject * PyLocals_GetCopy();
|
|
|
|
|
|
|
|
It also proposes the addition of several supporting functions and type
|
|
|
|
definitions to the CPython C API.
|
|
|
|
|
2024-08-04 23:54:25 -04:00
|
|
|
.. _pep-558-motivation:
|
2021-07-17 07:16:51 -04:00
|
|
|
|
2021-08-26 05:26:13 -04:00
|
|
|
Motivation
|
|
|
|
==========
|
2017-09-08 17:55:50 -04:00
|
|
|
|
|
|
|
While the precise semantics of the ``locals()`` builtin are nominally undefined,
|
|
|
|
in practice, many Python programs depend on it behaving exactly as it behaves in
|
|
|
|
CPython (at least when no tracing functions are installed).
|
|
|
|
|
|
|
|
Other implementations such as PyPy are currently replicating that behaviour,
|
|
|
|
up to and including replication of local variable mutation bugs that
|
|
|
|
can arise when a trace hook is installed [1]_.
|
|
|
|
|
2019-04-23 19:04:37 -04:00
|
|
|
While this PEP considers CPython's current behaviour when no trace hooks are
|
2019-12-30 01:22:46 -05:00
|
|
|
installed to be largely acceptable, it considers the current
|
2019-04-23 19:04:37 -04:00
|
|
|
behaviour when trace hooks are installed to be problematic, as it causes bugs
|
|
|
|
like [1]_ *without* even reliably enabling the desired functionality of allowing
|
|
|
|
debuggers like ``pdb`` to mutate local variables [3]_.
|
2017-10-22 02:12:25 -04:00
|
|
|
|
2019-12-30 01:22:46 -05:00
|
|
|
Review of the initial PEP and the draft implementation then identified an
|
|
|
|
opportunity for simplification of both the documentation and implementation
|
|
|
|
of the function level ``locals()`` behaviour by updating it to return an
|
2020-02-16 07:10:19 -05:00
|
|
|
independent snapshot of the function locals and closure variables on each
|
|
|
|
call, rather than continuing to return the semi-dynamic intermittently updated
|
|
|
|
shared copy that it has historically returned in CPython.
|
2019-12-30 01:22:46 -05:00
|
|
|
|
2024-08-04 23:54:25 -04:00
|
|
|
Specifically, the proposal in this PEP eliminates the historical behaviour where
|
|
|
|
adding a new local variable can change the behaviour of code executed with
|
|
|
|
``exec()`` in function scopes, even if that code runs *before* the local variable
|
|
|
|
is defined.
|
|
|
|
|
|
|
|
For example::
|
|
|
|
|
|
|
|
def f():
|
|
|
|
exec("x = 1")
|
|
|
|
print(locals().get("x"))
|
|
|
|
f()
|
|
|
|
|
|
|
|
prints ``1``, but::
|
|
|
|
|
|
|
|
def f():
|
|
|
|
exec("x = 1")
|
|
|
|
print(locals().get("x"))
|
|
|
|
x = 0
|
|
|
|
f()
|
|
|
|
|
|
|
|
prints ``None`` (the default value from the ``.get()`` call).
|
|
|
|
|
|
|
|
With this PEP both examples would print ``None``, as the call to
|
|
|
|
``exec()`` and the subsequent call to ``locals()`` would use
|
|
|
|
independent dictionary snapshots of the local variables rather
|
|
|
|
than using the same shared dictionary cached on the frame object.
|
2017-09-08 17:55:50 -04:00
|
|
|
|
|
|
|
Proposal
|
|
|
|
========
|
|
|
|
|
|
|
|
The expected semantics of the ``locals()`` builtin change based on the current
|
|
|
|
execution scope. For this purpose, the defined scopes of execution are:
|
|
|
|
|
|
|
|
* module scope: top-level module code, as well as any other code executed using
|
|
|
|
``exec()`` or ``eval()`` with a single namespace
|
|
|
|
* class scope: code in the body of a ``class`` statement, as well as any other
|
|
|
|
code executed using ``exec()`` or ``eval()`` with separate local and global
|
|
|
|
namespaces
|
2019-12-30 01:22:46 -05:00
|
|
|
* function scope: code in the body of a ``def`` or ``async def`` statement,
|
|
|
|
or any other construct that creates an optimized code block in CPython (e.g.
|
|
|
|
comprehensions, lambda functions)
|
2017-09-08 17:55:50 -04:00
|
|
|
|
2021-01-27 08:59:56 -05:00
|
|
|
This PEP proposes elevating most of the current behaviour of the CPython
|
|
|
|
reference implementation to become part of the language specification, *except*
|
|
|
|
that each call to ``locals()`` at function scope will create a new dictionary
|
|
|
|
object, rather than caching a common dict instance in the frame object that
|
|
|
|
each invocation will update and return.
|
|
|
|
|
|
|
|
This PEP also proposes to largely eliminate the concept of a separate "tracing"
|
|
|
|
mode from the CPython reference implementation. In releases up to and including
|
2021-06-27 03:37:01 -04:00
|
|
|
Python 3.10, the CPython interpreter behaves differently when a trace hook has
|
2021-01-27 08:59:56 -05:00
|
|
|
been registered in one or more threads via an implementation dependent mechanism
|
|
|
|
like ``sys.settrace`` ([4]_) in CPython's ``sys`` module or
|
2021-08-26 05:26:13 -04:00
|
|
|
``PyEval_SetTrace`` ([5]_) in CPython's C API. If this PEP is accepted, then
|
|
|
|
the only remaining behavioural difference when a trace hook is installed is that
|
|
|
|
some optimisations in the interpreter eval loop are disabled when the tracing
|
|
|
|
logic needs to run after each opcode.
|
2021-01-27 08:59:56 -05:00
|
|
|
|
|
|
|
This PEP proposes changes to CPython's behaviour at function scope that make
|
|
|
|
the ``locals()`` builtin semantics when a trace hook is registered identical to
|
|
|
|
those used when no trace hook is registered, while also making the related frame
|
|
|
|
API semantics clearer and easier for interactive debuggers to rely on.
|
|
|
|
|
|
|
|
The proposed elimination of tracing mode affects the semantics of frame object
|
2019-05-21 08:41:34 -04:00
|
|
|
references obtained through other means, such as via a traceback, or via the
|
2021-06-27 03:37:01 -04:00
|
|
|
``sys._getframe()`` API, as the write-through semantics needed for trace hook
|
|
|
|
support are always provided by the ``f_locals`` attribute on frame objects,
|
|
|
|
rather than being runtime state dependent.
|
2019-05-21 08:41:34 -04:00
|
|
|
|
2017-10-22 02:12:25 -04:00
|
|
|
|
|
|
|
New ``locals()`` documentation
|
|
|
|
------------------------------
|
|
|
|
|
|
|
|
The heart of this proposal is to revise the documentation for the ``locals()``
|
|
|
|
builtin to read as follows:
|
|
|
|
|
2019-12-30 01:22:46 -05:00
|
|
|
Return a mapping object representing the current local symbol table, with
|
2017-10-22 02:12:25 -04:00
|
|
|
variable names as the keys, and their currently bound references as the
|
2019-12-30 01:22:46 -05:00
|
|
|
values.
|
2017-10-22 02:12:25 -04:00
|
|
|
|
|
|
|
At module scope, as well as when using ``exec()`` or ``eval()`` with a
|
|
|
|
single namespace, this function returns the same namespace as ``globals()``.
|
|
|
|
|
|
|
|
At class scope, it returns the namespace that will be passed to the
|
|
|
|
metaclass constructor.
|
|
|
|
|
|
|
|
When using ``exec()`` or ``eval()`` with separate local and global
|
|
|
|
namespaces, it returns the local namespace passed in to the function call.
|
|
|
|
|
2019-12-30 01:22:46 -05:00
|
|
|
In all of the above cases, each call to ``locals()`` in a given frame of
|
|
|
|
execution will return the *same* mapping object. Changes made through
|
|
|
|
the mapping object returned from ``locals()`` will be visible as bound,
|
|
|
|
rebound, or deleted local variables, and binding, rebinding, or deleting
|
|
|
|
local variables will immediately affect the contents of the returned mapping
|
|
|
|
object.
|
|
|
|
|
|
|
|
At function scope (including for generators and coroutines), each call to
|
2020-02-16 07:10:19 -05:00
|
|
|
``locals()`` instead returns a fresh dictionary containing the current
|
|
|
|
bindings of the function's local variables and any nonlocal cell references.
|
|
|
|
In this case, name binding changes made via the returned dict are *not*
|
|
|
|
written back to the corresponding local variables or nonlocal cell
|
|
|
|
references, and binding, rebinding, or deleting local variables and nonlocal
|
|
|
|
cell references does *not* affect the contents of previously returned
|
|
|
|
dictionaries.
|
2019-12-30 01:22:46 -05:00
|
|
|
|
|
|
|
|
2022-08-17 01:26:55 -04:00
|
|
|
There would also be a ``versionchanged`` note for the release making this change:
|
2019-12-30 01:22:46 -05:00
|
|
|
|
|
|
|
In prior versions, the semantics of mutating the mapping object returned
|
|
|
|
from ``locals()`` were formally undefined. In CPython specifically,
|
|
|
|
the mapping returned at function scope could be implicitly refreshed by
|
|
|
|
other operations, such as calling ``locals()`` again, or the interpreter
|
|
|
|
implicitly invoking a Python level trace function. Obtaining the legacy
|
2020-02-16 07:10:19 -05:00
|
|
|
CPython behaviour now requires explicit calls to update the initially
|
|
|
|
returned dictionary with the results of subsequent calls to ``locals()``.
|
2017-10-22 02:12:25 -04:00
|
|
|
|
|
|
|
|
|
|
|
For reference, the current documentation of this builtin reads as follows:
|
|
|
|
|
|
|
|
Update and return a dictionary representing the current local symbol table.
|
|
|
|
Free variables are returned by locals() when it is called in function
|
|
|
|
blocks, but not in class blocks.
|
|
|
|
|
|
|
|
Note: The contents of this dictionary should not be modified; changes may
|
|
|
|
not affect the values of local and free variables used by the interpreter.
|
|
|
|
|
2019-04-23 19:04:37 -04:00
|
|
|
(In other words: the status quo is that the semantics and behaviour of
|
2019-12-30 01:22:46 -05:00
|
|
|
``locals()`` are formally implementation defined, whereas the proposed
|
2019-04-23 19:04:37 -04:00
|
|
|
state after this PEP is that the only implementation defined behaviour will be
|
2019-12-30 01:22:46 -05:00
|
|
|
that associated with whether or not the implementation emulates the CPython
|
|
|
|
frame API, with the behaviour in all other cases being defined by the language
|
|
|
|
and library references)
|
2019-04-23 19:04:37 -04:00
|
|
|
|
2017-09-08 17:55:50 -04:00
|
|
|
|
|
|
|
Module scope
|
|
|
|
------------
|
|
|
|
|
|
|
|
At module scope, as well as when using ``exec()`` or ``eval()`` with a
|
|
|
|
single namespace, ``locals()`` must return the same object as ``globals()``,
|
|
|
|
which must be the actual execution namespace (available as
|
|
|
|
``inspect.currentframe().f_locals`` in implementations that provide access
|
|
|
|
to frame objects).
|
|
|
|
|
|
|
|
Variable assignments during subsequent code execution in the same scope must
|
|
|
|
dynamically change the contents of the returned mapping, and changes to the
|
|
|
|
returned mapping must change the values bound to local variable names in the
|
|
|
|
execution environment.
|
|
|
|
|
2017-10-22 02:12:25 -04:00
|
|
|
To capture this expectation as part of the language specification, the following
|
|
|
|
paragraph will be added to the documentation for ``locals()``:
|
|
|
|
|
|
|
|
At module scope, as well as when using ``exec()`` or ``eval()`` with a
|
|
|
|
single namespace, this function returns the same namespace as ``globals()``.
|
|
|
|
|
2017-09-08 17:55:50 -04:00
|
|
|
This part of the proposal does not require any changes to the reference
|
|
|
|
implementation - it is standardisation of the current behaviour.
|
|
|
|
|
|
|
|
|
|
|
|
Class scope
|
|
|
|
-----------
|
|
|
|
|
|
|
|
At class scope, as well as when using ``exec()`` or ``eval()`` with separate
|
|
|
|
global and local namespaces, ``locals()`` must return the specified local
|
|
|
|
namespace (which may be supplied by the metaclass ``__prepare__`` method
|
|
|
|
in the case of classes). As for module scope, this must be a direct reference
|
|
|
|
to the actual execution namespace (available as
|
|
|
|
``inspect.currentframe().f_locals`` in implementations that provide access
|
|
|
|
to frame objects).
|
|
|
|
|
|
|
|
Variable assignments during subsequent code execution in the same scope must
|
|
|
|
change the contents of the returned mapping, and changes to the returned mapping
|
|
|
|
must change the values bound to local variable names in the
|
|
|
|
execution environment.
|
|
|
|
|
2017-10-22 02:12:25 -04:00
|
|
|
The mapping returned by ``locals()`` will *not* be used as the actual class
|
|
|
|
namespace underlying the defined class (the class creation process will copy
|
|
|
|
the contents to a fresh dictionary that is only accessible by going through the
|
|
|
|
class machinery).
|
2017-09-08 17:55:50 -04:00
|
|
|
|
2017-09-17 03:40:13 -04:00
|
|
|
For nested classes defined inside a function, any nonlocal cells referenced from
|
|
|
|
the class scope are *not* included in the ``locals()`` mapping.
|
|
|
|
|
2017-10-22 02:12:25 -04:00
|
|
|
To capture this expectation as part of the language specification, the following
|
|
|
|
two paragraphs will be added to the documentation for ``locals()``:
|
|
|
|
|
|
|
|
When using ``exec()`` or ``eval()`` with separate local and global
|
|
|
|
namespaces, [this function] returns the given local namespace.
|
|
|
|
|
|
|
|
At class scope, it returns the namespace that will be passed to the metaclass
|
|
|
|
constructor.
|
|
|
|
|
2017-09-08 17:55:50 -04:00
|
|
|
This part of the proposal does not require any changes to the reference
|
|
|
|
implementation - it is standardisation of the current behaviour.
|
|
|
|
|
|
|
|
|
|
|
|
Function scope
|
|
|
|
--------------
|
|
|
|
|
|
|
|
At function scope, interpreter implementations are granted significant freedom
|
|
|
|
to optimise local variable access, and hence are NOT required to permit
|
|
|
|
arbitrary modification of local and nonlocal variable bindings through the
|
|
|
|
mapping returned from ``locals()``.
|
|
|
|
|
2017-10-22 02:12:25 -04:00
|
|
|
Historically, this leniency has been described in the language specification
|
|
|
|
with the words "The contents of this dictionary should not be modified; changes
|
|
|
|
may not affect the values of local and free variables used by the interpreter."
|
|
|
|
|
|
|
|
This PEP proposes to change that text to instead say:
|
|
|
|
|
2019-12-30 01:22:46 -05:00
|
|
|
At function scope (including for generators and coroutines), each call to
|
2020-02-16 07:10:19 -05:00
|
|
|
``locals()`` instead returns a fresh dictionary containing the current
|
|
|
|
bindings of the function's local variables and any nonlocal cell references.
|
|
|
|
In this case, name binding changes made via the returned dict are *not*
|
|
|
|
written back to the corresponding local variables or nonlocal cell
|
|
|
|
references, and binding, rebinding, or deleting local variables and nonlocal
|
|
|
|
cell references does *not* affect the contents of previously returned
|
|
|
|
dictionaries.
|
2017-10-22 02:12:25 -04:00
|
|
|
|
|
|
|
This part of the proposal *does* require changes to the CPython reference
|
2019-12-30 01:22:46 -05:00
|
|
|
implementation, as CPython currently returns a shared mapping object that may
|
|
|
|
be implicitly refreshed by additional calls to ``locals()``, and the
|
|
|
|
"write back" strategy currently used to support namespace changes
|
|
|
|
from trace functions also doesn't comply with it (and causes the quirky
|
2021-08-26 05:26:13 -04:00
|
|
|
behavioural problems mentioned in the Motivation above).
|
2017-10-22 02:12:25 -04:00
|
|
|
|
|
|
|
|
|
|
|
CPython Implementation Changes
|
|
|
|
==============================
|
|
|
|
|
2021-06-27 03:37:01 -04:00
|
|
|
Summary of proposed implementation-specific changes
|
|
|
|
---------------------------------------------------
|
|
|
|
|
2021-07-21 08:28:21 -04:00
|
|
|
* Changes are made as necessary to provide the updated Python level semantics
|
2021-06-27 03:37:01 -04:00
|
|
|
* Two new functions are added to the stable ABI to replicate the updated
|
2022-08-17 01:26:55 -04:00
|
|
|
behaviour of the Python ``locals()`` builtin:
|
|
|
|
|
|
|
|
.. code-block:: c
|
2021-06-27 03:37:01 -04:00
|
|
|
|
|
|
|
PyObject * PyLocals_Get();
|
2021-07-21 08:28:21 -04:00
|
|
|
PyLocals_Kind PyLocals_GetKind();
|
2022-08-17 01:26:55 -04:00
|
|
|
|
2021-06-27 03:37:01 -04:00
|
|
|
* One new function is added to the stable ABI to efficiently get a snapshot of
|
2022-08-17 01:26:55 -04:00
|
|
|
the local namespace in the running frame:
|
|
|
|
|
|
|
|
.. code-block:: c
|
2021-06-27 03:37:01 -04:00
|
|
|
|
|
|
|
PyObject * PyLocals_GetCopy();
|
2022-08-17 01:26:55 -04:00
|
|
|
|
2021-06-27 03:37:01 -04:00
|
|
|
* Corresponding frame accessor functions for these new public APIs are added to
|
|
|
|
the CPython frame C API
|
2021-08-26 05:26:13 -04:00
|
|
|
* On optimised frames, the Python level ``f_locals`` API will return dynamically
|
|
|
|
created read/write proxy objects that directly access the frame's local and
|
|
|
|
closure variable storage. To provide interoperability with the existing
|
|
|
|
``PyEval_GetLocals()`` API, the proxy objects will continue to use the C level
|
|
|
|
frame locals data storage field to hold a value cache that also allows for
|
|
|
|
storage of arbitrary additional keys. Additional details on the expected
|
|
|
|
behaviour of these fast locals proxy objects are covered below.
|
2021-06-27 03:37:01 -04:00
|
|
|
* No C API function is added to get access to a mutable mapping for the local
|
|
|
|
namespace. Instead, ``PyObject_GetAttrString(frame, "f_locals")`` is used, the
|
|
|
|
same API as is used in Python code.
|
|
|
|
* ``PyEval_GetLocals()`` remains supported and does not emit a programmatic
|
|
|
|
warning, but will be deprecated in the documentation in favour of the new
|
2021-08-26 05:26:13 -04:00
|
|
|
APIs that don't rely on returning a borrowed reference
|
2021-06-27 03:37:01 -04:00
|
|
|
* ``PyFrame_FastToLocals()`` and ``PyFrame_FastToLocalsWithError()`` remain
|
|
|
|
supported and do not emit a programmatic warning, but will be deprecated in
|
2021-08-26 05:26:13 -04:00
|
|
|
the documentation in favour of the new APIs that don't require direct access
|
|
|
|
to the internal data storage layout of frame objects
|
2021-06-27 03:37:01 -04:00
|
|
|
* ``PyFrame_LocalsToFast()`` always raises ``RuntimeError()``, indicating that
|
|
|
|
``PyObject_GetAttrString(frame, "f_locals")`` should be used to obtain a
|
|
|
|
mutable read/write mapping for the local variables.
|
|
|
|
* The trace hook implementation will no longer call ``PyFrame_FastToLocals()``
|
|
|
|
implicitly. The version porting guide will recommend migrating to
|
2021-12-22 18:25:51 -05:00
|
|
|
``PyFrame_GetLocals()`` for read-only access and
|
2021-06-27 03:37:01 -04:00
|
|
|
``PyObject_GetAttrString(frame, "f_locals")`` for read/write access.
|
|
|
|
|
|
|
|
|
|
|
|
Providing the updated Python level semantics
|
|
|
|
--------------------------------------------
|
|
|
|
|
|
|
|
The implementation of the ``locals()`` builtin is modified to return a distinct
|
2021-08-26 05:26:13 -04:00
|
|
|
copy of the local namespace for optimised frames, rather than a direct reference
|
|
|
|
to the internal frame value cache updated by the ``PyFrame_FastToLocals()`` C
|
|
|
|
API and returned by the ``PyEval_GetLocals()`` C API.
|
2021-06-27 03:37:01 -04:00
|
|
|
|
|
|
|
|
2019-12-30 01:22:46 -05:00
|
|
|
Resolving the issues with tracing mode behaviour
|
|
|
|
------------------------------------------------
|
|
|
|
|
2017-10-22 02:12:25 -04:00
|
|
|
The current cause of CPython's tracing mode quirks (both the side effects from
|
|
|
|
simply installing a tracing function and the fact that writing values back to
|
|
|
|
function locals only works for the specific function being traced) is the way
|
|
|
|
that locals mutation support for trace hooks is currently implemented: the
|
2017-10-22 02:22:13 -04:00
|
|
|
``PyFrame_LocalsToFast`` function.
|
2017-10-22 02:12:25 -04:00
|
|
|
|
|
|
|
When a trace function is installed, CPython currently does the following for
|
|
|
|
function frames (those where the code object uses "fast locals" semantics):
|
|
|
|
|
2021-08-26 05:26:13 -04:00
|
|
|
1. Calls ``PyFrame_FastToLocals`` to update the frame value cache
|
2017-10-22 02:12:25 -04:00
|
|
|
2. Calls the trace hook (with tracing of the hook itself disabled)
|
2021-08-26 05:26:13 -04:00
|
|
|
3. Calls ``PyFrame_LocalsToFast`` to capture any changes made to the frame
|
|
|
|
value cache
|
2017-10-22 02:12:25 -04:00
|
|
|
|
|
|
|
This approach is problematic for a few different reasons:
|
|
|
|
|
2021-08-26 05:26:13 -04:00
|
|
|
* Even if the trace function doesn't mutate the value cache, the final step
|
|
|
|
resets any cell references back to the state they were in before the trace
|
|
|
|
function was called (this is the root cause of the bug report in [1]_)
|
|
|
|
* If the trace function *does* mutate the value cache, but then does something
|
|
|
|
that causes the value cache to be refreshed from the frame, those changes are
|
|
|
|
lost (this is one aspect of the bug report in [3]_)
|
2017-10-22 02:12:25 -04:00
|
|
|
* If the trace function attempts to mutate the local variables of a frame other
|
|
|
|
than the one being traced (e.g. ``frame.f_back.f_locals``), those changes
|
|
|
|
will almost certainly be lost (this is another aspect of the bug report in
|
|
|
|
[3]_)
|
2021-08-26 05:26:13 -04:00
|
|
|
* If a reference to the frame value cache (e.g. retrieved via ``locals()``) is
|
|
|
|
passed to another function, and *that* function mutates the value cache, then
|
|
|
|
those changes *may* be written back to the execution frame *if* a trace hook
|
|
|
|
is installed
|
2017-10-22 02:12:25 -04:00
|
|
|
|
|
|
|
The proposed resolution to this problem is to take advantage of the fact that
|
|
|
|
whereas functions typically access their *own* namespace using the language
|
|
|
|
defined ``locals()`` builtin, trace functions necessarily use the implementation
|
|
|
|
dependent ``frame.f_locals`` interface, as a frame reference is what gets
|
|
|
|
passed to hook implementations.
|
|
|
|
|
2021-08-26 05:26:13 -04:00
|
|
|
Instead of being a direct reference to the internal frame value cache historically
|
|
|
|
returned by the ``locals()`` builtin, the Python level ``frame.f_locals`` will be
|
|
|
|
updated to instead return instances of a dedicated fast locals proxy type that
|
|
|
|
writes and reads values directly to and from the fast locals array on the
|
|
|
|
underlying frame. Each access of the attribute produces a new instance of the
|
|
|
|
proxy (so creating proxy instances is intentionally a cheap operation).
|
|
|
|
|
|
|
|
Despite the new proxy type becoming the preferred way to access local variables
|
|
|
|
on optimised frames, the internal value cache stored on the frame is still
|
|
|
|
retained for two key purposes:
|
|
|
|
|
|
|
|
* maintaining backwards compatibility for and interoperability with the
|
|
|
|
``PyEval_GetLocals()`` C API
|
|
|
|
* providing storage space for additional keys that don't have slots in the
|
|
|
|
fast locals array (e.g. the ``__return__`` and ``__exception__`` keys set by
|
|
|
|
``pdb`` when tracing code execution for debugging purposes)
|
|
|
|
|
2021-12-22 18:25:51 -05:00
|
|
|
|
2021-08-26 05:26:13 -04:00
|
|
|
With the changes in this PEP, this internal frame value cache is no longer
|
|
|
|
directly accessible from Python code (whereas historically it was both
|
|
|
|
returned by the ``locals()`` builtin and available as the ``frame.f_locals``
|
|
|
|
attribute). Instead, the value cache is only accessible via the
|
|
|
|
``PyEval_GetLocals()`` C API and by directly accessing the internal storage of
|
|
|
|
a frame object.
|
|
|
|
|
|
|
|
Fast locals proxy objects and the internal frame value cache returned by
|
|
|
|
``PyEval_GetLocals()`` offer the following behavioural guarantees:
|
|
|
|
|
|
|
|
* changes made via a fast locals proxy will be immediately visible to the frame
|
|
|
|
itself, to other fast locals proxy objects for the same frame, and in the
|
|
|
|
internal value cache stored on the frame (it is this last point that provides
|
|
|
|
``PyEval_GetLocals()`` interoperability)
|
|
|
|
* changes made directly to the internal frame value cache will never be visible
|
|
|
|
to the frame itself, and will only be reliably visible via fast locals proxies
|
|
|
|
for the same frame if the change relates to extra variables that don't have
|
|
|
|
slots in the frame's fast locals array
|
2021-12-22 18:25:51 -05:00
|
|
|
* changes made by executing code in the frame will be immediately visible to all
|
|
|
|
fast locals proxy objects for that frame (both existing proxies and newly
|
|
|
|
created ones). Visibility in the internal frame value cache cache returned
|
|
|
|
by ``PyEval_GetLocals()`` is subject to the cache update guidelines discussed
|
|
|
|
in the next section
|
|
|
|
|
|
|
|
As a result of these points, only code using ``PyEval_GetLocals()``,
|
|
|
|
``PyLocals_Get()``, or ``PyLocals_GetCopy()`` will need to be concerned about
|
|
|
|
the frame value cache potentially becoming stale. Code using the new frame fast
|
|
|
|
locals proxy API (whether from Python or from C) will always see the live state
|
|
|
|
of the frame.
|
2021-08-26 05:26:13 -04:00
|
|
|
|
|
|
|
|
|
|
|
Fast locals proxy implementation details
|
|
|
|
----------------------------------------
|
|
|
|
|
2021-12-22 18:25:51 -05:00
|
|
|
Each fast locals proxy instance has a single internal attribute that is not
|
2021-08-26 05:26:13 -04:00
|
|
|
exposed as part of the Python runtime API:
|
|
|
|
|
|
|
|
* *frame*: the underlying optimised frame that the proxy provides access to
|
|
|
|
|
|
|
|
In addition, proxy instances use and update the following attributes stored on the
|
2021-12-22 18:25:51 -05:00
|
|
|
underlying frame or code object:
|
|
|
|
|
|
|
|
* *_name_to_offset_mapping*: a hidden mapping from variable names to fast local
|
|
|
|
storage offsets. This mapping is lazily initialized on the first frame read or
|
|
|
|
write access through a fast locals proxy, rather than being eagerly populated
|
|
|
|
as soon as the first fast locals proxy is created. Since the mapping is
|
|
|
|
identical for all frames running a given code object, a single copy is stored
|
2022-01-21 06:03:51 -05:00
|
|
|
on the code object, rather than each frame object populating its own mapping
|
2021-08-26 05:26:13 -04:00
|
|
|
* *locals*: the internal frame value cache returned by the ``PyEval_GetLocals()``
|
|
|
|
C API and updated by the ``PyFrame_FastToLocals()`` C API. This is the mapping
|
|
|
|
that the ``locals()`` builtin returns in Python 3.10 and earlier.
|
2020-01-01 11:30:39 -05:00
|
|
|
|
2021-12-22 18:25:51 -05:00
|
|
|
``__getitem__`` operations on the proxy will populate the ``_name_to_offset_mapping``
|
|
|
|
on the code object (if it is not already populated), and then either return the
|
|
|
|
relevant value (if the key is found in either the ``_name_to_offset_mapping``
|
|
|
|
mapping or the internal frame value cache), or else raise ``KeyError``. Variables
|
|
|
|
that are defined on the frame but not currently bound also raise ``KeyError``
|
|
|
|
(just as they're omitted from the result of ``locals()``).
|
2019-12-30 01:22:46 -05:00
|
|
|
|
2021-06-27 03:37:01 -04:00
|
|
|
As the frame storage is always accessed directly, the proxy will automatically
|
2021-08-26 05:26:13 -04:00
|
|
|
pick up name binding and unbinding operations that take place as the function
|
|
|
|
executes. The internal value cache is implicitly updated when individual
|
|
|
|
variables are read from the frame state (including for containment checks,
|
|
|
|
which need to check if the name is currently bound or unbound).
|
2019-12-30 01:22:46 -05:00
|
|
|
|
2021-06-27 03:37:01 -04:00
|
|
|
Similarly, ``__setitem__`` and ``__delitem__`` operations on the proxy will
|
|
|
|
directly affect the corresponding fast local or cell reference on the underlying
|
|
|
|
frame, ensuring that changes are immediately visible to the running Python code,
|
|
|
|
rather than needing to be written back to the runtime storage at some later time.
|
2021-08-26 05:26:13 -04:00
|
|
|
Such changes are also immediately written to the internal frame value cache to
|
2021-12-22 18:25:51 -05:00
|
|
|
make them visible to users of the ``PyEval_GetLocals()`` C API.
|
2017-10-22 02:12:25 -04:00
|
|
|
|
2021-07-10 10:05:15 -04:00
|
|
|
Keys that are not defined as local or closure variables on the underlying frame
|
2021-08-26 05:26:13 -04:00
|
|
|
are still written to the internal value cache on optimised frames. This allows
|
2021-07-17 07:16:51 -04:00
|
|
|
utilities like ``pdb`` (which writes ``__return__`` and ``__exception__``
|
2021-08-26 05:26:13 -04:00
|
|
|
values into the frame's ``f_locals`` mapping) to continue working as they always
|
2021-07-17 07:16:51 -04:00
|
|
|
have. These additional keys that do not correspond to a local or closure
|
|
|
|
variable on the frame will be left alone by future cache sync operations.
|
2021-12-22 18:25:51 -05:00
|
|
|
Using the frame value cache to store these extra keys (rather than defining a
|
|
|
|
new mapping that holds only the extra keys) provides full interoperability
|
|
|
|
with the existing ``PyEval_GetLocals()`` API (since users of either API will
|
|
|
|
see extra keys added by users of either API, rather than users of the new fast
|
|
|
|
locals proxy API only seeing keys added via that API).
|
2019-12-30 01:22:46 -05:00
|
|
|
|
2021-07-17 07:16:51 -04:00
|
|
|
An additional benefit of storing only the variable value cache on the frame
|
|
|
|
(rather than storing an instance of the proxy type), is that it avoids
|
|
|
|
creating a reference cycle from the frame back to itself, so the frame will
|
|
|
|
only be kept alive if another object retains a reference to a proxy instance.
|
2019-12-30 01:22:46 -05:00
|
|
|
|
2021-08-26 05:26:13 -04:00
|
|
|
Note: calling the ``proxy.clear()`` method has a similarly broad impact as
|
|
|
|
calling ``PyFrame_LocalsToFast()`` on an empty frame value cache in earlier
|
|
|
|
versions. Not only will the frame local variables be cleared, but also any cell
|
|
|
|
variables accessible from the frame (whether those cells are owned by the
|
|
|
|
frame itself or by an outer frame). This *can* clear a class's ``__class__``
|
|
|
|
cell if called on the frame of a method that uses the zero-arg ``super()``
|
|
|
|
construct (or otherwise references ``__class__``). This exceeds the scope of
|
|
|
|
calling ``frame.clear()``, as that only drop's the frame's references to cell
|
|
|
|
variables, it doesn't clear the cells themselves. This PEP could be a potential
|
|
|
|
opportunity to narrow the scope of attempts to clear the frame variables
|
|
|
|
directly by leaving cells belonging to outer frames alone, and only clearing
|
|
|
|
local variables and cells belonging directly to the frame underlying the proxy
|
2022-01-21 06:03:51 -05:00
|
|
|
(this issue affects :pep:`667` as well, as the question relates to the handling of
|
2021-08-26 05:26:13 -04:00
|
|
|
cell variables, and is entirely independent of the internal frame value cache).
|
|
|
|
|
2019-12-30 01:22:46 -05:00
|
|
|
|
2020-02-16 07:10:19 -05:00
|
|
|
Changes to the stable C API/ABI
|
|
|
|
-------------------------------
|
|
|
|
|
|
|
|
Unlike Python code, extension module functions that call in to the Python C API
|
|
|
|
can be called from any kind of Python scope. This means it isn't obvious from
|
|
|
|
the context whether ``locals()`` will return a snapshot or not, as it depends
|
|
|
|
on the scope of the calling Python code, not the C code itself.
|
|
|
|
|
|
|
|
This means it is desirable to offer C APIs that give predictable, scope
|
|
|
|
independent, behaviour. However, it is also desirable to allow C code to
|
|
|
|
exactly mimic the behaviour of Python code at the same scope.
|
|
|
|
|
2021-02-03 09:06:23 -05:00
|
|
|
To enable mimicking the behaviour of Python code, the stable C ABI would gain
|
2022-08-17 01:26:55 -04:00
|
|
|
the following new functions:
|
|
|
|
|
|
|
|
.. code-block:: c
|
2020-02-16 07:10:19 -05:00
|
|
|
|
|
|
|
PyObject * PyLocals_Get();
|
2021-07-21 08:28:21 -04:00
|
|
|
PyLocals_Kind PyLocals_GetKind();
|
2020-02-16 07:10:19 -05:00
|
|
|
|
|
|
|
``PyLocals_Get()`` is directly equivalent to the Python ``locals()`` builtin.
|
|
|
|
It returns a new reference to the local namespace mapping for the active
|
|
|
|
Python frame at module and class scope, and when using ``exec()`` or ``eval()``.
|
|
|
|
It returns a shallow copy of the active namespace at
|
|
|
|
function/coroutine/generator scope.
|
|
|
|
|
2021-07-21 08:28:21 -04:00
|
|
|
``PyLocals_GetKind()`` returns a value from the newly defined ``PyLocals_Kind``
|
|
|
|
enum, with the following options being available:
|
|
|
|
|
|
|
|
* ``PyLocals_DIRECT_REFERENCE``: ``PyLocals_Get()`` returns a direct reference
|
|
|
|
to the local namespace for the running frame.
|
|
|
|
* ``PyLocals_SHALLOW_COPY``: ``PyLocals_Get()`` returns a shallow copy of the
|
|
|
|
local namespace for the running frame.
|
|
|
|
* ``PyLocals_UNDEFINED``: an error occurred (e.g. no active Python thread
|
|
|
|
state). A Python exception will be set if this value is returned.
|
|
|
|
|
2021-08-26 05:26:13 -04:00
|
|
|
Since the enum is used in the stable ABI, an additional 31-bit value is set to
|
|
|
|
ensure that it is safe to cast arbitrary signed 32-bit signed integers to
|
|
|
|
``PyLocals_Kind`` values.
|
|
|
|
|
2021-07-21 08:28:21 -04:00
|
|
|
This query API allows extension module code to determine the potential impact
|
|
|
|
of mutating the mapping returned by ``PyLocals_Get()`` without needing access
|
2021-12-22 18:25:51 -05:00
|
|
|
to the details of the running frame object. Python code gets equivalent
|
|
|
|
information visually through lexical scoping (as covered in the new ``locals()``
|
2022-01-04 00:01:23 -05:00
|
|
|
builtin documentation).
|
2020-02-16 07:10:19 -05:00
|
|
|
|
|
|
|
To allow extension module code to behave consistently regardless of the active
|
2022-08-17 01:26:55 -04:00
|
|
|
Python scope, the stable C ABI would gain the following new function:
|
|
|
|
|
|
|
|
.. code-block:: c
|
2020-02-16 07:10:19 -05:00
|
|
|
|
|
|
|
PyObject * PyLocals_GetCopy();
|
|
|
|
|
|
|
|
``PyLocals_GetCopy()`` returns a new dict instance populated from the current
|
|
|
|
locals namespace. Roughly equivalent to ``dict(locals())`` in Python code, but
|
|
|
|
avoids the double-copy in the case where ``locals()`` already returns a shallow
|
2021-12-22 18:25:51 -05:00
|
|
|
copy. Akin to the following code, but doesn't assume there will only ever be
|
2022-08-17 01:26:55 -04:00
|
|
|
two kinds of locals result:
|
|
|
|
|
|
|
|
.. code-block:: c
|
2020-02-16 07:10:19 -05:00
|
|
|
|
2021-12-22 18:25:51 -05:00
|
|
|
locals = PyLocals_Get();
|
|
|
|
if (PyLocals_GetKind() == PyLocals_DIRECT_REFERENCE) {
|
|
|
|
locals = PyDict_Copy(locals);
|
|
|
|
}
|
2020-02-16 07:10:19 -05:00
|
|
|
|
|
|
|
The existing ``PyEval_GetLocals()`` API will retain its existing behaviour in
|
|
|
|
CPython (mutable locals at class and module scope, shared dynamic snapshot
|
|
|
|
otherwise). However, its documentation will be updated to note that the
|
|
|
|
conditions under which the shared dynamic snapshot get updated have changed.
|
|
|
|
|
|
|
|
The ``PyEval_GetLocals()`` documentation will also be updated to recommend
|
|
|
|
replacing usage of this API with whichever of the new APIs is most appropriate
|
|
|
|
for the use case:
|
|
|
|
|
2021-12-22 18:25:51 -05:00
|
|
|
* Use ``PyLocals_Get()`` (optionally combined with ``PyDictProxy_New()``) for
|
|
|
|
read-only access to the current locals namespace. This form of usage will
|
|
|
|
need to be aware that the copy may go stale in optimised frames.
|
2020-02-16 07:10:19 -05:00
|
|
|
* Use ``PyLocals_GetCopy()`` for a regular mutable dict that contains a copy of
|
|
|
|
the current locals namespace, but has no ongoing connection to the active
|
|
|
|
frame.
|
2021-06-27 03:37:01 -04:00
|
|
|
* Use ``PyLocals_Get()`` to exactly match the semantics of the Python level
|
|
|
|
``locals()`` builtin.
|
2021-07-21 08:28:21 -04:00
|
|
|
* Query ``PyLocals_GetKind()`` explicitly to implement custom handling
|
2020-02-16 07:10:19 -05:00
|
|
|
(e.g. raising a meaningful exception) for scopes where ``PyLocals_Get()``
|
|
|
|
would return a shallow copy rather than granting read/write access to the
|
|
|
|
locals namespace.
|
2021-06-27 03:37:01 -04:00
|
|
|
* Use implementation specific APIs (e.g. ``PyObject_GetAttrString(frame, "f_locals")``)
|
2021-07-21 08:28:21 -04:00
|
|
|
if read/write access to the frame is required and ``PyLocals_GetKind()``
|
|
|
|
returns something other than ``PyLocals_DIRECT_REFERENCE``.
|
2020-02-16 07:10:19 -05:00
|
|
|
|
|
|
|
|
2019-12-30 01:22:46 -05:00
|
|
|
Changes to the public CPython C API
|
|
|
|
-----------------------------------
|
2017-10-22 02:12:25 -04:00
|
|
|
|
2019-12-30 01:22:46 -05:00
|
|
|
The existing ``PyEval_GetLocals()`` API returns a borrowed reference, which
|
2020-02-16 07:10:19 -05:00
|
|
|
means it cannot be updated to return the new shallow copies at function
|
2021-06-27 03:37:01 -04:00
|
|
|
scope. Instead, it will continue to return a borrowed reference to an internal
|
|
|
|
dynamic snapshot stored on the frame object. This shared mapping will behave
|
|
|
|
similarly to the existing shared mapping in Python 3.10 and earlier, but the exact
|
|
|
|
conditions under which it gets refreshed will be different. Specifically, it
|
|
|
|
will be updated only in the following circumstance:
|
|
|
|
|
|
|
|
* any call to ``PyEval_GetLocals()``, ``PyLocals_Get()``, ``PyLocals_GetCopy()``,
|
|
|
|
or the Python ``locals()`` builtin while the frame is running
|
2020-02-16 07:10:19 -05:00
|
|
|
* any call to ``PyFrame_GetLocals()``, ``PyFrame_GetLocalsCopy()``,
|
2021-06-27 03:37:01 -04:00
|
|
|
``_PyFrame_BorrowLocals()``, ``PyFrame_FastToLocals()``, or
|
|
|
|
``PyFrame_FastToLocalsWithError()`` for the frame
|
2021-12-22 18:25:51 -05:00
|
|
|
* any operation on a fast locals proxy object that updates the shared
|
|
|
|
mapping as part of its implementation. In the initial reference
|
2021-07-17 07:16:51 -04:00
|
|
|
implementation, those operations are those that are intrinsically ``O(n)``
|
2021-12-22 18:25:51 -05:00
|
|
|
operations (``len(flp)``, mapping comparison, ``flp.copy()`` and rendering as
|
|
|
|
a string), as well as those that refresh the cache entries for individual keys.
|
2021-06-27 03:37:01 -04:00
|
|
|
|
2021-12-22 22:04:55 -05:00
|
|
|
Requesting a fast locals proxy will *not* implicitly update the shared dynamic
|
2021-06-27 03:37:01 -04:00
|
|
|
snapshot, and the CPython trace hook handling will no longer implicitly update
|
|
|
|
it either.
|
2020-02-16 07:10:19 -05:00
|
|
|
|
2021-06-27 03:37:01 -04:00
|
|
|
(Note: even though ``PyEval_GetLocals()`` is part of the stable C API/ABI, the
|
2020-02-16 07:10:19 -05:00
|
|
|
specifics of when the namespace it returns gets refreshed are still an
|
|
|
|
interpreter implementation detail)
|
|
|
|
|
|
|
|
The additions to the public CPython C API are the frame level enhancements
|
2022-08-17 01:26:55 -04:00
|
|
|
needed to support the stable C API/ABI updates:
|
|
|
|
|
|
|
|
.. code-block:: c
|
2020-02-16 07:10:19 -05:00
|
|
|
|
2021-07-21 08:28:21 -04:00
|
|
|
PyLocals_Kind PyFrame_GetLocalsKind(frame);
|
2020-02-16 07:10:19 -05:00
|
|
|
PyObject * PyFrame_GetLocals(frame);
|
|
|
|
PyObject * PyFrame_GetLocalsCopy(frame);
|
|
|
|
PyObject * _PyFrame_BorrowLocals(frame);
|
|
|
|
|
|
|
|
|
2021-07-21 08:28:21 -04:00
|
|
|
``PyFrame_GetLocalsKind(frame)`` is the underlying API for
|
|
|
|
``PyLocals_GetKind()``.
|
|
|
|
|
|
|
|
``PyFrame_GetLocals(frame)`` is the underlying API for ``PyLocals_Get()``.
|
2020-02-16 07:10:19 -05:00
|
|
|
|
|
|
|
``PyFrame_GetLocalsCopy(frame)`` is the underlying API for
|
|
|
|
``PyLocals_GetCopy()``.
|
|
|
|
|
|
|
|
``_PyFrame_BorrowLocals(frame)`` is the underlying API for
|
|
|
|
``PyEval_GetLocals()``. The underscore prefix is intended to discourage use and
|
|
|
|
to indicate that code using it is unlikely to be portable across
|
2021-06-27 03:37:01 -04:00
|
|
|
implementations. However, it is documented and visible to the linker in order
|
|
|
|
to avoid having to access the internals of the frame struct from the
|
|
|
|
``PyEval_GetLocals()`` implementation.
|
2017-10-22 02:12:25 -04:00
|
|
|
|
|
|
|
The ``PyFrame_LocalsToFast()`` function will be changed to always emit
|
|
|
|
``RuntimeError``, explaining that it is no longer a supported operation, and
|
2021-07-10 10:05:15 -04:00
|
|
|
affected code should be updated to use
|
|
|
|
``PyObject_GetAttrString(frame, "f_locals")`` to obtain a read/write proxy
|
|
|
|
instead.
|
2019-12-30 01:22:46 -05:00
|
|
|
|
2020-02-16 07:10:19 -05:00
|
|
|
In addition to the above documented interfaces, the draft reference
|
2022-08-17 01:26:55 -04:00
|
|
|
implementation also exposes the following undocumented interfaces:
|
|
|
|
|
|
|
|
.. code-block:: c
|
2019-12-30 01:22:46 -05:00
|
|
|
|
2020-02-16 07:10:19 -05:00
|
|
|
PyTypeObject _PyFastLocalsProxy_Type;
|
2021-08-26 05:26:13 -04:00
|
|
|
#define _PyFastLocalsProxy_CheckExact(self) Py_IS_TYPE(op, &_PyFastLocalsProxy_Type)
|
2019-12-30 01:22:46 -05:00
|
|
|
|
2021-06-27 03:37:01 -04:00
|
|
|
This type is what the reference implementation actually returns from
|
|
|
|
``PyObject_GetAttrString(frame, "f_locals")`` for optimized frames (i.e.
|
2021-07-21 08:28:21 -04:00
|
|
|
when ``PyFrame_GetLocalsKind()`` returns ``PyLocals_SHALLOW_COPY``).
|
2021-06-27 03:37:01 -04:00
|
|
|
|
|
|
|
|
|
|
|
Reducing the runtime overhead of trace hooks
|
|
|
|
--------------------------------------------
|
|
|
|
|
|
|
|
As noted in [9]_, the implicit call to ``PyFrame_FastToLocals()`` in the
|
|
|
|
Python trace hook support isn't free, and could be rendered unnecessary if
|
|
|
|
the frame proxy read values directly from the frame instead of getting them
|
|
|
|
from the mapping.
|
|
|
|
|
|
|
|
As the new frame locals proxy type doesn't require separate data refresh steps,
|
2021-07-21 08:28:21 -04:00
|
|
|
this PEP incorporates Victor Stinner's proposal to no longer implicitly call
|
2021-06-27 03:37:01 -04:00
|
|
|
``PyFrame_FastToLocalsWithError()`` before calling trace hooks implemented in
|
|
|
|
Python.
|
|
|
|
|
2021-12-22 22:04:55 -05:00
|
|
|
Code using the new fast locals proxy objects will have the dynamic locals snapshot
|
2021-07-10 10:05:15 -04:00
|
|
|
implicitly refreshed when accessing methods that need it, while code using the
|
|
|
|
``PyEval_GetLocals()`` API will implicitly refresh it when making that call.
|
2021-06-27 03:37:01 -04:00
|
|
|
|
|
|
|
The PEP necessarily also drops the implicit call to ``PyFrame_LocalsToFast()``
|
|
|
|
when returning from a trace hook, as that API now always raises an exception.
|
2017-09-08 17:55:50 -04:00
|
|
|
|
|
|
|
|
2021-08-26 05:26:13 -04:00
|
|
|
Rationale and Design Discussion
|
|
|
|
===============================
|
2017-09-08 17:55:50 -04:00
|
|
|
|
2019-12-30 01:22:46 -05:00
|
|
|
Changing ``locals()`` to return independent snapshots at function scope
|
|
|
|
-----------------------------------------------------------------------
|
2017-09-08 17:55:50 -04:00
|
|
|
|
|
|
|
The ``locals()`` builtin is a required part of the language, and in the
|
|
|
|
reference implementation it has historically returned a mutable mapping with
|
|
|
|
the following characteristics:
|
|
|
|
|
2019-12-30 01:22:46 -05:00
|
|
|
* each call to ``locals()`` returns the *same* mapping object
|
2017-10-22 02:12:25 -04:00
|
|
|
* for namespaces where ``locals()`` returns a reference to something other than
|
|
|
|
the actual local execution namespace, each call to ``locals()`` updates the
|
2019-12-30 01:22:46 -05:00
|
|
|
mapping object with the current state of the local variables and any referenced
|
2017-10-22 02:12:25 -04:00
|
|
|
nonlocal cells
|
2017-09-08 17:55:50 -04:00
|
|
|
* changes to the returned mapping *usually* aren't written back to the
|
|
|
|
local variable bindings or the nonlocal cell references, but write backs
|
|
|
|
can be triggered by doing one of the following:
|
|
|
|
|
|
|
|
* installing a Python level trace hook (write backs then happen whenever
|
|
|
|
the trace hook is called)
|
|
|
|
* running a function level wildcard import (requires bytecode injection in Py3)
|
|
|
|
* running an ``exec`` statement in the function's scope (Py2 only, since
|
|
|
|
``exec`` became an ordinary builtin in Python 3)
|
|
|
|
|
2019-12-30 01:22:46 -05:00
|
|
|
Originally this PEP proposed to retain the first two of these properties,
|
|
|
|
while changing the third in order to address the outright behaviour bugs that
|
|
|
|
it can cause.
|
2019-05-21 09:43:15 -04:00
|
|
|
|
2019-12-30 01:22:46 -05:00
|
|
|
In [7]_ Nathaniel Smith made a persuasive case that we could make the behaviour
|
|
|
|
of ``locals()`` at function scope substantially less confusing by retaining only
|
|
|
|
the second property and having each call to ``locals()`` at function scope
|
|
|
|
return an *independent* snapshot of the local variables and closure references
|
|
|
|
rather than updating an implicitly shared snapshot.
|
2019-05-21 09:43:15 -04:00
|
|
|
|
2019-12-30 01:22:46 -05:00
|
|
|
As this revised design also made the implementation markedly easier to follow,
|
|
|
|
the PEP was updated to propose this change in behaviour, rather than retaining
|
|
|
|
the historical shared snapshot.
|
2019-05-21 09:43:15 -04:00
|
|
|
|
2019-12-30 01:22:46 -05:00
|
|
|
|
|
|
|
Keeping ``locals()`` as a snapshot at function scope
|
|
|
|
----------------------------------------------------
|
|
|
|
|
|
|
|
As discussed in [7]_, it would theoretically be possible to change the semantics
|
|
|
|
of the ``locals()`` builtin to return the write-through proxy at function scope,
|
|
|
|
rather than switching it to return independent snapshots.
|
2019-05-21 09:43:15 -04:00
|
|
|
|
|
|
|
This PEP doesn't (and won't) propose this as it's a backwards incompatible
|
|
|
|
change in practice, even though code that relies on the current behaviour is
|
|
|
|
technically operating in an undefined area of the language specification.
|
|
|
|
|
|
|
|
Consider the following code snippet::
|
|
|
|
|
|
|
|
def example():
|
|
|
|
x = 1
|
|
|
|
locals()["x"] = 2
|
|
|
|
print(x)
|
|
|
|
|
|
|
|
Even with a trace hook installed, that function will consistently print ``1``
|
|
|
|
on the current reference interpreter implementation::
|
|
|
|
|
|
|
|
>>> example()
|
|
|
|
1
|
|
|
|
>>> import sys
|
|
|
|
>>> def basic_hook(*args):
|
|
|
|
... return basic_hook
|
|
|
|
...
|
|
|
|
>>> sys.settrace(basic_hook)
|
|
|
|
>>> example()
|
|
|
|
1
|
|
|
|
|
|
|
|
Similarly, ``locals()`` can be passed to the ``exec()`` and ``eval()`` builtins
|
2019-12-30 01:22:46 -05:00
|
|
|
at function scope (either explicitly or implicitly) without risking unexpected
|
|
|
|
rebinding of local variables or closure references.
|
2019-05-21 09:43:15 -04:00
|
|
|
|
|
|
|
Provoking the reference interpreter into incorrectly mutating the local variable
|
|
|
|
state requires a more complex setup where a nested function closes over a
|
|
|
|
variable being rebound in the outer function, and due to the use of either
|
|
|
|
threads, generators, or coroutines, it's possible for a trace function to start
|
|
|
|
running for the nested function before the rebinding operation in the outer
|
|
|
|
function, but finish running after the rebinding operation has taken place (in
|
|
|
|
which case the rebinding will be reverted, which is the bug reported in [1]_).
|
|
|
|
|
|
|
|
In addition to preserving the de facto semantics which have been in place since
|
2022-01-21 06:03:51 -05:00
|
|
|
:pep:`227` introduced nested scopes in Python 2.1, the other benefit of restricting
|
2019-05-21 09:43:15 -04:00
|
|
|
the write-through proxy support to the implementation-defined frame object API
|
|
|
|
is that it means that only interpreter implementations which emulate the full
|
|
|
|
frame API need to offer the write-through capability at all, and that
|
|
|
|
JIT-compiled implementations only need to enable it when a frame introspection
|
|
|
|
API is invoked, or a trace hook is installed, not whenever ``locals()`` is
|
|
|
|
accessed at function scope.
|
2017-10-22 02:12:25 -04:00
|
|
|
|
2019-12-30 01:22:46 -05:00
|
|
|
Returning snapshots from ``locals()`` at function scope also means that static
|
|
|
|
analysis for function level code will be more reliable, as only access to the
|
2020-02-16 07:10:19 -05:00
|
|
|
frame machinery will allow rebinding of local and nonlocal variable
|
|
|
|
references in a way that is hidden from static analysis.
|
2019-12-30 01:22:46 -05:00
|
|
|
|
2017-10-22 02:12:25 -04:00
|
|
|
|
2017-09-17 03:40:13 -04:00
|
|
|
What happens with the default args for ``eval()`` and ``exec()``?
|
2017-09-08 17:55:50 -04:00
|
|
|
-----------------------------------------------------------------
|
|
|
|
|
2017-09-17 03:40:13 -04:00
|
|
|
These are formally defined as inheriting ``globals()`` and ``locals()`` from
|
|
|
|
the calling scope by default.
|
2017-09-08 17:55:50 -04:00
|
|
|
|
2020-02-16 07:10:19 -05:00
|
|
|
There isn't any need for the PEP to change these defaults, so it doesn't, and
|
|
|
|
``exec()`` and ``eval()`` will start running in a shallow copy of the local
|
|
|
|
namespace when that is what ``locals()`` returns.
|
2019-12-30 01:22:46 -05:00
|
|
|
|
2020-02-16 07:10:19 -05:00
|
|
|
This behaviour will have potential performance implications, especially
|
2019-12-30 01:22:46 -05:00
|
|
|
for functions with large numbers of local variables (e.g. if these functions
|
2021-07-21 08:28:21 -04:00
|
|
|
are called in a loop, calling ``globals()`` and ``locals()`` once before the
|
2020-02-16 07:10:19 -05:00
|
|
|
loop and then passing the namespace into the function explicitly will give the
|
|
|
|
same semantics and performance characteristics as the status quo, whereas
|
|
|
|
relying on the implicit default would create a new shallow copy of the local
|
|
|
|
namespace on each iteration).
|
2019-12-30 01:22:46 -05:00
|
|
|
|
|
|
|
(Note: the reference implementation draft PR has updated the ``locals()`` and
|
2020-02-16 07:10:19 -05:00
|
|
|
``vars()``, ``eval()``, and ``exec()`` builtins to use ``PyLocals_Get()``. The
|
|
|
|
``dir()`` builtin still uses ``PyEval_GetLocals()``, since it's only using it
|
|
|
|
to make a list from the keys).
|
2019-12-30 01:22:46 -05:00
|
|
|
|
2017-09-08 17:55:50 -04:00
|
|
|
|
2024-08-04 23:54:25 -04:00
|
|
|
.. _pep-558-exec-eval-impact:
|
|
|
|
|
|
|
|
Additional considerations for ``eval()`` and ``exec()`` in optimized scopes
|
|
|
|
---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
Note: while implementing :pep:`667`, it was noted that neither that PEP nor this one
|
|
|
|
clearly explained the impact the ``locals()`` changes would have on code execution APIs
|
|
|
|
like ``exec()`` and ``eval()``. This section was added to this PEP's rationale to better
|
|
|
|
describe the impact and explain the intended benefits of the change.
|
|
|
|
|
|
|
|
When ``exec()`` was converted from a statement to a builtin function
|
|
|
|
in Python 3.0 (part of the core language changes in :pep:`3100`), the
|
|
|
|
associated implicit call to ``PyFrame_LocalsToFast()`` was removed, so
|
|
|
|
it typically appears as if attempts to write to local variables with
|
|
|
|
``exec()`` in optimized frames are ignored::
|
|
|
|
|
|
|
|
>>> def f():
|
|
|
|
... x = 0
|
|
|
|
... exec("x = 1")
|
|
|
|
... print(x)
|
|
|
|
... print(locals()["x"])
|
|
|
|
...
|
|
|
|
>>> f()
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
|
|
|
In truth, the writes aren't being ignored, they just aren't
|
|
|
|
being copied from the dictionary cache back to the optimized local
|
|
|
|
variable array. The changes to the dictionary are then overwritten
|
|
|
|
the next time the dictionary cache is refreshed from the array::
|
|
|
|
|
|
|
|
>>> def f():
|
|
|
|
... x = 0
|
|
|
|
... locals_cache = locals()
|
|
|
|
... exec("x = 1")
|
|
|
|
... print(x)
|
|
|
|
... print(locals_cache["x"])
|
|
|
|
... print(locals()["x"])
|
|
|
|
...
|
|
|
|
>>> f()
|
|
|
|
0
|
|
|
|
1
|
|
|
|
0
|
|
|
|
|
|
|
|
.. _pep-558-ctypes-example:
|
|
|
|
|
|
|
|
The behaviour becomes even stranger if a tracing function
|
|
|
|
or another piece of code invokes ``PyFrame_LocalsToFast()`` before
|
|
|
|
the cache is next refreshed. In those cases the change *is*
|
|
|
|
written back to the optimized local variable array::
|
|
|
|
|
|
|
|
>>> from sys import _getframe
|
|
|
|
>>> from ctypes import pythonapi, py_object, c_int
|
|
|
|
>>> _locals_to_fast = pythonapi.PyFrame_LocalsToFast
|
|
|
|
>>> _locals_to_fast.argtypes = [py_object, c_int]
|
|
|
|
>>> def f():
|
|
|
|
... _frame = _getframe()
|
|
|
|
... _f_locals = _frame.f_locals
|
|
|
|
... x = 0
|
|
|
|
... exec("x = 1")
|
|
|
|
... _locals_to_fast(_frame, 0)
|
|
|
|
... print(x)
|
|
|
|
... print(locals()["x"])
|
|
|
|
... print(_f_locals["x"])
|
|
|
|
...
|
|
|
|
>>> f()
|
|
|
|
1
|
|
|
|
1
|
|
|
|
1
|
|
|
|
|
|
|
|
This situation was more common in Python 3.10 and earlier
|
|
|
|
versions, as merely installing a tracing function was enough
|
|
|
|
to trigger implicit calls to ``PyFrame_LocalsToFast()`` after
|
|
|
|
every line of Python code. However, it can still happen in Python
|
|
|
|
3.11+ depending on exactly which tracing functions are active
|
|
|
|
(e.g. interactive debuggers intentionally do this so that changes
|
|
|
|
made at the debugging prompt are visible when code execution
|
|
|
|
resumes).
|
|
|
|
|
|
|
|
All of the above comments in relation to ``exec()`` apply to
|
|
|
|
*any* attempt to mutate the result of ``locals()`` in optimized
|
|
|
|
scopes, and are the main reason that the ``locals()`` builtin
|
|
|
|
docs contain this caveat:
|
|
|
|
|
|
|
|
Note: The contents of this dictionary should not be modified;
|
|
|
|
changes may not affect the values of local and free variables
|
|
|
|
used by the interpreter.
|
|
|
|
|
|
|
|
While the exact wording in the library reference is not entirely explicit,
|
|
|
|
both ``exec()`` and ``eval()`` have long used the results of calling
|
|
|
|
``globals()`` and ``locals()`` in the calling Python frame as their default
|
|
|
|
execution namespace.
|
|
|
|
|
|
|
|
This was historically also equivalent to using the calling frame's
|
|
|
|
``frame.f_globals`` and ``frame.f_locals`` attributes, but this PEP maps
|
|
|
|
the default namespace arguments for ``exec()`` and ``eval()`` to
|
|
|
|
``globals()`` and ``locals()`` in the calling frame in order to preserve
|
|
|
|
the property of defaulting to ignoring attempted writes to the local
|
|
|
|
namespace in optimized scopes.
|
|
|
|
|
|
|
|
This poses a potential compatibility issue for some code, as with the
|
|
|
|
previous implementation that returns the same dict when ``locals()`` is called
|
|
|
|
multiple times in function scope, the following code usually worked due to
|
|
|
|
the implicitly shared local variable namespace::
|
|
|
|
|
|
|
|
def f():
|
|
|
|
exec('a = 0') # equivalent to exec('a = 0', globals(), locals())
|
|
|
|
exec('print(a)') # equivalent to exec('print(a)', globals(), locals())
|
|
|
|
print(locals()) # {'a': 0}
|
|
|
|
# However, print(a) will not work here
|
|
|
|
f()
|
|
|
|
|
|
|
|
With ``locals()`` in an optimised scope returning the same shared dict for each call,
|
|
|
|
it was possible to store extra "fake locals" in that dict. While these aren't real
|
|
|
|
locals known by the compiler (so they can't be printed with code like ``print(a)``),
|
|
|
|
they can still be accessed via ``locals()`` and shared between multiple ``exec()``
|
|
|
|
calls in the same function scope. Furthermore, because they're *not* real locals,
|
|
|
|
they don't get implicitly updated or removed when the shared cache is refreshed
|
|
|
|
from the local variable storage array.
|
|
|
|
|
|
|
|
When the code in ``exec()`` tries to write to an existing local variable, the
|
|
|
|
runtime behaviour gets harder to predict::
|
|
|
|
|
|
|
|
def f():
|
|
|
|
a = None
|
|
|
|
exec('a = 0') # equivalent to exec('a = 0', globals(), locals())
|
|
|
|
exec('print(a)') # equivalent to exec('print(a)', globals(), locals())
|
|
|
|
print(locals()) # {'a': None}
|
|
|
|
f()
|
|
|
|
|
|
|
|
``print(a)`` will print ``None`` because the implicit ``locals()`` call in
|
|
|
|
``exec()`` refreshes the cached dict with the actual values on the frame.
|
|
|
|
This means that, unlike the "fake" locals created by writing back to ``locals()``
|
|
|
|
(including via previous calls to ``exec()``), the real locals known by the
|
|
|
|
compiler can't easily be modified by ``exec()`` (it can be done, but it requires
|
|
|
|
both retrieving the ``frame.f_locals`` attribute to enable writes back to the frame,
|
|
|
|
and then invoking ``PyFrame_LocalsToFast()``, as :ref:`shown <pep-558-ctypes-example>`
|
|
|
|
using ``ctypes`` above).
|
|
|
|
|
|
|
|
As noted in the :ref:`pep-558-motivation` section, this confusing side effect
|
|
|
|
happens even if the local variable is only defined *after* the ``exec()`` calls::
|
|
|
|
|
|
|
|
>>> def f():
|
|
|
|
... exec("a = 0")
|
|
|
|
... exec("print('a' in locals())") # Printing 'a' directly won't work
|
|
|
|
... print(locals())
|
|
|
|
... a = None
|
|
|
|
... print(locals())
|
|
|
|
...
|
|
|
|
>>> f()
|
|
|
|
False
|
|
|
|
{}
|
|
|
|
{'a': None}
|
|
|
|
|
|
|
|
Because ``a`` is a real local variable that is not currently bound to a value, it
|
|
|
|
gets explicitly removed from the dictionary returned by ``locals()`` whenever
|
|
|
|
``locals()`` is called prior to the ``a = None`` line. This removal is intentional,
|
|
|
|
as it allows the contents of ``locals()`` to be updated correctly in optimized
|
|
|
|
scopes when ``del`` statements are used to delete previously bound local variables.
|
|
|
|
|
|
|
|
As noted in the ``ctypes`` :ref:`example <pep-558-ctypes-example>`, the above behavioural
|
|
|
|
description may be invalidated if the CPython ``PyFrame_LocalsToFast()`` API gets invoked
|
|
|
|
while the frame is still running. In that case, the changes to ``a`` *might* become visible
|
|
|
|
to the running code, depending on exactly when that API is called (and whether the frame
|
|
|
|
has been primed for locals modification by accessing the ``frame.f_locals`` attribute).
|
|
|
|
|
|
|
|
As described above, two options were considered to replace this confusing behaviour:
|
|
|
|
|
|
|
|
* make ``locals()`` return write-through proxy instances (similar
|
|
|
|
to ``frame.f_locals``)
|
|
|
|
* make ``locals()`` return genuinely independent snapshots so that
|
|
|
|
attempts to change the values of local variables via ``exec()``
|
|
|
|
would be *consistently* ignored without any of the caveats
|
|
|
|
noted above.
|
|
|
|
|
|
|
|
The PEP chooses the second option for the following reasons:
|
|
|
|
|
|
|
|
* returning independent snapshots in optimized scopes preserves
|
|
|
|
the Python 3.0 change to ``exec()`` that resulted in attempts
|
|
|
|
to mutate local variables via ``exec()`` being ignored in most
|
|
|
|
cases
|
|
|
|
* the distinction between "``locals()`` gives an instantaneous
|
|
|
|
snapshot of the local variables in optimized scopes, and
|
|
|
|
read/write access in other scopes" and "``frame.f_locals``
|
|
|
|
gives read/write access to the local variables in all scopes,
|
|
|
|
including optimized scopes" allows the intent of a piece of
|
|
|
|
code to be clearer than it would be if both APIs granted
|
|
|
|
full read/write access in optimized scopes, even when write
|
|
|
|
access wasn't needed or desired
|
|
|
|
* in addition to improving clarity for human readers, ensuring
|
|
|
|
that name rebinding in optimized scopes remains lexically
|
|
|
|
visible in the code (as long as the frame introspection APIs
|
|
|
|
are not accessed) allows compilers and interpreters to apply
|
|
|
|
related performance optimizations more consistently
|
|
|
|
* only Python implementations that support the optional frame
|
|
|
|
introspection APIs will need to provide the new write-through
|
|
|
|
proxy support for optimized frames
|
|
|
|
|
|
|
|
With the semantic changes to ``locals()`` in this PEP, it becomes much easier to explain
|
|
|
|
the behavior of ``exec()`` and ``eval()``: in optimized scopes, they will *never* implicitly
|
|
|
|
affect local variables; in other scopes, they will *always* implicitly affect local
|
|
|
|
variables. In optimized scopes, any implicit assignment to the local variables will be
|
|
|
|
discarded when the code execution API returns, since a fresh copy of the local variables
|
|
|
|
is used on each invocation.
|
|
|
|
|
|
|
|
|
|
|
|
Retaining the internal frame value cache
|
|
|
|
----------------------------------------
|
|
|
|
|
|
|
|
Retaining the internal frame value cache results in some visible quirks when
|
|
|
|
frame proxy instances are kept around and re-used after name binding and
|
|
|
|
unbinding operations have been executed on the frame.
|
|
|
|
|
|
|
|
The primary reason for retaining the frame value cache is to maintain backwards
|
|
|
|
compatibility with the ``PyEval_GetLocals()`` API. That API returns a borrowed
|
|
|
|
reference, so it must refer to persistent state stored on the frame object.
|
|
|
|
Storing a fast locals proxy object on the frame creates a problematic reference
|
|
|
|
cycle, so the cleanest option is to instead continue to return a frame value
|
|
|
|
cache, just as this function has done since optimised frames were first
|
|
|
|
introduced.
|
|
|
|
|
|
|
|
With the frame value cache being kept around anyway, it then further made sense
|
|
|
|
to rely on it to simplify the fast locals proxy mapping implementation.
|
|
|
|
|
|
|
|
Note: the fact :pep:`667` *doesn't* use the internal frame value cache as part of the
|
|
|
|
write-through proxy implementation is the key Python level difference between the two PEPs.
|
|
|
|
|
|
|
|
|
2019-05-21 08:41:34 -04:00
|
|
|
Changing the frame API semantics in regular operation
|
|
|
|
-----------------------------------------------------
|
|
|
|
|
2024-08-04 23:54:25 -04:00
|
|
|
Note: when this PEP was first written, it predated the Python 3.11 change to drop the
|
|
|
|
implicit writeback of the frame local variables whenever a tracing function was installed,
|
|
|
|
so making that change was included as part of the proposal.
|
|
|
|
|
2019-05-21 08:41:34 -04:00
|
|
|
Earlier versions of this PEP proposed having the semantics of the frame
|
|
|
|
``f_locals`` attribute depend on whether or not a tracing hook was currently
|
|
|
|
installed - only providing the write-through proxy behaviour when a tracing hook
|
2019-12-30 01:22:46 -05:00
|
|
|
was active, and otherwise behaving the same as the historical ``locals()``
|
|
|
|
builtin.
|
2019-05-21 08:41:34 -04:00
|
|
|
|
|
|
|
That was adopted as the original design proposal for a couple of key reasons,
|
|
|
|
one pragmatic and one more philosophical:
|
|
|
|
|
|
|
|
* Object allocations and method wrappers aren't free, and tracing functions
|
|
|
|
aren't the only operations that access frame locals from outside the function.
|
|
|
|
Restricting the changes to tracing mode meant that the additional memory and
|
2019-12-30 01:22:46 -05:00
|
|
|
execution time overhead of these changes would be as close to zero in regular
|
2019-05-21 08:41:34 -04:00
|
|
|
operation as we can possibly make them.
|
|
|
|
* "Don't change what isn't broken": the current tracing mode problems are caused
|
|
|
|
by a requirement that's specific to tracing mode (support for external
|
|
|
|
rebinding of function local variable references), so it made sense to also
|
|
|
|
restrict any related fixes to tracing mode
|
|
|
|
|
|
|
|
However, actually attempting to implement and document that dynamic approach
|
|
|
|
highlighted the fact that it makes for a really subtle runtime state dependent
|
|
|
|
behaviour distinction in how ``frame.f_locals`` works, and creates several
|
|
|
|
new edge cases around how ``f_locals`` behaves as trace functions are added
|
|
|
|
and removed.
|
|
|
|
|
|
|
|
Accordingly, the design was switched to the current one, where
|
|
|
|
``frame.f_locals`` is always a write-through proxy, and ``locals()`` is always
|
2019-12-30 01:22:46 -05:00
|
|
|
a snapshot, which is both simpler to implement and easier to explain.
|
2019-05-21 08:41:34 -04:00
|
|
|
|
|
|
|
Regardless of how the CPython reference implementation chooses to handle this,
|
|
|
|
optimising compilers and interpreters also remain free to impose additional
|
2019-12-30 01:22:46 -05:00
|
|
|
restrictions on debuggers, such as making local variable mutation through frame
|
2019-05-21 08:41:34 -04:00
|
|
|
objects an opt-in behaviour that may disable some optimisations (just as the
|
|
|
|
emulation of CPython's frame API is already an opt-in flag in some Python
|
|
|
|
implementations).
|
|
|
|
|
|
|
|
|
2021-07-10 10:05:15 -04:00
|
|
|
Continuing to support storing additional data on optimised frames
|
|
|
|
-----------------------------------------------------------------
|
2021-06-27 03:37:01 -04:00
|
|
|
|
2021-07-10 10:05:15 -04:00
|
|
|
One of the draft iterations of this PEP proposed removing the ability to store
|
2021-06-27 03:37:01 -04:00
|
|
|
additional data on optimised frames by writing to ``frame.f_locals`` keys that
|
|
|
|
didn't correspond to local or closure variable names on the underlying frame.
|
|
|
|
|
2021-07-10 10:05:15 -04:00
|
|
|
While this idea offered some attractive simplification of the fast locals proxy
|
|
|
|
implementation, ``pdb`` stores ``__return__`` and ``__exception__`` values on
|
|
|
|
arbitrary frames, so the standard library test suite fails if that functionality
|
|
|
|
no longer works.
|
|
|
|
|
|
|
|
Accordingly, the ability to store arbitrary keys was retained, at the expense
|
2021-12-22 18:25:51 -05:00
|
|
|
of certain operations on proxy objects being slower than could otherwise be
|
|
|
|
(since they can't assume that only names defined on the code object will be
|
|
|
|
accessible through the proxy).
|
2021-07-17 07:16:51 -04:00
|
|
|
|
|
|
|
It is expected that the exact details of the interaction between the fast locals
|
|
|
|
proxy and the ``f_locals`` value cache on the underlying frame will evolve over
|
|
|
|
time as opportunities for improvement are identified.
|
2021-06-27 03:37:01 -04:00
|
|
|
|
|
|
|
|
2017-09-08 17:55:50 -04:00
|
|
|
Historical semantics at function scope
|
|
|
|
--------------------------------------
|
|
|
|
|
|
|
|
The current semantics of mutating ``locals()`` and ``frame.f_locals`` in CPython
|
|
|
|
are rather quirky due to historical implementation details:
|
|
|
|
|
|
|
|
* actual execution uses the fast locals array for local variable bindings and
|
|
|
|
cell references for nonlocal variables
|
|
|
|
* there's a ``PyFrame_FastToLocals`` operation that populates the frame's
|
|
|
|
``f_locals`` attribute based on the current state of the fast locals array
|
|
|
|
and any referenced cells. This exists for three reasons:
|
|
|
|
|
|
|
|
* allowing trace functions to read the state of local variables
|
|
|
|
* allowing traceback processors to read the state of local variables
|
2017-10-22 02:12:25 -04:00
|
|
|
* allowing ``locals()`` to read the state of local variables
|
2017-09-08 17:55:50 -04:00
|
|
|
* a direct reference to ``frame.f_locals`` is returned from ``locals()``, so if
|
|
|
|
you hand out multiple concurrent references, then all those references will be
|
|
|
|
to the exact same dictionary
|
|
|
|
* the two common calls to the reverse operation, ``PyFrame_LocalsToFast``, were
|
|
|
|
removed in the migration to Python 3: ``exec`` is no longer a statement (and
|
2017-09-08 17:57:54 -04:00
|
|
|
hence can no longer affect function local namespaces), and the compiler now
|
2017-09-08 17:55:50 -04:00
|
|
|
disallows the use of ``from module import *`` operations at function scope
|
|
|
|
* however, two obscure calling paths remain: ``PyFrame_LocalsToFast`` is called
|
|
|
|
as part of returning from a trace function (which allows debuggers to make
|
|
|
|
changes to the local variable state), and you can also still inject the
|
|
|
|
``IMPORT_STAR`` opcode when creating a function directly from a code object
|
|
|
|
rather than via the compiler
|
|
|
|
|
|
|
|
This proposal deliberately *doesn't* formalise these semantics as is, since they
|
|
|
|
only make sense in terms of the historical evolution of the language and the
|
|
|
|
reference implementation, rather than being deliberately designed.
|
|
|
|
|
2017-09-17 03:40:13 -04:00
|
|
|
|
2020-02-16 07:10:19 -05:00
|
|
|
Proposing several additions to the stable C API/ABI
|
|
|
|
---------------------------------------------------
|
|
|
|
|
|
|
|
Historically, the CPython C API (and subsequently, the stable ABI) has
|
|
|
|
exposed only a single API function related to the Python ``locals`` builtin:
|
|
|
|
``PyEval_GetLocals()``. However, as it returns a borrowed reference, it is
|
|
|
|
not possible to adapt that interface directly to supporting the new ``locals()``
|
|
|
|
semantics proposed in this PEP.
|
|
|
|
|
|
|
|
An earlier iteration of this PEP proposed a minimalist adaptation to the new
|
|
|
|
semantics: one C API function that behaved like the Python ``locals()`` builtin,
|
|
|
|
and another that behaved like the ``frame.f_locals`` descriptor (creating and
|
|
|
|
returning the write-through proxy if necessary).
|
|
|
|
|
|
|
|
The feedback [8]_ on that version of the C API was that it was too heavily based
|
|
|
|
on how the Python level semantics were implemented, and didn't account for the
|
|
|
|
behaviours that authors of C extensions were likely to *need*.
|
|
|
|
|
|
|
|
The broader API now being proposed came from grouping the potential reasons for
|
|
|
|
wanting to access the Python ``locals()`` namespace from an extension module
|
|
|
|
into the following cases:
|
|
|
|
|
|
|
|
* needing to exactly replicate the semantics of the Python level ``locals()``
|
|
|
|
operation. This is the ``PyLocals_Get()`` API.
|
|
|
|
* needing to behave differently depending on whether writes to the result of
|
|
|
|
``PyLocals_Get()`` will be visible to Python code or not. This is handled by
|
2021-07-21 08:28:21 -04:00
|
|
|
the ``PyLocals_GetKind()`` query API.
|
2020-02-16 07:10:19 -05:00
|
|
|
* always wanting a mutable namespace that has been pre-populated from the
|
|
|
|
current Python ``locals()`` namespace, but *not* wanting any changes to
|
|
|
|
be visible to Python code. This is the ``PyLocals_GetCopy()`` API.
|
|
|
|
* always wanting a read-only view of the current locals namespace, without
|
2021-12-22 18:25:51 -05:00
|
|
|
incurring the runtime overhead of making a full copy each time. This isn't
|
|
|
|
readily offered for optimised frames due to the need to check whether names
|
|
|
|
are currently bound or not, so no specific API is being added to cover it.
|
2020-02-16 07:10:19 -05:00
|
|
|
|
|
|
|
Historically, these kinds of checks and operations would only have been
|
|
|
|
possible if a Python implementation emulated the full CPython frame API. With
|
|
|
|
the proposed API, extension modules can instead ask more clearly for the
|
|
|
|
semantics that they actually need, giving Python implementations more
|
|
|
|
flexibility in how they provide those capabilities.
|
|
|
|
|
|
|
|
|
2021-08-26 05:26:13 -04:00
|
|
|
Comparison with PEP 667
|
|
|
|
-----------------------
|
|
|
|
|
2024-04-19 00:06:29 -04:00
|
|
|
NOTE: the comparison below is against PEP 667 as it was in December 2021.
|
|
|
|
It does not reflect the state of PEP 667 as of April 2024 (when this PEP was
|
|
|
|
withdrawn in favour of proceeding with PEP 667).
|
|
|
|
|
2022-01-21 06:03:51 -05:00
|
|
|
:pep:`667` offers a partially competing proposal for this PEP that suggests it
|
2021-08-26 05:26:13 -04:00
|
|
|
would be reasonable to eliminate the internal frame value cache on optimised
|
|
|
|
frames entirely.
|
|
|
|
|
2022-01-21 06:03:51 -05:00
|
|
|
These changes were originally offered as amendments to :pep:`558`, and the PEP
|
2021-08-26 05:26:13 -04:00
|
|
|
author rejected them for three main reasons:
|
|
|
|
|
2021-12-22 18:25:51 -05:00
|
|
|
* the initial claim that ``PyEval_GetLocals()`` was unfixable because it returns
|
2022-01-21 06:03:51 -05:00
|
|
|
a borrowed reference was simply false, as it is still working in the :pep:`558`
|
2021-08-26 05:26:13 -04:00
|
|
|
reference implementation. All that is required to keep it working is to
|
|
|
|
retain the internal frame value cache and design the fast locals proxy in
|
|
|
|
such a way that it is reasonably straightforward to keep the cache up to date
|
|
|
|
with changes in the frame state without incurring significant runtime overhead
|
|
|
|
when the cache isn't needed. Given that this claim is false, the proposal to
|
|
|
|
require that all code using the ``PyEval_GetLocals()`` API be rewritten to use
|
2022-01-21 06:03:51 -05:00
|
|
|
a new API with different refcounting semantics fails :pep:`387`'s requirement
|
2021-08-26 05:26:13 -04:00
|
|
|
that API compatibility breaks should have a large benefit to breakage ratio
|
|
|
|
(since there's no significant benefit gained from dropping the cache, no code
|
|
|
|
breakage can be justified). The only genuinely unfixable public API is
|
|
|
|
``PyFrame_LocalsToFast()`` (which is why both PEPs propose breaking that).
|
|
|
|
* without some form of internal value cache, the API performance characteristics
|
|
|
|
of the fast locals proxy mapping become quite unintuitive. ``len(proxy)``, for
|
|
|
|
example, becomes consistently O(n) in the number of variables defined on the
|
|
|
|
frame, as the proxy has to iterate over the entire fast locals array to see
|
|
|
|
which names are currently bound to values before it can determine the answer.
|
2021-12-22 18:25:51 -05:00
|
|
|
By contrast, maintaining an internal frame value cache potentially allows
|
|
|
|
proxies to largely be treated as normal dictionaries from an algorithmic
|
|
|
|
complexity point of view, with allowances only needing to be made for the
|
|
|
|
initial implicit O(n) cache refresh that runs the first time an operation
|
|
|
|
that relies on the cache being up to date is executed.
|
2021-08-26 05:26:13 -04:00
|
|
|
* the claim that a cache-free implementation would be simpler is highly suspect,
|
2022-01-21 06:03:51 -05:00
|
|
|
as :pep:`667` includes only a pure Python sketch of a subset of a mutable mapping
|
2021-08-26 05:26:13 -04:00
|
|
|
implementation, rather than a full-fledged C implementation of a new mapping
|
|
|
|
type integrated with the underlying data storage for optimised frames.
|
2022-01-21 06:03:51 -05:00
|
|
|
:pep:`558`'s fast locals proxy implementation delegates heavily to the
|
2021-08-26 05:26:13 -04:00
|
|
|
frame value cache for the operations needed to fully implement the mutable
|
|
|
|
mapping API, allowing it to re-use the existing dict implementations of the
|
|
|
|
following operations:
|
|
|
|
|
|
|
|
* ``__len__``
|
|
|
|
* ``__str__``
|
|
|
|
* ``__or__`` (dict union)
|
|
|
|
* ``__iter__`` (allowing the ``dict_keyiterator`` type to be reused)
|
|
|
|
* ``__reversed__`` (allowing the ``dict_reversekeyiterator`` type to be reused)
|
|
|
|
* ``keys()`` (allowing the ``dict_keys`` type to be reused)
|
|
|
|
* ``values()`` (allowing the ``dict_values`` type to be reused)
|
|
|
|
* ``items()`` (allowing the ``dict_items`` type to be reused)
|
|
|
|
* ``copy()``
|
|
|
|
* ``popitem()``
|
|
|
|
* value comparison operations
|
|
|
|
|
|
|
|
Of the three reasons, the first is the most important (since we need compelling
|
|
|
|
reasons to break API backwards compatibility, and we don't have them).
|
|
|
|
|
2022-01-21 06:03:51 -05:00
|
|
|
However, after reviewing :pep:`667`'s proposed Python level semantics, the author
|
2021-12-22 18:25:51 -05:00
|
|
|
of this PEP eventually agreed that they *would* be simpler for users of the
|
|
|
|
Python ``locals()`` API, so this distinction between the two PEPs has been
|
|
|
|
eliminated: regardless of which PEP and implementation is accepted, the fast
|
|
|
|
locals proxy object *always* provides a consistent view of the current state
|
|
|
|
of the local variables, even if this results in some operations becoming O(n)
|
|
|
|
that would be O(1) on a regular dictionary (specifically, ``len(proxy)``
|
|
|
|
becomes O(n), since it needs to check which names are currently bound, and proxy
|
|
|
|
mapping comparisons avoid relying on the length check optimisation that allows
|
|
|
|
differences in the number of stored keys to be detected quickly for regular
|
|
|
|
mappings).
|
|
|
|
|
|
|
|
Due to the adoption of these non-standard performance characteristics in the
|
|
|
|
proxy implementation, the ``PyLocals_GetView()`` and ``PyFrame_GetLocalsView()``
|
|
|
|
C APIs were also removed from the proposal in this PEP.
|
|
|
|
|
|
|
|
This leaves the only remaining points of distinction between the two PEPs as
|
|
|
|
specifically related to the C API:
|
|
|
|
|
2022-01-21 06:03:51 -05:00
|
|
|
* :pep:`667` still proposes completely unnecessary C API breakage (the programmatic
|
2021-12-22 18:25:51 -05:00
|
|
|
deprecation and eventual removal of ``PyEval_GetLocals()``,
|
|
|
|
``PyFrame_FastToLocalsWithError()``, and ``PyFrame_FastToLocals()``) without
|
2022-08-17 01:26:55 -04:00
|
|
|
justification, when it is entirely possible to keep these working indefinitely
|
2021-12-22 18:25:51 -05:00
|
|
|
(and interoperably) given a suitably designed fast locals proxy implementation
|
|
|
|
* the fast locals proxy handling of additional variables is defined in this PEP
|
|
|
|
in a way that is fully interoperable with the existing ``PyEval_GetLocals()``
|
2022-01-21 06:03:51 -05:00
|
|
|
API. In the proxy implementation proposed in :pep:`667`, users of the new frame
|
2021-12-22 18:25:51 -05:00
|
|
|
API will not see changes made to additional variables by users of the old API,
|
|
|
|
and changes made to additional variables via the old API will be overwritten
|
|
|
|
on subsequent calls to ``PyEval_GetLocals()``.
|
2022-01-21 06:03:51 -05:00
|
|
|
* the ``PyLocals_Get()`` API in this PEP is called ``PyEval_Locals()`` in :pep:`667`.
|
2021-12-22 18:25:51 -05:00
|
|
|
This function name is a bit strange as it lacks a verb, making it look more
|
|
|
|
like a type name than a data access API.
|
|
|
|
* this PEP adds ``PyLocals_GetCopy()`` and ``PyFrame_GetLocalsCopy()`` APIs to
|
|
|
|
allow extension modules to easily avoid incurring a double copy operation in
|
2022-08-17 01:26:55 -04:00
|
|
|
frames where ``PyLocals_Get()`` already makes a copy
|
2021-12-22 18:25:51 -05:00
|
|
|
* this PEP adds ``PyLocals_Kind``, ``PyLocals_GetKind()``, and
|
|
|
|
``PyFrame_GetLocalsKind()`` to allow extension modules to identify when code
|
|
|
|
is running at function scope without having to inspect non-portable frame and
|
2021-12-22 22:04:55 -05:00
|
|
|
code object APIs (without the proposed query API, the existing equivalent to
|
2021-12-22 18:25:51 -05:00
|
|
|
the new ``PyLocals_GetKind() == PyLocals_SHALLOW_COPY`` check is to include
|
|
|
|
the CPython internal frame API headers and check if
|
|
|
|
``_PyFrame_GetCode(PyEval_GetFrame())->co_flags & CO_OPTIMIZED`` is set)
|
|
|
|
|
|
|
|
The Python pseudo-code below is based on the implementation sketch presented
|
2022-01-21 06:03:51 -05:00
|
|
|
in :pep:`667` as of the time of writing (2021-10-24). The differences that
|
2021-12-22 18:25:51 -05:00
|
|
|
provide the improved interoperability between the new fast locals proxy API
|
|
|
|
and the existing ``PyEval_GetLocals()`` API are noted in comments.
|
|
|
|
|
2022-01-21 06:03:51 -05:00
|
|
|
As in :pep:`667`, all attributes that start with an underscore are invisible and
|
2021-12-22 18:25:51 -05:00
|
|
|
cannot be accessed directly. They serve only to illustrate the proposed design.
|
|
|
|
|
2022-01-21 06:03:51 -05:00
|
|
|
For simplicity (and as in :pep:`667`), the handling of module and class level
|
2021-12-22 18:25:51 -05:00
|
|
|
frames is omitted (they're much simpler, as ``_locals`` *is* the execution
|
|
|
|
namespace, so no translation is required).
|
|
|
|
|
|
|
|
::
|
|
|
|
|
|
|
|
NULL: Object # NULL is a singleton representing the absence of a value.
|
|
|
|
|
|
|
|
class CodeType:
|
|
|
|
|
|
|
|
_name_to_offset_mapping_impl: dict | NULL
|
|
|
|
...
|
|
|
|
|
|
|
|
def __init__(self, ...):
|
|
|
|
self._name_to_offset_mapping_impl = NULL
|
|
|
|
self._variable_names = deduplicate(
|
|
|
|
self.co_varnames + self.co_cellvars + self.co_freevars
|
|
|
|
)
|
|
|
|
...
|
|
|
|
|
|
|
|
def _is_cell(self, offset):
|
|
|
|
... # How the interpreter identifies cells is an implementation detail
|
|
|
|
|
|
|
|
@property
|
|
|
|
def _name_to_offset_mapping(self):
|
|
|
|
"Mapping of names to offsets in local variable array."
|
|
|
|
if self._name_to_offset_mapping_impl is NULL:
|
|
|
|
|
|
|
|
self._name_to_offset_mapping_impl = {
|
|
|
|
name: index for (index, name) in enumerate(self._variable_names)
|
|
|
|
}
|
|
|
|
return self._name_to_offset_mapping_impl
|
|
|
|
|
|
|
|
class FrameType:
|
|
|
|
|
|
|
|
_fast_locals : array[Object] # The values of the local variables, items may be NULL.
|
|
|
|
_locals: dict | NULL # Dictionary returned by PyEval_GetLocals()
|
|
|
|
|
|
|
|
def __init__(self, ...):
|
|
|
|
self._locals = NULL
|
|
|
|
...
|
|
|
|
|
|
|
|
@property
|
|
|
|
def f_locals(self):
|
|
|
|
return FastLocalsProxy(self)
|
|
|
|
|
|
|
|
class FastLocalsProxy:
|
|
|
|
|
|
|
|
__slots__ "_frame"
|
|
|
|
|
|
|
|
def __init__(self, frame:FrameType):
|
|
|
|
self._frame = frame
|
|
|
|
|
|
|
|
def _set_locals_entry(self, name, val):
|
|
|
|
f = self._frame
|
|
|
|
if f._locals is NULL:
|
|
|
|
f._locals = {}
|
|
|
|
f._locals[name] = val
|
|
|
|
|
|
|
|
def __getitem__(self, name):
|
|
|
|
f = self._frame
|
|
|
|
co = f.f_code
|
|
|
|
if name in co._name_to_offset_mapping:
|
|
|
|
index = co._name_to_offset_mapping[name]
|
|
|
|
val = f._fast_locals[index]
|
|
|
|
if val is NULL:
|
|
|
|
raise KeyError(name)
|
|
|
|
if co._is_cell(offset)
|
|
|
|
val = val.cell_contents
|
|
|
|
if val is NULL:
|
|
|
|
raise KeyError(name)
|
|
|
|
# PyEval_GetLocals() interop: implicit frame cache refresh
|
|
|
|
self._set_locals_entry(name, val)
|
|
|
|
return val
|
|
|
|
# PyEval_GetLocals() interop: frame cache may contain additional names
|
|
|
|
if f._locals is NULL:
|
|
|
|
raise KeyError(name)
|
|
|
|
return f._locals[name]
|
|
|
|
|
|
|
|
def __setitem__(self, name, value):
|
|
|
|
f = self._frame
|
|
|
|
co = f.f_code
|
|
|
|
if name in co._name_to_offset_mapping:
|
|
|
|
index = co._name_to_offset_mapping[name]
|
|
|
|
kind = co._local_kinds[index]
|
|
|
|
if co._is_cell(offset)
|
|
|
|
cell = f._locals[index]
|
|
|
|
cell.cell_contents = val
|
|
|
|
else:
|
|
|
|
f._fast_locals[index] = val
|
|
|
|
# PyEval_GetLocals() interop: implicit frame cache update
|
|
|
|
# even for names that are part of the fast locals array
|
|
|
|
self._set_locals_entry(name, val)
|
|
|
|
|
|
|
|
def __delitem__(self, name):
|
|
|
|
f = self._frame
|
|
|
|
co = f.f_code
|
|
|
|
if name in co._name_to_offset_mapping:
|
|
|
|
index = co._name_to_offset_mapping[name]
|
|
|
|
kind = co._local_kinds[index]
|
|
|
|
if co._is_cell(offset)
|
|
|
|
cell = f._locals[index]
|
|
|
|
cell.cell_contents = NULL
|
|
|
|
else:
|
|
|
|
f._fast_locals[index] = NULL
|
|
|
|
# PyEval_GetLocals() interop: implicit frame cache update
|
|
|
|
# even for names that are part of the fast locals array
|
|
|
|
if f._locals is not NULL:
|
|
|
|
del f._locals[name]
|
|
|
|
|
|
|
|
def __iter__(self):
|
|
|
|
f = self._frame
|
|
|
|
co = f.f_code
|
|
|
|
for index, name in enumerate(co._variable_names):
|
|
|
|
val = f._fast_locals[index]
|
|
|
|
if val is NULL:
|
|
|
|
continue
|
|
|
|
if co._is_cell(offset):
|
|
|
|
val = val.cell_contents
|
|
|
|
if val is NULL:
|
|
|
|
continue
|
|
|
|
yield name
|
|
|
|
for name in f._locals:
|
|
|
|
# Yield any extra names not defined on the frame
|
|
|
|
if name in co._name_to_offset_mapping:
|
|
|
|
continue
|
|
|
|
yield name
|
|
|
|
|
|
|
|
def popitem(self):
|
|
|
|
f = self._frame
|
|
|
|
co = f.f_code
|
|
|
|
for name in self:
|
|
|
|
val = self[name]
|
|
|
|
# PyEval_GetLocals() interop: implicit frame cache update
|
|
|
|
# even for names that are part of the fast locals array
|
|
|
|
del name
|
|
|
|
return name, val
|
|
|
|
|
|
|
|
def _sync_frame_cache(self):
|
|
|
|
# This method underpins PyEval_GetLocals, PyFrame_FastToLocals
|
|
|
|
# PyFrame_GetLocals, PyLocals_Get, mapping comparison, etc
|
|
|
|
f = self._frame
|
|
|
|
co = f.f_code
|
|
|
|
res = 0
|
|
|
|
if f._locals is NULL:
|
|
|
|
f._locals = {}
|
|
|
|
for index, name in enumerate(co._variable_names):
|
|
|
|
val = f._fast_locals[index]
|
|
|
|
if val is NULL:
|
|
|
|
f._locals.pop(name, None)
|
|
|
|
continue
|
|
|
|
if co._is_cell(offset):
|
|
|
|
if val.cell_contents is NULL:
|
|
|
|
f._locals.pop(name, None)
|
|
|
|
continue
|
|
|
|
f._locals[name] = val
|
|
|
|
|
|
|
|
def __len__(self):
|
|
|
|
self._sync_frame_cache()
|
|
|
|
return len(self._locals)
|
|
|
|
|
2022-01-21 06:03:51 -05:00
|
|
|
Note: the simplest way to convert the earlier iterations of the :pep:`558`
|
2021-12-22 18:25:51 -05:00
|
|
|
reference implementation into a preliminary implementation of the now proposed
|
|
|
|
semantics is to remove the ``frame_cache_updated`` checks in affected operations,
|
|
|
|
and instead always sync the frame cache in those methods. Adopting that approach
|
|
|
|
changes the algorithmic complexity of the following operations as shown (where
|
|
|
|
``n`` is the number of local and cell variables defined on the frame):
|
2021-08-26 05:26:13 -04:00
|
|
|
|
|
|
|
* ``__len__``: O(1) -> O(n)
|
2021-12-22 18:25:51 -05:00
|
|
|
* value comparison operations: no longer benefit from O(1) length check shortcut
|
2021-08-26 05:26:13 -04:00
|
|
|
* ``__iter__``: O(1) -> O(n)
|
|
|
|
* ``__reversed__``: O(1) -> O(n)
|
|
|
|
* ``keys()``: O(1) -> O(n)
|
|
|
|
* ``values()``: O(1) -> O(n)
|
|
|
|
* ``items()``: O(1) -> O(n)
|
|
|
|
* ``popitem()``: O(1) -> O(n)
|
|
|
|
|
2021-12-22 18:25:51 -05:00
|
|
|
The length check and value comparison operations have relatively limited
|
|
|
|
opportunities for improvement: without allowing usage of a potentially stale
|
|
|
|
cache, the only way to know how many variables are currently bound is to iterate
|
|
|
|
over all of them and check, and if the implementation is going to be spending
|
|
|
|
that many cycles on an operation anyway, it may as well spend it updating the
|
|
|
|
frame value cache and then consuming the result. These operations are O(n) in
|
2022-01-21 06:03:51 -05:00
|
|
|
both this PEP and in :pep:`667`. Customised implementations could be provided that
|
2021-12-22 18:25:51 -05:00
|
|
|
*are* faster than updating the frame cache, but it's far from clear that the
|
|
|
|
extra code complexity needed to speed these operations up would be worthwhile
|
|
|
|
when it only offers a linear performance improvement rather than an algorithmic
|
|
|
|
complexity improvement.
|
|
|
|
|
|
|
|
The O(1) nature of the other operations can be restored by adding implementation
|
|
|
|
code that doesn't rely on the value cache being up to date.
|
|
|
|
|
2022-08-17 01:26:55 -04:00
|
|
|
Keeping the iterator/iterable retrieval methods as O(1) will involve
|
2021-12-22 18:25:51 -05:00
|
|
|
writing custom replacements for the corresponding builtin dict helper types,
|
2022-01-21 06:03:51 -05:00
|
|
|
just as proposed in :pep:`667`. As illustrated above, the implementations would
|
|
|
|
be similar to the pseudo-code presented in :pep:`667`, but not identical (due to
|
2021-12-22 18:25:51 -05:00
|
|
|
the improved ``PyEval_GetLocals()`` interoperability offered by this PEP
|
|
|
|
affecting the way it stores extra variables).
|
|
|
|
|
|
|
|
``popitem()`` can be improved from "always O(n)" to "O(n) worst case" by
|
|
|
|
creating a custom implementation that relies on the improved iteration APIs.
|
|
|
|
|
|
|
|
To ensure stale frame information is never presented in the Python fast locals
|
|
|
|
proxy API, these changes in the reference implementation will need to be
|
|
|
|
implemented before merging.
|
|
|
|
|
|
|
|
The current implementation at time of writing (2021-10-24) also still stores a
|
|
|
|
copy of the fast refs mapping on each frame rather than storing a single
|
|
|
|
instance on the underlying code object (as it still stores cell references
|
|
|
|
directly, rather than check for cells on each fast locals array access). Fixing
|
|
|
|
this would also be required before merging.
|
2021-08-26 05:26:13 -04:00
|
|
|
|
|
|
|
|
2017-09-08 17:55:50 -04:00
|
|
|
Implementation
|
|
|
|
==============
|
|
|
|
|
2019-04-23 19:04:37 -04:00
|
|
|
The reference implementation update is in development as a draft pull
|
|
|
|
request on GitHub ([6]_).
|
2017-09-08 17:55:50 -04:00
|
|
|
|
2017-10-22 02:12:25 -04:00
|
|
|
|
|
|
|
Acknowledgements
|
|
|
|
================
|
|
|
|
|
|
|
|
Thanks to Nathaniel J. Smith for proposing the write-through proxy idea in
|
|
|
|
[1]_ and pointing out some critical design flaws in earlier iterations of the
|
|
|
|
PEP that attempted to avoid introducing such a proxy.
|
|
|
|
|
2020-02-16 07:10:19 -05:00
|
|
|
Thanks to Steve Dower and Petr Viktorin for asking that more attention be paid
|
2022-08-17 01:26:55 -04:00
|
|
|
to the developer experience of the proposed C API additions [8]_ [13]_.
|
2020-02-16 07:10:19 -05:00
|
|
|
|
2021-08-26 05:26:13 -04:00
|
|
|
Thanks to Larry Hastings for the suggestion on how to use enums in the stable
|
|
|
|
ABI while ensuring that they safely support typecasting from arbitrary
|
|
|
|
integers.
|
|
|
|
|
2021-06-27 03:37:01 -04:00
|
|
|
Thanks to Mark Shannon for pushing for further simplification of the C level
|
2021-07-21 08:28:21 -04:00
|
|
|
API and semantics, as well as significant clarification of the PEP text (and for
|
|
|
|
restarting discussion on the PEP in early 2021 after a further year of
|
2022-08-17 01:26:55 -04:00
|
|
|
inactivity) [10]_ [11]_ [12]_. Mark's comments that were ultimately published as
|
2022-01-21 06:03:51 -05:00
|
|
|
:pep:`667` also directly resulted in several implementation efficiency improvements
|
2021-08-26 05:26:13 -04:00
|
|
|
that avoid incurring the cost of redundant O(n) mapping refresh operations
|
2021-12-22 18:25:51 -05:00
|
|
|
when the relevant mappings aren't used, as well as the change to ensure that
|
|
|
|
the state reported through the Python level ``f_locals`` API is never stale.
|
2021-06-27 03:37:01 -04:00
|
|
|
|
2017-10-22 02:12:25 -04:00
|
|
|
|
2017-09-08 17:55:50 -04:00
|
|
|
References
|
|
|
|
==========
|
|
|
|
|
2022-08-17 01:26:55 -04:00
|
|
|
.. [1] `Broken local variable assignment given threads + trace hook + closure
|
|
|
|
<https://github.com/python/cpython/issues/74929>`_
|
2017-09-08 17:55:50 -04:00
|
|
|
|
2022-08-17 01:26:55 -04:00
|
|
|
.. [3] `Updating function local variables from pdb is unreliable
|
|
|
|
<https://github.com/python/cpython/issues/5384)>`_
|
2017-09-08 17:55:50 -04:00
|
|
|
|
2022-08-17 01:26:55 -04:00
|
|
|
.. [4] `CPython's Python API for installing trace hooks
|
|
|
|
<https://docs.python.org/dev/library/sys.html#sys.settrace>`_
|
2017-10-22 02:12:25 -04:00
|
|
|
|
2022-08-17 01:26:55 -04:00
|
|
|
.. [5] `CPython's C API for installing trace hooks
|
|
|
|
<https://docs.python.org/3/c-api/init.html#c.PyEval_SetTrace>`_
|
2017-10-22 02:12:25 -04:00
|
|
|
|
2022-08-17 01:26:55 -04:00
|
|
|
.. [6] `PEP 558 reference implementation
|
|
|
|
<https://github.com/python/cpython/pull/3640/files>`_
|
2017-10-22 02:12:25 -04:00
|
|
|
|
2022-08-17 01:26:55 -04:00
|
|
|
.. [7] `Nathaniel's review of possible function level semantics for locals()
|
|
|
|
<https://mail.python.org/pipermail/python-dev/2019-May/157738.html>`_
|
2019-04-23 19:04:37 -04:00
|
|
|
|
2022-08-17 01:26:55 -04:00
|
|
|
.. [8] `Discussion of more intentionally designed C API enhancements
|
|
|
|
<https://discuss.python.org/t/pep-558-defined-semantics-for-locals/2936/3>`_
|
2019-12-30 01:22:46 -05:00
|
|
|
|
2022-08-17 01:26:55 -04:00
|
|
|
.. [9] `Disable automatic update of frame locals during tracing
|
|
|
|
<https://github.com/python/cpython/issues/86363>`_
|
2020-02-16 07:10:19 -05:00
|
|
|
|
2022-08-17 01:26:55 -04:00
|
|
|
.. [10] `python-dev thread: Resurrecting PEP 558 (Defined semantics for locals())
|
|
|
|
<https://mail.python.org/archives/list/python-dev@python.org/thread/TUQOEWQSCQZPUDV2UFFKQ3C3I4WGFPAJ/>`_
|
2017-10-22 02:12:25 -04:00
|
|
|
|
2022-08-17 01:26:55 -04:00
|
|
|
.. [11] `python-dev thread: Comments on PEP 558
|
|
|
|
<https://mail.python.org/archives/list/python-dev@python.org/thread/A3UN4DGBCOB45STE6AQBITJFW6UZE43O/>`_
|
2021-07-21 08:28:21 -04:00
|
|
|
|
2022-08-17 01:26:55 -04:00
|
|
|
.. [12] `python-dev thread: More comments on PEP 558
|
|
|
|
<https://mail.python.org/archives/list/python-dev@python.org/thread/7TKPMD5LHCBXGFUIMKDAUZELRH6EX76S/>`_
|
2021-07-21 08:28:21 -04:00
|
|
|
|
2022-08-17 01:26:55 -04:00
|
|
|
.. [13] `Petr Viktorin's suggestion to use an enum for PyLocals_Get's behaviour
|
|
|
|
<https://mail.python.org/archives/list/python-dev@python.org/message/BTQUBHIVE766RPIWLORC5ZYRCRC4CEBL/>`_
|
2021-07-21 08:28:21 -04:00
|
|
|
|
2017-09-08 17:55:50 -04:00
|
|
|
Copyright
|
|
|
|
=========
|
|
|
|
|
2021-07-18 00:56:28 -04:00
|
|
|
This document is placed in the public domain or under the
|
|
|
|
CC0-1.0-Universal license, whichever is more permissive.
|