676 lines
26 KiB
ReStructuredText
676 lines
26 KiB
ReStructuredText
PEP: 649
|
|
Title: Deferred Evaluation Of Annotations Using Descriptors
|
|
Author: Larry Hastings <larry@hastings.org>
|
|
Status: Draft
|
|
Type: Standards Track
|
|
Content-Type: text/x-rst
|
|
Created: 11-Jan-2021
|
|
Post-History: 11-Jan-2021
|
|
|
|
|
|
Abstract
|
|
========
|
|
|
|
As of Python 3.9, Python supports two different behaviors
|
|
for annotations:
|
|
|
|
* original Python semantics, in which annotations are evaluated
|
|
at the time they are bound, and
|
|
* PEP 563 semantics, currently enabled per-module by
|
|
``from __future__ import annotations``, in which annotations
|
|
are converted back into strings and must be parsed by ``eval()``
|
|
to be used.
|
|
|
|
Original Python semantics created a circular references problem
|
|
for static typing analysis. PEP 563 solved that problem, but
|
|
its novel semantics introduced new problems.
|
|
|
|
This PEP proposes a third way that embodies the best of both
|
|
previous approaches. It solves the same circular reference
|
|
problems solved by PEP 563, while preserving Python's original
|
|
straightforward runtime semantics for annotations.
|
|
|
|
In this new approach, the code to generate the annotations
|
|
dict is written to its own callable, and ``__annotations__``
|
|
is a "data descriptor" which calls the callable once and
|
|
preserves the result.
|
|
|
|
If accepted, these new semantics for annotations would initially
|
|
be gated behind ``from __future__ import co_annotations``. However,
|
|
these semantics would eventually be promoted to be the default behavior.
|
|
Thus this PEP would *supersede* PEP 563, and PEP 563's behavior would
|
|
be deprecated and eventually removed.
|
|
|
|
Overview
|
|
========
|
|
|
|
.. note:: The code presented in this section is highly simplified
|
|
for clarity. The intention is to communicate the high-level
|
|
concepts involved without getting lost in with the details.
|
|
The actual details are often quite different. See the
|
|
Implementation_ section later in this PEP for a much more
|
|
accurate description of how this PEP works.
|
|
|
|
Consider this example code::
|
|
|
|
def foo(x: int = 3, y: MyType = None) -> float:
|
|
...
|
|
class MyType:
|
|
...
|
|
foo_y_type = foo.__annotations__['y']
|
|
|
|
As we see here, annotations are available at runtime through an
|
|
``__annotations__`` attribute on functions, classes, and modules.
|
|
When annotations are specified on one of these objects,
|
|
``__annotations__`` is a dictionary mapping the names of the
|
|
fields to the value specified as that field's annotation.
|
|
|
|
The default behavior in Python 3.9 is to evaluate the expressions
|
|
for the annotations, and build the annotations dict, at the time
|
|
the function, class, or module is bound. At runtime the above
|
|
code actually works something like this::
|
|
|
|
annotations = {'x': int, 'y': MyType, 'return': float}
|
|
def foo(x = 3, y = "abc"):
|
|
...
|
|
foo.__annotations__ = annotations
|
|
class MyType:
|
|
...
|
|
foo_y_type = foo.__annotations__['y']
|
|
|
|
The crucial detail here is that the values ``int``, ``MyType``,
|
|
and ``float`` are looked up at the time the function object is
|
|
bound, and these values are stored in the annotations dict.
|
|
But this code doesn't run—it throws a ``NameError`` on the first
|
|
line, because ``MyType`` hasn't been defined yet.
|
|
|
|
PEP 563's solution is to decompile the expressions back
|
|
into strings, and store those *strings* in the annotations dict.
|
|
The equivalent runtime code would look something like this::
|
|
|
|
annotations = {'x': 'int', 'y': 'MyType', 'return': 'float'}
|
|
def foo(x = 3, y = "abc"):
|
|
...
|
|
foo.__annotations__ = annotations
|
|
class MyType:
|
|
...
|
|
foo_y_type = foo.__annotations__['y']
|
|
|
|
This code now runs successfully. However, ``foo_y_type``
|
|
is no longer a reference to ``MyType``, it is the *string*
|
|
``'MyType'``. The code would have to be further modified to
|
|
call ``eval()`` or ``typing.get_type_hints()`` to convert
|
|
the string into a useful reference to the actual ``MyType``
|
|
object.
|
|
|
|
This PEP proposes a third approach, delaying the evaluation of
|
|
the annotations by computing them in their own function. If
|
|
this PEP was active, the generated code would work something
|
|
like this::
|
|
|
|
class function:
|
|
# __annotations__ on a function object is already a
|
|
# "data descriptor" in Python, we're just changing what it does
|
|
@property
|
|
def __annotations__(self):
|
|
return self.__co_annotations__()
|
|
|
|
# ...
|
|
|
|
def foo_annotations_fn():
|
|
return {'x': int, 'y': MyType, 'return': float}
|
|
def foo(x = 3, y = "abc"):
|
|
...
|
|
foo.__co_annotations__ = foo_annotations_fn
|
|
class MyType:
|
|
...
|
|
foo_y_type = foo.__annotations__['y']
|
|
|
|
The important change is that the code constructing the
|
|
annotations dict now lives in a function—here, called
|
|
``foo_annotations__fn()``. But this function isn't called
|
|
until we ask for the value of ``foo.__annotations__``,
|
|
and we don't do that until *after* the definition of ``MyType``.
|
|
So this code also runs successfully, and ``foo_y_type`` now
|
|
has the correct value, the class ``MyType``.
|
|
|
|
|
|
Motivation
|
|
==========
|
|
|
|
Python's original semantics for annotations made its use for
|
|
static type analysis painful due to forward reference problems.
|
|
This was the main justification for PEP 563, and we need not
|
|
revisit those arguments here.
|
|
|
|
However, PEP 563's solution was to decompile code for Python
|
|
annotations back into strings at compile time, requiring
|
|
users of annotations to ``eval()`` those strings to restore
|
|
them to their actual Python values. This has several drawbacks:
|
|
|
|
* It requires Python implementations to stringize their
|
|
annotations. This is surprising behavior—unprecedented
|
|
for a language-level feature. Also, adding this feature
|
|
to CPython was complicated, and this complicated code would
|
|
need to be reimplemented independently by every other Python
|
|
implementation.
|
|
* It requires a code change every time existing code uses an
|
|
annotation, to handle converting the stringized
|
|
annotation back into a useful value.
|
|
* ``eval()`` is slow.
|
|
* ``eval()`` isn't always available; it's sometimes removed
|
|
from Python for space reasons.
|
|
* In order to evaluate the annotations stored with a class,
|
|
it requires obtaining a reference to that class's globals,
|
|
which PEP 563 suggests should be done by looking up that class
|
|
by name in ``sys.modules``—another surprising requirement for
|
|
a language-level feature.
|
|
* It adds an ongoing maintenance burden to Python implementations.
|
|
Every time the language adds a new feature available in expressions,
|
|
the implementation's stringizing code must be updated in
|
|
tandem to support decompiling it.
|
|
|
|
This PEP also solves the forward reference problem outlined in
|
|
PEP 563 while avoiding the problems listed above:
|
|
|
|
* Python implementations would generate annotations as code
|
|
objects. This is simpler than stringizing, and is something
|
|
Python implementations are already quite good at. This means:
|
|
|
|
- alternate implementations would need to write less code
|
|
to implement this feature, and
|
|
- the implementation would be simpler overall, which should
|
|
reduce its ongoing maintenance cost.
|
|
|
|
* Code examining annotations at runtime would no longer need
|
|
to use ``eval()`` or anything else—it would automatically
|
|
get the correct values. This is easier, almost certainly
|
|
faster, and removes the dependency on ``eval()``.
|
|
|
|
|
|
Backwards Compatibility
|
|
=======================
|
|
|
|
PEP 563 changed the semantics of annotations. When its semantics
|
|
are active, annotations must assume they will be evaluated in
|
|
*module-level* scope. They may no longer refer directly
|
|
to local variables or class attributes. This PEP retains that
|
|
semantic change, also requiring that annotations be evaluated in
|
|
*module-level* scope. Thus, code changed so its annotations are
|
|
compatible with PEP 563 should *already* compatible with this
|
|
aspect of this PEP and would not need further change. Modules
|
|
still using stock semantics would have to be revised so its
|
|
annotations evaluate properly in module-level scope, in the same
|
|
way they would have to be to achieve compatibility with PEP 563.
|
|
|
|
PEP 563 also requires using ``eval()`` or ``typing.get_type_hints()``
|
|
to examine annotations. Code updated to work with PEP 563 that calls
|
|
``eval()`` directly would have to be updated simply to remove the
|
|
``eval()`` call. Code using ``typing.get_type_hints()`` would
|
|
continue to work unchanged, though future use of that function
|
|
would become optional in most cases.
|
|
|
|
Because this PEP makes the same backwards-compatible change
|
|
to annotation scoping as PEP 563, this PEP will be initially gated
|
|
with a per-module ``from __future__ import co_annotations``
|
|
before it eventually becomes the default behavior.
|
|
|
|
Apart from these two changes already discussed:
|
|
|
|
* the evaluation of values in annotation dicts will be
|
|
delayed until the ``__annotations__`` attribute is evaluated, and
|
|
* annotations are now evaluated in module-level scope,
|
|
|
|
this PEP preserves nearly all existing behavior of annotations
|
|
dicts. Specifically:
|
|
|
|
* Annotations dicts are mutable, and any changes to them are
|
|
preserved.
|
|
* The ``__annotations__`` attribute can be explicitly set,
|
|
and any value set this way will be preserved.
|
|
* The ``__annotations__`` attribute can be deleted using
|
|
the ``del`` statement.
|
|
|
|
However, there are two uncommon interactions possible with class
|
|
and module annotations that work today—both with stock semantics,
|
|
and with PEP 563 semantics—that would no longer work when this PEP
|
|
was active. These two interactions would have to be prohibited.
|
|
The good news is, neither is common, and neither is considered good
|
|
practice. In fact, they're rarely seen outside of Python's own
|
|
regression test suite. They are:
|
|
|
|
* *Code that sets annotations on module or class attributes
|
|
from inside any kind of flow control statement.* It's
|
|
currently possible to set module and class attributes with
|
|
annotations inside an ``if`` or ``try`` statement, and it works
|
|
as one would expect. It's untenable to support this behavior
|
|
when this PEP is active.
|
|
* *Code in module or class scope that references or modifies the
|
|
local* ``__annotations__`` *dict directly.* Currently, when
|
|
setting annotations on module or class attributes, the generated
|
|
code simply creates a local ``__annotations__`` dict, then sets
|
|
mappings in it as needed. It's also possible for user code
|
|
to directly modify this dict, though this doesn't seem like it's
|
|
an intentional feature. Although it'd be possible to support
|
|
this after a fashion when this PEP was active, the semantics
|
|
would likely be surprising and wouldn't make anyone happy.
|
|
|
|
Note that these are both also pain points for static type checkers,
|
|
and are unsupported by those checkers. It seems reasonable to
|
|
declare that both are at the very least unsupported, and their
|
|
use results in undefined behavior. It might be worth making a
|
|
small effort to explicitly prohibit them with compile-time checks.
|
|
|
|
There's one more idiom that's actually somewhat common when
|
|
dealing with class annotations, and which will become
|
|
more problematic when this PEP is active: code often accesses
|
|
class annotations via ``cls.__dict__.get("__annotations__", {})``
|
|
rather than simply ``cls.__annotations__``. It's due to a flaw
|
|
in the original design of annotations themselves. This topic
|
|
will be examined in a separate discussion; the outcome of
|
|
that discussion will likely guide the future evolution of this
|
|
PEP.
|
|
|
|
|
|
Mistaken Rejection Of This Approach In November 2017
|
|
====================================================
|
|
|
|
During the early days of discussion around PEP 563,
|
|
using code to delay the evaluation of annotations was
|
|
briefly discussed, in a November 2017 thread in
|
|
``comp.lang.python-dev``. At the time the
|
|
technique was termed an "implicit lambda expression".
|
|
|
|
Guido van Rossum—Python's BDFL at the time—replied,
|
|
asserting that these "implicit lambda expression" wouldn't
|
|
work, because they'd only be able to resolve symbols at
|
|
module-level scope:
|
|
|
|
IMO the inability of referencing class-level definitions
|
|
from annotations on methods pretty much kills this idea.
|
|
|
|
https://mail.python.org/pipermail/python-dev/2017-November/150109.html
|
|
|
|
This led to a short discussion about extending lambda-ized
|
|
annotations for methods to be able to refer to class-level
|
|
definitions, by maintaining a reference to the class-level scope.
|
|
This idea, too, was quickly rejected.
|
|
|
|
PEP 563 summarizes the above discussion here:
|
|
|
|
https://www.python.org/dev/peps/pep-0563/#keeping-the-ability-to-use-function-local-state-when-defining-annotations
|
|
|
|
What's puzzling is PEP 563's own changes to the scoping rules
|
|
of annotations—it *also* doesn't permit annotations to reference
|
|
class-level definitions. It's not immediately clear why an
|
|
inability to reference class-level definitions was enough to
|
|
reject using "implicit lambda expressions" for annotations,
|
|
but was acceptable for stringized annotations.
|
|
|
|
In retrospect there was probably a pivot during the development
|
|
of PEP 563. It seems that, early on, there was a prevailing
|
|
assumption that PEP 563 would support references to class-level
|
|
definitions. But by the time PEP 563 was finalized, this
|
|
assumption had apparently been abandoned. And it looks like
|
|
"implicit lambda expressions" were never reconsidered in this
|
|
new light.
|
|
|
|
PEP 563 semantics have shipped in three major Python releases.
|
|
These semantics are now widely used in organizations depending
|
|
on static type analysis. Evaluating annotations at module-level
|
|
scope is clearly acceptable to all interested parties. Therefore,
|
|
delayed evaluation of annotations with code using the same scoping
|
|
rules is obviously also completely viable.
|
|
|
|
|
|
.. _Implementation:
|
|
|
|
Implementation
|
|
==============
|
|
|
|
There's a prototype implementation of this PEP, here:
|
|
|
|
https://github.com/larryhastings/co_annotations/
|
|
|
|
As of this writing, all features described in this PEP are
|
|
implemented, and there are some rudimentary tests in the
|
|
test suite. There are still some broken tests, and the
|
|
repo is many months behind.
|
|
|
|
|
|
from __future__ import co_annotations
|
|
-------------------------------------
|
|
|
|
In the prototype, the semantics presented in this PEP are gated with::
|
|
|
|
from __future__ import co_annotations
|
|
|
|
|
|
|
|
__co_annotations__
|
|
------------------
|
|
|
|
Python supports runtime metadata for annotations for three different
|
|
types: function, classes, and modules. The basic approach to
|
|
implement this PEP is much the same for all three with only minor
|
|
variations.
|
|
|
|
With this PEP, each of these types adds a new attribute,
|
|
``__co_annotations__``, with the following semantics:
|
|
|
|
* ``__co_annotations__`` is always set, and may contain either
|
|
``None`` or a callable.
|
|
* ``__co_annotations__`` cannot be deleted.
|
|
* ``__annotations__`` and ``__co_annotations__`` can't both
|
|
be set to a useful value simultaneously:
|
|
|
|
- If you set ``__annotations__`` to a dict, this also sets
|
|
``__co_annotations__`` to None.
|
|
- If you set ``__co_annotations__`` to a callable, this also
|
|
deletes ``__annotations__``
|
|
|
|
Internally, ``__co_annotations__`` is a "data descriptor",
|
|
where functions are called whenever user code gets, sets,
|
|
or deletes the attribute. In all three cases, the object
|
|
has a separate internal place to store the current value
|
|
of the ``__co_annotations__`` attribute.
|
|
|
|
``__annotations__`` is also reimplemented as a data descriptor,
|
|
with its own separate internal storage for its internal value.
|
|
The code implementing the "get" for ``__annotations__`` works
|
|
something like this::
|
|
|
|
if (the internal value is set)
|
|
return the internal annotations dict
|
|
if (__co_annotations__ is not None)
|
|
call the __co_annotations__ function
|
|
if the result is a dict:
|
|
store the result as the internal value
|
|
set __co_annotations__ to None
|
|
return the internal value
|
|
do whatever this object does when there are no annotations
|
|
|
|
|
|
Unbound code objects
|
|
--------------------
|
|
|
|
When Python code defines one of these three objects with
|
|
annotations, the Python compiler generates a separate code
|
|
object which builds and returns the appropriate annotations
|
|
dict. The "annotation code object" is then stored *unbound*
|
|
as the internal value of ``__co_annotations__``; it is then
|
|
bound on demand when the user asks for ``__annotations__``.
|
|
|
|
This is an important optimization, for both speed and
|
|
memory consumption. Python processes rarely examine
|
|
annotations at runtime. Therefore, pre-binding these
|
|
code objects to function objects would be a waste of
|
|
resources in nearly all cases.
|
|
|
|
Note that user code isn't permitted to see these unbound code
|
|
objects. If the user gets the value of ``__co_annotations__``,
|
|
and the internal value of ``__co_annotations__`` is an unbound
|
|
code object, it is bound, and the resulting function object is
|
|
stored as the new value of ``__co_annotations__``.
|
|
|
|
|
|
The annotations function
|
|
------------------------
|
|
|
|
Annotations functions take no arguments and
|
|
must return either None or a dict (or subclass of dict).
|
|
|
|
The bytecode generated for annotations code objects
|
|
always uses the ``BUILD_CONST_KEY_MAP`` opcode to build the
|
|
dict. Stock and PEP 563 semantics only uses this bytecode
|
|
for function annotations; for class and module annotations,
|
|
they generate a longer and slightly-less-efficient stanza
|
|
of bytecode.
|
|
|
|
Also, when generating the bytecode for an annotations code
|
|
object, all ``LOAD_*`` opcodes are forced to be ``LOAD_GLOBAL``.
|
|
|
|
|
|
Function Annotations
|
|
--------------------
|
|
|
|
When compiling a function, the CPython bytecode compiler
|
|
visits the annotations for the function all in one place,
|
|
starting with ``compiler_visit_annotations()``. If there
|
|
are any annotations, they create the scope for the annotations
|
|
function on demand, and ``compiler_visit_annotations()``
|
|
assembles it.
|
|
|
|
The code object is passed in in place of the
|
|
annotations dict for the ``MAKE_FUNCTION`` bytecode.
|
|
``MAKE_FUNCTION`` supports a new bit in its oparg
|
|
bitfield, ``0x10``, which tells it to expect a
|
|
``co_annotations`` code object on the stack.
|
|
The bitfields for ``annotations`` (``0x04``) and
|
|
``co_annotations`` (``0x10``) are mutually exclusive.
|
|
|
|
When binding an unbound annotation code object, a function will
|
|
use its own ``__globals__`` as the new function's globals.
|
|
|
|
One quirk of Python: you can't actually remove the annotations
|
|
from a function object.
|
|
If you delete the ``__annotations__`` attribute of a function,
|
|
then get its ``__annotations__`` member,
|
|
it will create an empty dict and use that as its
|
|
``__annotations__``. Naturally the implementation of this
|
|
PEP maintains this quirk.
|
|
|
|
|
|
Class Annotations
|
|
-----------------
|
|
|
|
When compiling a class body, the compiler maintains two scopes:
|
|
one for the normal class body code, and one for annotations.
|
|
(This is facilitated by four new functions: ``compiler.c``
|
|
adds ``compiler_push_scope()`` and ``compiler_pop_scope()``,
|
|
and ``symtable.c`` adds ``symtable_push_scope()`` and
|
|
``symtable_pop_scope()``.)
|
|
Once the code generator reaches the end of the class body,
|
|
but before it generates the bytecode for the class body,
|
|
it assembles the bytecode for ``__co_annotations__``, then
|
|
assigns that to ``__co_annotations__`` using ``STORE_NAME``.
|
|
|
|
It also sets a new ``__globals__`` attribute. Currently it
|
|
does this by calling ``globals()`` and storing the result.
|
|
(Surely there's a more elegant way to find the class's
|
|
globals--but this was good enough for the prototype.) When
|
|
binding an unbound annotation code object, a class will use
|
|
the value of this ``__globals__`` attribute. When the class
|
|
drops its reference to the unbound code object--either because
|
|
it has bound it to a function, or because ``__annotations__``
|
|
has been explicitly set--it also deletes its ``__globals__``
|
|
attribute.
|
|
|
|
As discussed above, examination or modification of
|
|
``__annotations__`` from within the class body is no
|
|
longer supported. Also, any flow control (``if`` or ``try`` blocks)
|
|
around declarations of members with annotations is unsupported.
|
|
|
|
If you delete the ``__annotations__`` attribute of a class,
|
|
then get its ``__annotations__`` member, it will return the
|
|
annotations dict of the first base class with annotations set.
|
|
If no base classes have annotations set, it will raise
|
|
``AttributeError``.
|
|
|
|
Although it's an implementation-specific detail, currently
|
|
classes store the internal value of ``__co_annotations__``
|
|
in their ``tp_dict`` under the same name.
|
|
|
|
|
|
Module Annotations
|
|
------------------
|
|
|
|
Module annotations work much the same as class annotations.
|
|
The main difference is, a module uses its own dict as the
|
|
``__globals__`` when binding the function.
|
|
|
|
If you delete the ``__annotations__`` attribute of a class,
|
|
then get its ``__annotations__`` member,
|
|
the module will raise ``AttributeError``.
|
|
|
|
|
|
Interactive REPL Shell
|
|
----------------------
|
|
|
|
Everything works the same inside Python's interactive REPL shell,
|
|
except for module annotations in the interactive module (``__main__``)
|
|
itself. Since that module is never "finished", there's no specific
|
|
point where we can compile the ``__co_annotations__`` function.
|
|
|
|
For the sake of simplicity, in this case we forego delayed evaluation.
|
|
Module-level annotations in the REPL shell will continue to work
|
|
exactly as they do today, evaluating immediately and setting the
|
|
result directly inside the ``__annotations__`` dict.
|
|
|
|
(It might be possible to support delayed evaluation here.
|
|
But it gets complicated quickly, and for a nearly-non-existent
|
|
use case.)
|
|
|
|
|
|
Local Annotations Inside Functions
|
|
----------------------------------
|
|
|
|
Python supports syntax for local variable annotations inside
|
|
functions. However, these annotations have no runtime effect.
|
|
Thus this PEP doesn't need to do anything to support them.
|
|
|
|
|
|
Performance
|
|
-----------
|
|
|
|
Performance with this PEP should be favorable. In general,
|
|
resources are only consumed on demand—"you only pay for what you use".
|
|
|
|
There are three scenarios to consider:
|
|
|
|
* the runtime cost when annotations aren't defined,
|
|
* the runtime cost when annotations are defined but *not* referenced, and
|
|
* the runtime cost when annotations are defined *and* referenced.
|
|
|
|
We'll examine each of these scenarios in the context of all three
|
|
semantics for annotations: stock, PEP 563, and this PEP.
|
|
|
|
When there are no annotations, all three semantics have the same
|
|
runtime cost: zero. No annotations dict is created and no code is
|
|
generated for it. This requires no runtime processor time and
|
|
consumes no memory.
|
|
|
|
When annotations are defined but not referenced, the runtime cost
|
|
of Python with this PEP should be slightly faster than either
|
|
original Python semantics or PEP 563 semantics. With those, the
|
|
annotations dicts are built but never examined; with this PEP,
|
|
the annotations dicts won't even be built. All that happens at
|
|
runtime is the loading of a single constant (a simple code
|
|
object) which is then set as an attribute on an object. Since
|
|
the annotations are never referenced, the code object is never
|
|
bound to a function, the code to create the dict is never
|
|
executed, and the dict is never constructed.
|
|
|
|
When annotations are both defined and referenced, code using
|
|
this PEP should be much faster than code using PEP 563 semantics,
|
|
and roughly the same as original Python semantics. PEP 563
|
|
semantics requires invoking ``eval()`` for every value inside
|
|
an annotations dict, which is much slower. And, as already
|
|
mentioned, this PEP generates more efficient bytecode for class
|
|
and module annotations than either stock or PEP 563 semantics.
|
|
|
|
Memory use should also be comparable in all three scenarios across
|
|
all three semantic contexts. In the first and third scenarios,
|
|
memory usage should be roughly equivalent in all cases.
|
|
In the second scenario, when annotations are defined but not
|
|
referenced, using this PEP's semantics will mean the
|
|
function/class/module will store one unused code object; with
|
|
the other two semantics, they'll store one unused dictionary.
|
|
|
|
|
|
For Future Discussion
|
|
=====================
|
|
|
|
__globals__
|
|
-----------
|
|
|
|
Is it permissible to add the ``__globals__`` reference to class
|
|
objects as proposed here? It's not clear why this hasn't already
|
|
been done; PEP 563 could have made use of class globals, but instead
|
|
makes do with looking up classes inside ``sys.modules``. Yet Python
|
|
seems strangely allergic to adding a ``__globals__`` reference to
|
|
class objects.
|
|
|
|
If adding ``__globals__`` to class objects is indeed a bad idea
|
|
(for reasons I don't know), here are two alternatives as to
|
|
how classes could get a reference to their globals for the
|
|
implementation of this PEP:
|
|
|
|
* The generate code for a class could bind its annotations code
|
|
object to a function at the time the class is bound, rather than
|
|
waiting for ``__annotations__`` to be referenced, making them an
|
|
exception to the rule (even though "special cases aren't special
|
|
enough to break the rules"). This would result in a small
|
|
additional runtime cost when annotations were defined but not
|
|
referenced on class objects. Honestly I'm more worried about
|
|
the lack of symmetry in semantics. (But I wouldn't want to
|
|
pre-bind all annotations code objects, as that would become
|
|
much more costly for function objects, even as annotations are
|
|
rarely used at runtime.)
|
|
* Use the class's ``__module__`` attribute to look up its module
|
|
by name in ``sys.modules``. This is what PEP 563 advises.
|
|
While this is passable for userspace or library code, it seems
|
|
like a little bit of a code smell for this to be defined semantics
|
|
baked into the language itself.
|
|
|
|
Also, the prototype gets globals for class objects by calling
|
|
``globals()`` then storing the result. I'm sure there's a much
|
|
faster way to do this, I just didn't know what it was when I was
|
|
prototyping. I'm sure we can revise this to something much faster
|
|
and much more sanitary. I'd prefer to make it completely internal
|
|
anyway, and not make it visible to the user (via this new
|
|
__globals__ attribute). There's possibly already a good place to
|
|
put it anyway--``ht_module``.
|
|
|
|
|
|
Bikeshedding the name
|
|
---------------------
|
|
|
|
During most of the development of this PEP, user code actually
|
|
could see the raw annotation code objects. ``__co_annotations__``
|
|
could only be set to a code object; functions and other callables
|
|
weren't permitted. In that context the name ``co_annotations``
|
|
makes a lot of sense. But with this last-minute pivot where
|
|
``__co_annotations__`` now presents itself as a callable,
|
|
perhaps the name of the attribute and the name of the
|
|
``from __future__ import`` needs a re-think.
|
|
|
|
|
|
Acknowledgements
|
|
================
|
|
|
|
Thanks to Barry Warsaw, Eric V. Smith, and Mark Shannon
|
|
for feedback and encouragement. Thanks in particular to
|
|
Mark Shannon for two key suggestions—build the entire
|
|
annotations dict inside a single code object, and only
|
|
bind it to a function on demand—that quickly became
|
|
among the best aspects of this proposal.
|
|
|
|
|
|
Copyright
|
|
=========
|
|
|
|
This document is placed in the public domain or under the
|
|
CC0-1.0-Universal license, whichever is more permissive.
|
|
|
|
|
|
|
|
..
|
|
Local Variables:
|
|
mode: indented-text
|
|
indent-tabs-mode: nil
|
|
sentence-end-double-space: t
|
|
fill-column: 70
|
|
coding: utf-8
|
|
End:
|