PEP 558: Make fast locals proxy independent of the legacy dynamic snapshot (#1787)
This commit is contained in:
parent
cd26ba8af9
commit
70442b01a0
255
pep-0558.rst
255
pep-0558.rst
|
@ -7,7 +7,7 @@ Status: Draft
|
|||
Type: Standards Track
|
||||
Content-Type: text/x-rst
|
||||
Created: 08-Sep-2017
|
||||
Python-Version: 3.10
|
||||
Python-Version: 3.11
|
||||
Post-History: 2017-09-08, 2019-05-22, 2019-05-30, 2019-12-30
|
||||
|
||||
|
||||
|
@ -17,7 +17,7 @@ Abstract
|
|||
The semantics of the ``locals()`` builtin have historically been underspecified
|
||||
and hence implementation dependent.
|
||||
|
||||
This PEP proposes formally standardising on the behaviour of the CPython 3.8
|
||||
This PEP proposes formally standardising on the behaviour of the CPython 3.10
|
||||
reference implementation for most execution scopes, with some adjustments to the
|
||||
behaviour at function scope to make it more predictable and independent of the
|
||||
presence or absence of tracing functions.
|
||||
|
@ -29,7 +29,6 @@ Python C API/ABI::
|
|||
int PyLocals_GetReturnsCopy();
|
||||
PyObject * PyLocals_GetCopy();
|
||||
PyObject * PyLocals_GetView();
|
||||
int PyLocals_RefreshViews();
|
||||
|
||||
It also proposes the addition of several supporting functions and type
|
||||
definitions to the CPython C API.
|
||||
|
@ -82,7 +81,7 @@ 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
|
||||
Python 3.9, the CPython interpreter behaves differently when a trace hook has
|
||||
Python 3.10, the CPython interpreter behaves differently when a trace hook has
|
||||
been registered in one or more threads via an implementation dependent mechanism
|
||||
like ``sys.settrace`` ([4]_) in CPython's ``sys`` module or
|
||||
``PyEval_SetTrace`` ([5]_) in CPython's C API.
|
||||
|
@ -94,7 +93,9 @@ API semantics clearer and easier for interactive debuggers to rely on.
|
|||
|
||||
The proposed elimination of tracing mode affects the semantics of frame object
|
||||
references obtained through other means, such as via a traceback, or via the
|
||||
``sys._getframe()`` API.
|
||||
``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.
|
||||
|
||||
|
||||
New ``locals()`` documentation
|
||||
|
@ -256,6 +257,59 @@ behavioural problems mentioned in the Rationale).
|
|||
CPython Implementation Changes
|
||||
==============================
|
||||
|
||||
Summary of proposed implementation-specific changes
|
||||
---------------------------------------------------
|
||||
|
||||
* Changes are made as neccessary to provide the updated Python level semantics
|
||||
* Two new functions are added to the stable ABI to replicate the updated
|
||||
behaviour of the Python ``locals()`` builtin::
|
||||
|
||||
PyObject * PyLocals_Get();
|
||||
int PyLocals_GetReturnsCopy();
|
||||
* One new function is added to the stable ABI to efficiently get a snapshot of
|
||||
the local namespace in the running frame::
|
||||
|
||||
PyObject * PyLocals_GetCopy();
|
||||
* One new function is added to the stable ABI to get a read-only view of the
|
||||
local namespace in the running frame::
|
||||
|
||||
PyObject * PyLocals_GetView();
|
||||
* Corresponding frame accessor functions for these new public APIs are added to
|
||||
the CPython frame C API
|
||||
* On optimised frames, the Python level ``f_locals`` API will become a direct
|
||||
read/write proxy for the frame's local and closure variable storage, and hence
|
||||
no longer support storing additional data that doesn't correspond to a local
|
||||
or closure variable on the underyling frame object
|
||||
* 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
|
||||
APIs
|
||||
* ``PyFrame_FastToLocals()`` and ``PyFrame_FastToLocalsWithError()`` remain
|
||||
supported and do not emit a programmatic warning, but will be deprecated in
|
||||
the documentation in favour of the new APIs
|
||||
* ``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
|
||||
``PyFrame_GetLocalsView()`` for read-only access and
|
||||
``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
|
||||
copy of the local namespace rather than a direct reference to the internal
|
||||
dynamically updated snapshot returned by ``PyEval_GetLocals()``.
|
||||
|
||||
At least for now, this copied snapshot will continue to include any extra
|
||||
key/value pairs injected via the ``PyEval_GetLocals()`` API, but that could
|
||||
potentially change in a future release if that API is ever fully deprecated.
|
||||
|
||||
|
||||
Resolving the issues with tracing mode behaviour
|
||||
------------------------------------------------
|
||||
|
||||
|
@ -296,51 +350,53 @@ dependent ``frame.f_locals`` interface, as a frame reference is what gets
|
|||
passed to hook implementations.
|
||||
|
||||
Instead of being a direct reference to the internal dynamic snapshot used to
|
||||
populate the independent snapshots returned by ``locals()``, ``frame.f_locals``
|
||||
will be updated to instead return a dedicated proxy type (implemented as a
|
||||
private subclass of the existing ``types.MappingProxyType``) that has two
|
||||
internal attributes not exposed as part of the Python runtime API:
|
||||
populate the independent snapshots returned by ``locals()``, the Python level
|
||||
``frame.f_locals`` will be updated to instead return a dedicated proxy type
|
||||
that has two internal attributes not exposed as part of the Python runtime
|
||||
API:
|
||||
|
||||
* *mapping*: an implicitly updated snapshot of the function local variables
|
||||
and closure references, as well as any arbitrary items that have been set via
|
||||
the mapping API, even if they don't have storage allocated for them on the
|
||||
underlying frame
|
||||
* *frame*: the underlying frame that the snapshot is for
|
||||
* *fast_refs*: a mapping from variable names to either fast local storage
|
||||
offsets (for local variables) or to closure cells (for closure variables).
|
||||
This mapping is lazily initialized on the first access to the mapping, rather
|
||||
being eagerly populated as soon as the proxy is created.
|
||||
|
||||
For backwards compatibility, the stored snapshot will continue to be made
|
||||
available through the public ``PyEval_GetLocals()`` C API.
|
||||
``__getitem__`` operations on the proxy will populate the ``fast_refs`` mapping
|
||||
(if it is not already populated), and then either return the relevant value
|
||||
(if the key is found in the ``fast_refs`` mapping), or else raise ``KeyError``.
|
||||
|
||||
``__getitem__`` operations on the proxy will read directly from the stored
|
||||
snapshot.
|
||||
As the frame storage is always accessed directly, the proxy will automatically
|
||||
pick up name binding operations that take place as the function executes.
|
||||
|
||||
The stored snapshot is implicitly updated when the ``f_locals`` attribute is
|
||||
retrieved from the frame object, as well as individual keys being updated by
|
||||
mutating operations on the proxy itself. This means that if a reference to the
|
||||
proxy is obtained from within the function, the proxy won't implicitly pick up
|
||||
name binding operations that take place as the function executes - the
|
||||
``f_locals`` attribute on the frame will need to be accessed again in order to
|
||||
trigger a refresh.
|
||||
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.
|
||||
|
||||
``__setitem__`` and ``__delitem__`` operations on the proxy will affect not only
|
||||
the dynamic snapshot, but *also* the corresponding fast local or cell reference
|
||||
on the underlying frame.
|
||||
Unlike the existing ``f_locals`` implementation on optimised frames, the frame
|
||||
locals proxy will raise ``KeyError`` for attempts to write to keys that aren't
|
||||
defined as local or closure variables on the underyling frame.
|
||||
|
||||
After a frame has finished executing, cell references can still be updated via
|
||||
the proxy, but the link back to the underlying frame is explicitly broken to
|
||||
avoid creating a persistent reference cycle that unexpectedly keeps frames
|
||||
alive.
|
||||
Other ``Mapping`` and ``MutableMapping`` methods will behave as expected for a
|
||||
mapping with these essential method semantics.
|
||||
|
||||
Other MutableMapping methods will behave as expected for a mapping with these
|
||||
essential method semantics.
|
||||
For backwards compatibility with the existing ``PyEval_GetLocals()`` C API, the
|
||||
C level ``f_locals`` struct field does *not* store an instance of the new proxy
|
||||
type. In most cases the C level ``f_locals`` struct field will be ``NULL`` on an
|
||||
optimised frame, but if ``PyEval_GetLocals()`` is called, or
|
||||
``PyFrame_LocalsToFast()`` or ``PyFrame_FastToLocalsWithError`` are called for
|
||||
any other reason (e.g. to resolve a Python level ``locals()`` builtin call),
|
||||
then the field will be populated with an implicitly updated snapshot of the
|
||||
local variables and closure references for the frame, just as it is today.
|
||||
|
||||
This internal dynamic snapshot will preserve the existing semantics where keys
|
||||
that are added but do not correspond to a local or closure variable on the frame
|
||||
will be left alone by future snapshot updates.
|
||||
|
||||
Making the behaviour at function scope less surprising
|
||||
------------------------------------------------------
|
||||
|
||||
The ``locals()`` builtin will be made aware of the new fast locals proxy type,
|
||||
and when it detects it on a frame, will return a fresh snapshot of the local
|
||||
namespace (i.e. the equivalent of ``dict(frame.f_locals)``) rather than
|
||||
returning the proxy directly.
|
||||
Storing only the optional dynamic snapshot on the frame rather than storing an
|
||||
instance of the proxy type also 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.
|
||||
|
||||
|
||||
Changes to the stable C API/ABI
|
||||
|
@ -378,7 +434,6 @@ Python scope, the stable C ABI would gain the following new functions::
|
|||
|
||||
PyObject * PyLocals_GetCopy();
|
||||
PyObject * PyLocals_GetView();
|
||||
int PyLocals_RefreshViews();
|
||||
|
||||
``PyLocals_GetCopy()`` returns a new dict instance populated from the current
|
||||
locals namespace. Roughly equivalent to ``dict(locals())`` in Python code, but
|
||||
|
@ -386,19 +441,8 @@ avoids the double-copy in the case where ``locals()`` already returns a shallow
|
|||
copy.
|
||||
|
||||
``PyLocals_GetView()`` returns a new read-only mapping proxy instance for the
|
||||
current locals namespace. This view is immediately updated for all local
|
||||
variable changes at module and class scope, and when using exec() or eval().
|
||||
It is updated at implementation dependent times at function/coroutine/generator
|
||||
scope (accessing the existing ``PyEval_GetLocals()`` API, or any of the
|
||||
``PyLocals_Get*`` APIs, including calling ``PyLocals_GetView()`` again, will
|
||||
always force an update).
|
||||
|
||||
``PyLocals_RefreshViews()`` updates any views previously returned by
|
||||
``PyLocals_GetView()`` with the current status of the frame. A non-zero return
|
||||
value indicates that an error occurred with the update, and the views may not
|
||||
accurately reflect the current state of the frame. The Python exception state
|
||||
will be set in such cases. This function also refreshes the shared dynamic
|
||||
snapshot returned by ``PyEval_GetLocals()`` in optimised scopes.
|
||||
current locals namespace. This view immediately reflects all local variable
|
||||
changes, independently of whether the running frame is optimised or not.
|
||||
|
||||
The existing ``PyEval_GetLocals()`` API will retain its existing behaviour in
|
||||
CPython (mutable locals at class and module scope, shared dynamic snapshot
|
||||
|
@ -409,17 +453,20 @@ 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:
|
||||
|
||||
* Use ``PyLocals_Get()`` to exactly match the semantics of the Python level
|
||||
``locals()`` builtin.
|
||||
* Use ``PyLocals_GetView()`` for read-only access to the current locals
|
||||
namespace.
|
||||
* 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.
|
||||
* Use ``PyLocals_Get()`` to exactly match the semantics of the Python level
|
||||
``locals()`` builtin.
|
||||
* Query ``PyLocals_GetReturnsCopy()`` explicitly to implement custom handling
|
||||
(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.
|
||||
* Use implementation specific APIs (e.g. ``PyObject_GetAttrString(frame, "f_locals")``)
|
||||
if read/write access to the frame is required and ``PyLocals_GetReturnsCopy()``
|
||||
is true.
|
||||
|
||||
|
||||
Changes to the public CPython C API
|
||||
|
@ -427,20 +474,23 @@ Changes to the public CPython C API
|
|||
|
||||
The existing ``PyEval_GetLocals()`` API returns a borrowed reference, which
|
||||
means it cannot be updated to return the new shallow copies at function
|
||||
scope. Instead, it will return a borrowed reference to the internal mapping
|
||||
maintained by the fast locals proxy. This shared mapping will behave similarly
|
||||
to the existing shared mapping in Python 3.8 and earlier, but the exact
|
||||
conditions under which it gets refreshed will be different. Specifically:
|
||||
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:
|
||||
|
||||
* accessing the Python level ``f_locals`` frame attribute
|
||||
* any call to ``PyEval_GetLocals()``, ``PyLocals_Get()``, ``PyLocals_GetCopy()``,
|
||||
or the Python ``locals()`` builtin while the frame is running
|
||||
* any call to ``PyFrame_GetLocals()``, ``PyFrame_GetLocalsCopy()``,
|
||||
``PyFrame_GetLocalsView()``, ``_PyFrame_BorrowLocals()``, or
|
||||
``PyFrame_RefreshLocalsViews()`` for the frame
|
||||
* any call to ``PyLocals_Get()``, ``PyLocals_GetCopy()``, ``PyLocals_GetView()``,
|
||||
``PyLocals_RefreshViews()``, or the Python ``locals()`` builtin while the
|
||||
frame is running
|
||||
``_PyFrame_BorrowLocals()``, ``PyFrame_FastToLocals()``, or
|
||||
``PyFrame_FastToLocalsWithError()`` for the frame
|
||||
|
||||
(Even though ``PyEval_GetLocals()`` is part of the stable C API/ABI, the
|
||||
Accessing the frame "view" APIs will *not* implicitly update the shared dynamic
|
||||
snapshot, and the CPython trace hook handling will no longer implicitly update
|
||||
it either.
|
||||
|
||||
(Note: even though ``PyEval_GetLocals()`` is part of the stable C API/ABI, the
|
||||
specifics of when the namespace it returns gets refreshed are still an
|
||||
interpreter implementation detail)
|
||||
|
||||
|
@ -451,7 +501,6 @@ needed to support the stable C API/ABI updates::
|
|||
int PyFrame_GetLocalsReturnsCopy(frame);
|
||||
PyObject * PyFrame_GetLocalsCopy(frame);
|
||||
PyObject * PyFrame_GetLocalsView(frame);
|
||||
int PyFrame_RefreshLocalsViews(frame);
|
||||
PyObject * _PyFrame_BorrowLocals(frame);
|
||||
|
||||
``PyFrame_GetLocals(frame)`` is the underlying API for ``PyLocals_Get()``.
|
||||
|
@ -464,27 +513,18 @@ needed to support the stable C API/ABI updates::
|
|||
|
||||
``PyFrame_GetLocalsView(frame)`` is the underlying API for ``PyLocals_GetView()``.
|
||||
|
||||
``PyFrame_RefreshLocalsViews(frame)`` is the underlying API for
|
||||
``PyLocals_RefreshViews()``. In the draft reference implementation, it is also
|
||||
needed in CPython when accessing the frame ``f_locals`` attribute directly from
|
||||
the frame struct, or the mapping returned by ``_PyFrame_BorrowLocals(frame)``,
|
||||
and ``PyFrame_GetLocalsReturnsCopy()`` is true for that frame (otherwise the
|
||||
locals proxy may report stale information).
|
||||
|
||||
``_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
|
||||
implementations. However, it is documented and visible to the linker because
|
||||
the dynamic snapshot stored inside the write-through proxy is otherwise
|
||||
completely inaccessible from C code (in the draft reference implementation,
|
||||
the struct definition for the fast locals proxy itself is deliberately kept
|
||||
private to the frame implementation, so not even the rest of CPython can see
|
||||
it - instances must be manipulated via the Python mapping C API).
|
||||
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.
|
||||
|
||||
The ``PyFrame_LocalsToFast()`` function will be changed to always emit
|
||||
``RuntimeError``, explaining that it is no longer a supported operation, and
|
||||
affected code should be updated to use ``PyFrame_GetLocals(frame)``,
|
||||
``PyFrame_GetLocalsCopy(frame)``, or ``PyFrame_GetLocalsView(frame)`` instead.
|
||||
``PyFrame_GetLocalsCopy(frame)``, ``PyFrame_GetLocalsView(frame)``, or
|
||||
``PyObject_GetAttrString(frame, "f_locals")`` instead.
|
||||
|
||||
In addition to the above documented interfaces, the draft reference
|
||||
implementation also exposes the following undocumented interfaces::
|
||||
|
@ -493,9 +533,30 @@ implementation also exposes the following undocumented interfaces::
|
|||
#define _PyFastLocalsProxy_CheckExact(self) \
|
||||
(Py_TYPE(self) == &_PyFastLocalsProxy_Type)
|
||||
|
||||
This type is what the reference implementation actually stores in ``f_locals``
|
||||
for optimized frames (i.e. when ``PyFrame_GetLocalsReturnsCopy()`` returns
|
||||
true).
|
||||
This type is what the reference implementation actually returns from
|
||||
``PyObject_GetAttrString(frame, "f_locals")`` for optimized frames (i.e.
|
||||
when ``PyFrame_GetLocalsReturnsCopy()`` returns true).
|
||||
|
||||
|
||||
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,
|
||||
this PEP incorporate's Victor Stinner's proposal to no longer implicitly call
|
||||
``PyFrame_FastToLocalsWithError()`` before calling trace hooks implemented in
|
||||
Python.
|
||||
|
||||
Code using the new frame view APIs won't need the dynamic locals snapshot
|
||||
refreshed, while code using the ``PyEval_GetLocals()`` API will implicitly
|
||||
refresh it when making that call.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
Design Discussion
|
||||
|
@ -660,6 +721,24 @@ emulation of CPython's frame API is already an opt-in flag in some Python
|
|||
implementations).
|
||||
|
||||
|
||||
Dropping support for storing additional data on optimised frames
|
||||
----------------------------------------------------------------
|
||||
|
||||
Earlier iterations of this PEP proposed preserving the ability to store
|
||||
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.
|
||||
|
||||
While that property has been retained for the historical ``PyEval_GetLocals()``
|
||||
C API, it has been dropped from the new fast locals proxy proposal in order to
|
||||
simplify the semantics and implementation.
|
||||
|
||||
Note: if this change proves problematic in practice, it would be reasonably
|
||||
straightforward to amend the implementation to store unknown keys in the C level
|
||||
``f_locals`` mapping, the same way ``PyEval_GetLocals()`` allows. However,
|
||||
starting the new API with it disallowed offers the best chance of potentially
|
||||
being able to deprecate and remove the behaviour entirely in the future.
|
||||
|
||||
|
||||
Historical semantics at function scope
|
||||
--------------------------------------
|
||||
|
||||
|
@ -751,6 +830,10 @@ PEP that attempted to avoid introducing such a proxy.
|
|||
Thanks to Steve Dower and Petr Viktorin for asking that more attention be paid
|
||||
to the developer experience of the proposed C API additions [8]_.
|
||||
|
||||
Thanks to Mark Shannon for pushing for further simplification of the C level
|
||||
API and semantics (and restarting discussion on the PEP in early 2021 after a
|
||||
few years of inactivity).
|
||||
|
||||
|
||||
References
|
||||
==========
|
||||
|
@ -779,6 +862,8 @@ References
|
|||
.. [8] Discussion of more intentionally designed C API enhancements
|
||||
(https://discuss.python.org/t/pep-558-defined-semantics-for-locals/2936/3)
|
||||
|
||||
.. [9] Disable automatic update of frame locals during tracing
|
||||
(https://bugs.python.org/issue42197)
|
||||
|
||||
Copyright
|
||||
=========
|
||||
|
|
Loading…
Reference in New Issue