PEP 695: Incorporate feedback (#2706)

Incorporated changes from latest round of feedback, and describe new implementation strategy.

Co-authored-by: Eric Traut <erictr@microsoft.com>
This commit is contained in:
Eric Traut 2022-07-11 21:04:34 -07:00 committed by GitHub
parent ce65237e30
commit eba9adb2d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 111 additions and 38 deletions

View File

@ -91,10 +91,10 @@ equivalent.
Type variables defined within the global scope also need to be given a name
that starts with an underscore to indicate that the variable is private to
the module. Globally-defined type variables are also often given names to
indicate their variance leading to cumbersome names like "_T_contra" and
indicate their variance, leading to cumbersome names like "_T_contra" and
"_KT_co". The current mechanisms for allocating type variables also requires
the developer to supply a redundant name in quotes (e.g. ``T = TypeVar("T")``).
This PEP eliminates the need for the redundant name and the cumbersome
This PEP eliminates the need for the redundant name and cumbersome
variable names.
Defining type parameters today requires importing the ``TypeVar`` and
@ -225,6 +225,20 @@ time.
def func2[T](T): ... # Syntax Error
Class type parameter names are not mangled if they begin with a double
underscore. Mangling would not make sense because type parameters, unlike other
class-scoped variables, cannot be accessed through the class dictionary, and
the notion of a "private" type parameter doesn't make sense. Other class-scoped
variables are mangled if they begin with a double underscore, so the mangled
name is used to determine whether there is a name collision with type parameters.
::
class ClassA[__T, _ClassA__S]:
__T = 0 # OK
__S = 0 # Syntax Error (because mangled name is _ClassA__S)
Type Parameter Scopes
---------------------
@ -393,7 +407,8 @@ At runtime, a ``type`` statement will generate an instance of
include:
* ``__name__`` is a str representing the name of the type alias
* ``__parameters__`` is a tuple of ``TypeVar``, ``TypeVarTuple``, or ``ParamSpec`` objects
* ``__parameters__`` is a tuple of ``TypeVar``, ``TypeVarTuple``, or
``ParamSpec`` objects that parameterize the type alias if it is generic
* ``__value__`` is the evaluated value of the type alias
The ``__value__`` attribute initially has a value of ``None`` while the type
@ -606,47 +621,47 @@ that includes a list of type parameters associated with the function or class.
Compiler Changes
----------------
The compiler maintains a list of "active type parameters" as it recursively
The compiler maintains a list of "active type variables" as it recursively
generates byte codes for the program. Consider the following example.
::
class Outer[K, V]:
# Active type parameters are K and V
# Active type variables are K and V
class Inner[T]:
# Active type parameters are K, V, and T
# Active type variables are K, V, and T
def method[M](self, a: M) -> M:
# Active type parameters are K, V, T, and M
# Active type variables are K, V, T, and M
...
An active type parameter symbol cannot be used for other purposes within
An active type variable symbol cannot be used for other purposes within
these scopes. This includes local parameters, local variables, variables
bound from other scopes (nonlocal or global), or other type parameters. An
attempt to reuse a type parameter name in one of these manners results in
attempt to reuse a type variable name in one of these manners results in
a syntax error.
::
class ClassA[K, V]:
class Inner[K]: # Syntax error: K already in use as type parameter
class Inner[K]: # Syntax error: K already in use as type variable
...
class ClassB[K, V]:
def method(self, K): # Syntax error: K already in use as type parameter
def method(self, K): # Syntax error: K already in use as type variable
...
class ClassC[T, T]: # Syntax error: T already in use as type parameter
class ClassC[T, T]: # Syntax error: T already in use as type variable
...
def func1[T]():
...
A type parameter is considered "active" when compiling the arguments for
A type variable is considered "active" when compiling the arguments for
a class declaration, the type annotations for a function declaration, and
the right-hand expression in a type alias declaration. Type parameters are
the right-hand expression in a type alias declaration. Type variable are
not considered "active" when compiling the default argument expressions for
a function declaration or decorator expressions for classes or functions.
@ -655,39 +670,67 @@ a function declaration or decorator expressions for classes or functions.
T = list
@decorator(T) # T in decorator refers to outer variable
class ClassA[T](Base[T], metaclass=Meta[T]) # T refers to type parameter
class ClassA[T](Base[T], metaclass=Meta[T]) # T refers to type variable
...
@decorator(T) # T in decorator refers to outer variable
def func1[T](a: list[T]) -> T: # T refers to type parameter
def func1[T](a: list[T]) -> T: # T refers to type variable
...
def func2[T](a = T): # T in default refers to outer variable
...
When a type parameter is referenced, the compiler generates the byte codes
to construct a ``typing.TypeParameter`` object. This new class can be thought
of as a proxy for ``TypeVar``, ``TypeVarTuple`` or ``ParamSpec``. It has a
name and ``args`` and ``kwargs`` properties (required for ``ParamSpec``) so
it can be used within a type expression at runtime. It also implements a
``__or__`` and ``__ror__`` method so it can be used in unions, an ``__iter__``
method so it can be unpacked (needed for ``TypeVarTuple``) and a ``__repr__``
method so its name can be printed. It does not allow for runtime differentiation
between a ``TypeVar``, ``TypeVarTuple`` or ``ParamSpec``, nor does it
allow for runtime introspection of the upper bound, constraints, or
variance.
When a ``class``, ``def``, or ``type`` statement includes one or more type
parameters, the compiler emits byte codes to construct the corresponding
``typing.TypeVar``, ``typing.TypeVarTuple``, or ``typing.ParamSpec`` instances.
It then builds a new tuple that includes all active type variables and stores
this new tuple in a local variable. Active type variables include all type
parameters declared by outer ``class`` and ``def`` scopes plus those declared
by the ``class``, ``def``, or ``type`` statement itself. (In the reference
implementation, the local variable happens to have the name
``__type_variables__``, but this is an implementation detail. Other Python
compilers or future versions of the CPython compiler may choose a different
name or an entirely anonymous local variable slot for this purpose.)
If a bound expression is provided for a type parameter, it is validated for
syntax but is not evaluated at runtime. That is, no byte codes are generated
for the bound expression. Type checkers should evaluate its type statically.
When a type variable is referenced, the compiler generates opcodes that
load the active type variable tuple from either the local variable or (if
there are no local type variables) through the use of a new opcode called
``LOAD_TYPEVARS`` that loads the tuple of active type variables from the
current function object. It then emits opcodes to index into this tuple to
fetch the desired type variable.
When a new function is created, the "active type variables" tuple is copied
to the C struct field ``func_typevars`` of the function object, making the type
variables from outer scopes available to inner scopes of the function or class.
A new read-only attribute called ``__type_variables__`` is available on class,
function, and type alias objects. This attribute is a tuple of the active
type variables that are visible within the scope of that class, function,
or type alias. This attribute is used for runtime evaluation of stringified
(forward referenced) type annotations that include references to type
parameters. Functions like ``typing.get_type_hints`` can use this attribute
to populate the ``locals`` dictionary with values for type parameters that
are in scope when calling ``eval`` to evaluate the stringified expression.
Library Changes
---------------
Several classes in the ``typing`` module that are currently implemented in
Python must be reimplemented in C. This includes: ``TypeVar``,
``TypeVarTuple``, ``ParamSpec``, and ``Generic``. The new class
``TypeAliasType`` (described above) also must be implemented in C.
The ``typing.get_type_hints`` must be updated to use the new
``__type_variables__`` attribute.
Reference Implementation
========================
This proposal is prototyped in the CPython code base in
`this fork <https://github.com/erictraut/cpython/tree/type_param_syntax>`_.
`this fork <https://github.com/erictraut/cpython/tree/type_param_syntax2>`_.
The Pyright type checker supports the behavior described in this PEP.
@ -712,12 +755,16 @@ function decorators, which are common in Python. Only one other popular
programming language, C++, uses this approach.
Extends Keyword
---------------
We considered introducing an "extends" keyword for specifying the upper bound
of a type parameter. This is consistent with Java and TypeScript, two other
popular languages. However, "extends" didn't feel very Pythonic. It also didn't
lend itself to supporting constrained type parameters.
Bounds Syntax
-------------
We explored various syntactic options for specifying the bounds and constraints
for a type variable. We considered, but ultimately rejected, the use
of a ``<:`` token like in Scala, the use of an ``extends`` or ``with``
keyword like in various other languages, and the use of a function call
syntax similar to today's ``typing.TypeVar`` constructor. The simple colon
syntax is consistent with many other programming languages (see Appendix A),
and it was heavily preferred by a cross section of Python developers who
were surveyed.
Explicit Variance
@ -740,6 +787,19 @@ compiler. This mangled name would be based on the qualified name of the
generic class, function or type alias it was associated with. This approach
was rejected because qualified names are not necessarily unique, which means
the mangled name would need to be based on some other randomized value.
Furthermore, this approach is not compatible with techniques used for
evaluating quoted (forward referenced) type annotations.
Lambda Lifting
--------------
When considering implementation options, we considered introducing a new
scope and executing the ``class``, ``def``, or ``type`` statement within
a lambda -- a technique that is sometimes referred to as "lambda lifting".
We ultimately rejected this idea because it did not work well for statements
within a class body (because class-scoped symbols cannot be accessed by
inner scopes). It also introduced many odd behaviors for scopes that were
further nested within the lambda.
Appendix A: Survey of Type Parameter Syntax
@ -748,7 +808,10 @@ Appendix A: Survey of Type Parameter Syntax
Support for generic types is found in many programming languages. In this
section, we provide a survey of the options used by other popular programming
languages. This is relevant because familiarity with other languages will
make it easier for Python developers to understand this concept.
make it easier for Python developers to understand this concept. We provide
additional details here (for example, default type argument support) that
may be useful when considering future extensions to the Python type system.
C++
---
@ -1098,6 +1161,16 @@ Summary
+------------+----------+---------+--------+----------+-----------+-----------+
Acknowledgements
================
Thanks to Sebastian Rittau for kick-starting the discussions that led to this
proposal, to Jukka Lehtosalo for proposing the syntax for type alias
statements and to Jelle Zijlstra, Daniel Moisset, and Guido van Rossum
for their valuable feedback and suggested improvements to the specification
and implementation.
Copyright
=========