PEP 646: First draft (#1740)
(This draft will need some work before it is acceptable, see my message to typing-sig and my comments in the Google Doc. But I'd like to claim the PEP number.)
This commit is contained in:
parent
4e5abc4ca4
commit
c86d1cc89d
|
@ -0,0 +1,729 @@
|
|||
PEP: 0646
|
||||
Title: Variadic Generics
|
||||
Author: Mark Mendoza <mendoza.mark.a@gmail.com>,
|
||||
Matthew Rahtz <mrahtz@google.com>,
|
||||
Vincent Siles <vsiles@fb.com>
|
||||
Sponsor: Guido van Rossum <guido@python.org>
|
||||
Status: Draft
|
||||
Type: Standards Track
|
||||
Content-Type: text/x-rst
|
||||
Created: 16-Sep-2020
|
||||
Python-Version: 3.10
|
||||
Post-History: 07-Oct-2020
|
||||
|
||||
Abstract
|
||||
========
|
||||
|
||||
PEP 484 introduced ``TypeVar``, enabling creation of generics parameterised
|
||||
with a single type. In this PEP, we introduce ``TypeTuple``, enabling parameterisation
|
||||
with an *arbitrary* number of types - that is, a *variadic* type variable,
|
||||
enabling *variadic* generics. This allows the type of array-like structures
|
||||
in numerical computing libraries such as NumPy and TensorFlow to be
|
||||
parameterised with the array *shape*, enabling static type checkers
|
||||
to catch shape-related bugs in code that uses these libraries.
|
||||
|
||||
Motivation
|
||||
==========
|
||||
|
||||
There are two main use-cases for variadic generics.
|
||||
|
||||
The primary motivation is to enable typing of array shapes in numerical
|
||||
computing libraries such as NumPy and TensorFlow. This is the use-case
|
||||
much of the PEP will focus on.
|
||||
|
||||
Additionally, variadic generics allow us to concisely specify the type
|
||||
signature of ``map`` and ``zip``.
|
||||
|
||||
We discuss each of these three motivations below.
|
||||
|
||||
Array Shapes
|
||||
-------------
|
||||
|
||||
In the context of numerical computation with libraries such as NumPy and
|
||||
TensorFlow, the *shape* of arguments is often just as important as the
|
||||
argument *type*. For example, consider the following function which converts a
|
||||
batch [#batch]_ of videos to grayscale:
|
||||
|
||||
::
|
||||
|
||||
def to_gray(videos: Tensor): ...
|
||||
|
||||
From the signature alone, it is not obvious what shape of array [#array]_
|
||||
we should pass for the ``videos`` argument. Possibilities include, for
|
||||
example,
|
||||
|
||||
batch × time × height × width × channels
|
||||
|
||||
and
|
||||
|
||||
time × batch × channels × height × width. [#timebatch]_
|
||||
|
||||
Ideally, we should have some way of making the required shape clear in the
|
||||
signature itself. Multiple proposals [#numeric-stack]_ [#typing-ideas]_
|
||||
[#syntax-proposal]_ have suggested the use of the standard generics syntax for
|
||||
this purpose. We would write:
|
||||
|
||||
::
|
||||
|
||||
def to_gray(videos: Tensor[Time, Batch, Height, Width, Channels]): ...
|
||||
|
||||
However, note that arrays can be of arbitrary rank - ``Tensor`` as used above is
|
||||
generic in an arbitrary number of axes. One way around this would be to use a different
|
||||
``Tensor`` class for each rank...
|
||||
|
||||
::
|
||||
|
||||
Axis1 = TypeVar('Axis1')
|
||||
Axis2 = TypeVar('Axis2')
|
||||
|
||||
class Tensor1(Generic[Axis1]): ...
|
||||
|
||||
class Tensor2(Generic[Axis1, Axis2]): ...
|
||||
|
||||
...but this would be cumbersome, both for users (who would have to sprinkle 1s and 2s
|
||||
and so on throughout their code) and for the authors of tensor libraries (who would have to duplicate implementations throughout multiple classes).
|
||||
|
||||
Variadic generics are necessary for a ``Tensor`` that is generic in an arbitrary
|
||||
number of axes to be cleanly defined as a single class.
|
||||
|
||||
``map`` and ``zip``
|
||||
-------------------
|
||||
|
||||
PEP 612 [#pep-612]_ introduced ``ParamSpec``, enabling parameter types of one
|
||||
callable to be forwarded to another callable. However, in many cases we actually
|
||||
wish to *transform* parameter types before using them elsewhere in the
|
||||
signature.
|
||||
|
||||
Consider, for example, the signature of ``map`` for a particular choice of
|
||||
function and iterables:
|
||||
|
||||
::
|
||||
|
||||
def func(int, str) -> float: ...
|
||||
iter1: List[int]
|
||||
iter2: List[str]
|
||||
|
||||
def map(func: Callable[[int, str], float],
|
||||
iter1: Iterable[int],
|
||||
iter2: Iterable[str]) -> Iterable[float]: ...
|
||||
|
||||
Note that the number of iterables passed to ``map`` is dependent
|
||||
on the supplied function, and that the types of those iterables
|
||||
must correspond to the types of supplied function's arguments.
|
||||
|
||||
A similar example is ``zip``:
|
||||
|
||||
::
|
||||
|
||||
iter1: List[int]
|
||||
iter2: List[str]
|
||||
|
||||
def zip(iter1: Iterable[int],
|
||||
iter2: Iterable[str]) -> Iterable[Tuple[int, str]]: ...
|
||||
|
||||
Neither of these signatures can be specified in the general case using
|
||||
existing typing mechanisms. The signature of ``zip``, for example, is
|
||||
currently specified [#zip-sig]_ with a number of overloads.
|
||||
|
||||
Specification
|
||||
=============
|
||||
|
||||
In order to support the above use-cases, we introduce:
|
||||
|
||||
* ``TypeTupleVar``, a ``TypeVar`` that acts as a placeholder not for a single
|
||||
type but for an *arbitrary* number of types.
|
||||
* A new syntax for parameterizing generic functions and classes using a
|
||||
type tuple variable.
|
||||
* Two new type operators, ``Apply`` and ``Map``.
|
||||
|
||||
These are described in detail below.
|
||||
|
||||
Type Tuple Variables
|
||||
--------------------
|
||||
|
||||
In the same way that a normal type variable is a stand-in for a single type,
|
||||
a type *tuple* variable is a stand-in for an arbitrary number of types in a flat
|
||||
ordered list.
|
||||
|
||||
Type tuple variables are created with:
|
||||
|
||||
::
|
||||
|
||||
from typing import TypeTupleVar
|
||||
|
||||
Ts = TypeTupleVar('Ts')
|
||||
|
||||
A type tuple variable behaves in a similar way to a parameterized ``Tuple``.
|
||||
For example, in a generic object instantiated with type parameters
|
||||
``int`` and ``str``, ``Ts`` behaves similarly to ``Tuple[int, str]``.
|
||||
|
||||
Parameterizing Types: Star Operator
|
||||
'''''''''''''''''''''''''''''''''''
|
||||
|
||||
One use of type tuple variables are to parameterize variadic types
|
||||
such as ``Tuple``.
|
||||
|
||||
To differentiate type tuple variables from normal type variables, we introduce
|
||||
a new use for the star operator:
|
||||
|
||||
::
|
||||
|
||||
Tuple[*Ts]
|
||||
|
||||
The star operator here serves to 'expand' the type tuple into
|
||||
its component types. For example, in a generic object instantiated
|
||||
with ``Ts`` being ``int`` and ``str``, then ``Tuple[*Ts]`` would
|
||||
be equivalent to ``Tuple[int, str]``.
|
||||
|
||||
For consistency, the star operator can also be applied directly to a
|
||||
parameterised ``Tuple``:
|
||||
|
||||
::
|
||||
|
||||
Types = Tuple[int, str, bool, float, double]
|
||||
Tuple[*Types] # Also valid
|
||||
|
||||
|
||||
Parameterizing Types: ``Expand``
|
||||
''''''''''''''''''''''''''''''''
|
||||
|
||||
Because the new use of the star operator requires a syntax change and is
|
||||
therefore incompatible with previous versions of Python, we also introduce the
|
||||
``Expand`` type operator for use in existing versions of Python. ``Expand``
|
||||
behaves identically to the star operator, but without requiring a syntax change.
|
||||
In any place you would normally write ``*Ts``, you can also write ``Expand[Ts]``.
|
||||
|
||||
Parameterizing Function Signatures and Classes
|
||||
''''''''''''''''''''''''''''''''''''''''''''''
|
||||
|
||||
Type tuple variables can be used anywhere a normal ``TupleVar`` can. For example,
|
||||
in class definitions, function signatures, and variable annotations:
|
||||
|
||||
::
|
||||
|
||||
Shape = TypeTupleVar('Shape')
|
||||
|
||||
class Tensor(Generic[*Shape]):
|
||||
|
||||
def __init__(self, shape: Tuple[int, ...]):
|
||||
self.shape: Shape = shape
|
||||
|
||||
def __abs__(self) -> Tensor[*Shape]: ...
|
||||
|
||||
def __add__(self, other: Tensor[*Shape]) -> Tensor[*Shape]: ...
|
||||
|
||||
class Height: pass
|
||||
class Width: pass
|
||||
x: Tensor[Height, Width] = Tensor(shape=(640, 480))
|
||||
x.shape # Inferred type is Tuple[Height, Width]
|
||||
y = abs(x) # Tensor[Height, Width]
|
||||
z = x + y # Tensor[Height, Width]
|
||||
|
||||
Unexpanded Type Tuple Variables
|
||||
'''''''''''''''''''''''''''''''
|
||||
|
||||
Until now, we have always expanded type tuple variables.
|
||||
However, type tuple variables can also be used without being expanded.
|
||||
When used in this way, the type tuple variable behaves like a
|
||||
``Tuple`` parameterised by the types that the type tuple variable
|
||||
is bound to. That is:
|
||||
|
||||
::
|
||||
|
||||
def foo(x: Tuple[*Ts]) -> Tuple[*Ts]: ...
|
||||
# could also be written as
|
||||
def foo(x: Ts) -> Ts: ...
|
||||
|
||||
Type tuple variables can also be used unexpanded in in the context
|
||||
of generic classes. However, note that when used in this way,
|
||||
type parameters to the generic class must be explicitly
|
||||
enclosed in a ``Tuple``.
|
||||
|
||||
::
|
||||
|
||||
class Foo(Generic[Ts]): ...
|
||||
|
||||
foo: Foo[Tuple[int, str]]
|
||||
|
||||
See `Concatenating Multiple type tuple Variables`_ below for why this
|
||||
is important.
|
||||
|
||||
|
||||
``*args`` as a Type Tuple Variable
|
||||
''''''''''''''''''''''''''''''''''
|
||||
|
||||
PEP 484 states that when a type annotation is provided for ``*args``, each argument
|
||||
must be of the type annotated. That is, if we specify ``*args`` to be type ``int``,
|
||||
then *all* arguments must be of type ``int``. This limits our ability to specify
|
||||
the type signatures of functions that take heterogeneous argument types.
|
||||
|
||||
If ``*args`` is annotated as being an expanded type tuple variable, however, the
|
||||
types of the individual arguments become the types in the type tuple:
|
||||
|
||||
::
|
||||
|
||||
def args_to_tuple(*args: *Ts) -> Tuple[*Ts]: ...
|
||||
|
||||
args_to_tuple(1, 'a') # Inferred type is Tuple[int, str]
|
||||
|
||||
Inside the body of ``args_to_tuple``, the type of ``args`` is ``Tuple[*Ts]``
|
||||
(with ``*Ts`` substituted for the actual types at runtime).
|
||||
|
||||
Note that, for consistency, the following is also valid syntactically:
|
||||
|
||||
::
|
||||
|
||||
def foo(*args: *Tuple[int, str]): ...
|
||||
|
||||
However, since it is a strange thing to do (why not just specify the arguments
|
||||
directly as ``arg1: int, arg2: str``?), we recommend type checkers
|
||||
emit a warning when coming across such annotations.
|
||||
|
||||
Also note that when a type tuple variable is used in this way, it *must*
|
||||
be in conjunction with the star operator:
|
||||
|
||||
::
|
||||
|
||||
def foo(*args: Ts): ... # NOT valid
|
||||
|
||||
Finally, note that a type tuple variable may *not* be used as the type of
|
||||
``**kwargs``. (We do not yet know of a use-case for this feature, so prefer
|
||||
to leave the ground fresh for a potential future PEP.)
|
||||
|
||||
::
|
||||
|
||||
def foo(**kwargs: *Ts): ... # NOT valid
|
||||
|
||||
``Map``
|
||||
-------
|
||||
|
||||
To enable typing of functions such as ``map`` and ``zip``, we introduce the
|
||||
``Map`` type operator. Not to be confused with the existing operator
|
||||
``typing.Mapping``, ``Map`` is analogous to ``map``, but for types:
|
||||
|
||||
::
|
||||
|
||||
from typing import Map
|
||||
|
||||
def args_to_tuples(*args: *Ts) -> Map[Tuple, Ts]: ...
|
||||
|
||||
args_to_tuples(1, 'a') # Inferred type is Tuple[Tuple[int], Tuple[str]]
|
||||
|
||||
``Map`` takes two operands. The first operand is a parameterizable
|
||||
type (or type alias [#type_aliases]) such as ``Tuple`` or ``List``. The second operand
|
||||
is a type tuple variable or a parameterized ``Tuple`` such as ``Tuple[int, str]``.
|
||||
The result of ``Map`` is a ``Tuple``, where the Nth type in the ``Tuple`` is the
|
||||
first operand parameterized by the Nth type in the second operand.
|
||||
|
||||
Because ``Map`` returns a parameterized ``Tuple``, it can be used anywhere
|
||||
that a type tuple variable would be. For example:
|
||||
|
||||
::
|
||||
|
||||
# Equivalent to 'arg1: List[T1], arg2: List[T2], ...'
|
||||
def foo(*args: *Map[List, Ts]): ...
|
||||
|
||||
# Equivalent to '-> Tuple[List[T1], List[T2], ...]'
|
||||
def bar(*args: *Ts) -> Map[List, Ts]: ...
|
||||
|
||||
bar() # Inferred type is Tuple[()] (an empty tuple)
|
||||
bar(1) # Inferred type is Tuple[List[int]]
|
||||
bar(1, 'a') # Inferred type is Tuple[List[int], List[str]]
|
||||
|
||||
``map`` and ``zip``
|
||||
'''''''''''''''''''
|
||||
|
||||
``Map`` allows us to specify the signature of ``map`` as:
|
||||
|
||||
::
|
||||
|
||||
ArgTs = TypeTupleVar('ArgTs')
|
||||
ReturnT = TypeVar('ReturnT')
|
||||
|
||||
def map(func: Callable[[*ArgTs], ReturnT],
|
||||
*iterables: *Map[Iterable, ArgTs]) -> Iterable[ReturnT]: ...
|
||||
|
||||
def func(int, str) -> float: ...
|
||||
# ArgTs is bound to Tuple[int, str]
|
||||
# Map[Iterable, ArgTs] is Iterable[int], Iterable[str]
|
||||
# Therefore, iter1 must be type Iterable[int],
|
||||
# and iter2 must be type Iterable[str]
|
||||
map(func, iter1, iter2)
|
||||
|
||||
Similarly, we can specify the signature of ``zip`` as:
|
||||
|
||||
::
|
||||
|
||||
def zip(*iterables: *Map[Iterable, ArgTs]) -> Iterable[*ArgTs]): ...
|
||||
|
||||
l1: List[int]
|
||||
l2: List[str]
|
||||
zip(l1, l2) # Iterable[int, str]
|
||||
|
||||
Nesting
|
||||
'''''''
|
||||
|
||||
Because the type of the result of ``Map`` is the same as the type of its second
|
||||
operand, the result of one ``Map`` *can* be used as the input to another ``Map``:
|
||||
|
||||
::
|
||||
|
||||
Map[Tuple, *Map[Tuple, Ts]] # Valid!
|
||||
|
||||
Accessing Individual Types
|
||||
--------------------------
|
||||
|
||||
``Map`` allows us to operate on types in a bulk fashion. For situations where we
|
||||
require access to each individual type, overloads can be used with individual
|
||||
``TypeVar`` instances in place of the type tuple variable:
|
||||
|
||||
::
|
||||
|
||||
Shape = TypeTupleVar('Shape')
|
||||
Axis1 = TypeVar('Axis1')
|
||||
Axis2 = TypeVar('Axis2')
|
||||
Axis3 = TypeVar('Axis3')
|
||||
|
||||
class Tensor(Generic[*Shape]): ...
|
||||
|
||||
@overload
|
||||
class Tensor(Generic[Axis1, Axis2]):
|
||||
|
||||
def transpose(self) -> Tensor[Axis2, Axis1]: ...
|
||||
|
||||
@overload
|
||||
class Tensor(Generic[Axis1, Axis2, Axis3]):
|
||||
|
||||
def transpose(self) -> Tensor[Axis3, Axis2, Axis1]: ...
|
||||
|
||||
Concatenating Other Types to a Type Tuple Variable
|
||||
--------------------------------------------------
|
||||
|
||||
If a type tuple variable appears with other types in the same type parameter
|
||||
list, the effect is to concatenate those types with the types
|
||||
in the type tuple variable:
|
||||
|
||||
::
|
||||
|
||||
Shape = TypeTupleVar('Shape')
|
||||
class Batch: pass
|
||||
class Height: pass
|
||||
class Width: pass
|
||||
|
||||
class Tensor(Generic[*Shape]): ...
|
||||
|
||||
def add_batch(x: Tensor[*Shape]) -> Tensor[Batch, *Shape]: ...
|
||||
|
||||
x: Tensor[Height, Width]
|
||||
add_batch(x) # Inferred type is Tensor[Batch, Height, Width]
|
||||
|
||||
Type tuple variables can also be combined with regular ``TypeVar`` instances:
|
||||
|
||||
::
|
||||
|
||||
T1 = TypeVar('T1')
|
||||
T2 = TypeVar('T2')
|
||||
|
||||
class Foo(Generic[T1, T2, *Ts]): ...
|
||||
|
||||
foo: Foo[int, str, bool, float] # T1=int, T2=str, Ts=Tuple[bool, float]
|
||||
|
||||
Concatenating Multiple Type Tuple Variables
|
||||
-------------------------------------------
|
||||
|
||||
If multiple type tuple variables appear in a parameter list, in order
|
||||
to prevent ambiguity about which types would be bound to which type
|
||||
tuple variables, the type tuple variables must not be expanded:
|
||||
|
||||
::
|
||||
|
||||
# NOT allowed
|
||||
class Bar(Generic[*Ts1, *Ts2]): ...
|
||||
# How would we decide which types are bound to Ts1
|
||||
# and which are bound to Ts2?
|
||||
bar: Bar[int, str, bool]
|
||||
|
||||
# The right way
|
||||
class Bar(Generic[Ts1, Ts2]): ...
|
||||
bar: Bar[Tuple[int], Tuple[str, bool]]
|
||||
|
||||
Rationale and Rejected Ideas
|
||||
============================
|
||||
|
||||
Supporting Variadicity Through aliases
|
||||
--------------------------------------
|
||||
|
||||
As noted in the introduction, it **is** possible to avoid variadic generics
|
||||
by simply defining aliases for each possible number of type parameters:
|
||||
|
||||
::
|
||||
|
||||
class Tensor1(Generic[Axis1]): ...
|
||||
class Tensor2(Generic[Axis1, Axis2]): ...
|
||||
|
||||
However, this seems somewhat clumsy - it requires users to unnecessarily
|
||||
pepper their code with 1s, 2s, and so on for each rank necessary.
|
||||
|
||||
Naming of ``Map``
|
||||
-----------------
|
||||
|
||||
One downside to the name ``Map`` is that it might suggest a hash map. We
|
||||
considered a number of different options for the name of this operator.
|
||||
|
||||
* ``ForEach``. This is rather long, and we thought might imply a side-effect.
|
||||
* ``Transform``. The meaning of this isn't obvious enough at first glance.
|
||||
* ``Apply``. This is inconsistent with ``apply``, an older Python function
|
||||
which enabled conversion of iterables to arguments before the star
|
||||
operator was introduced.
|
||||
|
||||
In the end, we decided that ``Map`` was good enough.
|
||||
|
||||
Naming of ``TypeTupleVar``
|
||||
--------------------------
|
||||
|
||||
``TypeTupleVar`` began as ``ListVariadic``, based on its naming in
|
||||
an early implementation in Pyre.
|
||||
|
||||
We then changed this to ``TypeVar(list=True)``, on the basis that a)
|
||||
it better emphasises the similarity to ``TypeVar``, and b) the meaning
|
||||
of 'list' is more easily understood than the jargon of 'variadic'.
|
||||
|
||||
We finally settled on ``TypeTupleVar`` based on the justification
|
||||
that c) this emphasises the tuple-like behaviour, and d) type tuple
|
||||
variables are a sufficiently different kind of thing to regular
|
||||
type variables that we may later wish to support keyword arguments
|
||||
to its constructor that should not be supported by regular
|
||||
type variables (such as ``arbitrary_len`` [#arbitrary_len]_).
|
||||
|
||||
Accessing Individual Types Without Overloads
|
||||
--------------------------------------------
|
||||
|
||||
We chose to support access to individual types in the type tuple variable
|
||||
using overloads (see the `Accessing Individual Types`_ section). One
|
||||
alternative would have been to allow explicit access to arbitrary parts
|
||||
of the type tuple variable - for example, through indexing:
|
||||
|
||||
::
|
||||
|
||||
def foo(t: Tuple[*Ts]):
|
||||
x: Ts[1] = t[1]
|
||||
|
||||
We decided to omit this mechanism from this PEP because a) it adds complexity,
|
||||
b) we were not aware of any use-cases that need it, and c) if it turns out to be
|
||||
needed in the future, it can easily be added in a future PEP.
|
||||
|
||||
Integer Generics
|
||||
----------------
|
||||
|
||||
Consider a function such as `np.tile`:
|
||||
|
||||
::
|
||||
|
||||
x = np.zeros((3,)) # A tensor of length 3
|
||||
y = np.tile(x, reps=2) # y is now length 6
|
||||
|
||||
Intuitively, we would specify the signature of such a function as:
|
||||
|
||||
::
|
||||
|
||||
@overload
|
||||
def tile(A: Tensor[N], reps: Literal[2]) -> Tensor[2*N]: ...
|
||||
# ...and other overloads for different values of `reps`
|
||||
|
||||
``N`` is *sort* of like a type variable. However, type variables
|
||||
stand in for *types*, whereas here we want ``N`` to stand in for a
|
||||
particular *value*. ``N`` should be some sort of 'integer type variable'.
|
||||
|
||||
(Note that ``N`` could *not* be created as simply ``TypeTupleVar('N', bound=int)``.
|
||||
This would state that ``N`` could stand for an ``int`` or any *subtype* of ``int``.
|
||||
For our signature above, we would need ``N`` to stand for any *instance* of
|
||||
type ``int``.)
|
||||
|
||||
We decided to omit integer type variables for this PEP, postponing it for a future
|
||||
PEP when necessary.
|
||||
|
||||
Integer Parameterization
|
||||
------------------------
|
||||
|
||||
The examples of this PEP have parameterised tensor types
|
||||
using the semantic meaning of each axes, e.g. ``Tensor[Batch, Time]``.
|
||||
However, we may also wish to parameterize using the actual
|
||||
integer value of each part of the shape, such as ``Tensor[Literal[64], Literal[64]]``.
|
||||
|
||||
There are two aspects related to such integer parameterization that we decided
|
||||
to ignore in this PEP:
|
||||
|
||||
**Examples of integer parameterization**. Thought it clearly *is* valid to
|
||||
parameterize with literal types, we wish to encourage the use of semantic
|
||||
labelling of tensor axes wherever possible: having each axis labelled serves
|
||||
as extra protection against mistakes when manipulating axes.
|
||||
|
||||
**Syntactic sugar for integer parameterization**. Typing ``Literal`` is
|
||||
cumbersome; ideally, we could write ``Tensor[64, 64]`` as syntactic sugar
|
||||
for ``Tensor[Literal[64], Literal[64]]``. However, this would require an
|
||||
inconsistency: because of forward referencing, ``Tensor['Batch']`` and
|
||||
``Tensor[Literal['Batch']]`` mean different things. For this to work, we
|
||||
would have to stipulate this sugar only applies for integers. We leave
|
||||
this discussion for a future PEP. (If you do wish to employ such types
|
||||
in your code currently, we recommend ``import Typing.Literal as L``
|
||||
enabling the much shorter ``L[64]``.)
|
||||
|
||||
Checking the Number of Types in a Variadic Generic
|
||||
--------------------------------------------------
|
||||
|
||||
Consider reduction operations, which behave as:
|
||||
|
||||
::
|
||||
|
||||
x = np.zeros((2, 3, 5))
|
||||
reduce_sum(x, axis=0) # Shape (3, 5)
|
||||
reduce_sum(x, axis=1) # Shape (2, 5)
|
||||
|
||||
One way to compactly specify the signature of these operations would be
|
||||
write something like:
|
||||
|
||||
::
|
||||
|
||||
Shape = TypeTupleVar('Shape')
|
||||
|
||||
# Tensor of rank N goes in, tensor of rank N-1 comes out
|
||||
def reduce_sum(x: Tensor[Shape[N]], axis: int) -> Tensor[Shape[N-1]]: ...
|
||||
|
||||
``Shape[N]`` here states that number of types in ``Shapes`` is bound to ``N``,
|
||||
where ``N`` is some object that we can perform arithmetic on.
|
||||
|
||||
Lacking an urgent use-case for this feature, we omit it from this PEP,
|
||||
leaving it to a future PEP if necessary.
|
||||
|
||||
(Note that reduction operations are only used as an example here.
|
||||
Reduction functions can in fact be typed without this feature,
|
||||
using overloads:
|
||||
|
||||
::
|
||||
|
||||
@overload
|
||||
def reduce_sum(x: Tensor[A, B], axis: Literal[0]) -> Tensor[B]: ...
|
||||
|
||||
@overload
|
||||
def reduce_sum(x: Tensor[A, B], axis: Literal[1]) -> Tensor[A]: ...
|
||||
|
||||
...
|
||||
|
||||
Although more verbose, typing reduction operations this way is superior
|
||||
to the approach above, since it preserves information about *which*
|
||||
axis has been removed.)
|
||||
|
||||
Backwards Compatibility
|
||||
=======================
|
||||
|
||||
TODO
|
||||
|
||||
* ``Tuple`` needs to be upgraded to support parameterization with a
|
||||
a type tuple variable.
|
||||
|
||||
|
||||
Reference Implementation
|
||||
========================
|
||||
|
||||
TODO
|
||||
|
||||
Footnotes
|
||||
==========
|
||||
|
||||
.. [#hkt] A third potential use is in enabling higher-kinded types that take
|
||||
an arbitrary number of type operands, but we do not discuss this use
|
||||
here.
|
||||
|
||||
.. [#batch] 'Batch' is machine learning parlance for 'a number of'.
|
||||
|
||||
.. [#array] We use the term 'array' to refer to a matrix with an arbitrary
|
||||
number of dimensions. In NumPy, the corresponding class is the ``ndarray``;
|
||||
in TensorFlow, the ``Tensor``; and so on.
|
||||
|
||||
.. [#timebatch] If the shape begins with 'batch × time', then
|
||||
``videos_batch[0][1]`` would select the second frame of the first video. If the
|
||||
shape begins with 'time × batch', then ``videos_batch[1][0]`` would select the
|
||||
same frame.
|
||||
|
||||
.. [#kwargs] In the case of ``**kwargs``, we mean the Nth argument as
|
||||
it appears in the function *definition*, *not* the Nth keyword argument
|
||||
specified in the function *call*.
|
||||
|
||||
.. [#type_aliases] For example, in ``asyncio`` [#asyncio]_, it is convenient to define
|
||||
a type alias
|
||||
``_FutureT = Union[Future[_T], Generator[Any, None, _T], Awaitable[_T]]``.
|
||||
We should also be able to apply ``Map`` to alias - e.g. ``Map[_FutureT, Ts]``.
|
||||
|
||||
References
|
||||
==========
|
||||
|
||||
.. [#pep-612] PEP 612, "Parameter Specification Variables":
|
||||
https://www.python.org/dev/peps/pep-0612
|
||||
|
||||
.. [#pep-484] PEP 484, "Type Hints":
|
||||
https://www.python.org/dev/peps/pep-0484
|
||||
|
||||
.. [#numeric-stack] Static typing of Python numeric stack:
|
||||
https://paper.dropbox.com/doc/Static-typing-of-Python-numeric-stack-summary-6ZQzTkgN6e0oXko8fEWwN
|
||||
|
||||
.. [#typing-ideas] Ideas for array shape typing in Python: https://docs.google.com/document/d/1vpMse4c6DrWH5rq2tQSx3qwP_m_0lyn-Ij4WHqQqRHY/edit
|
||||
|
||||
.. [#syntax-proposal] Shape annotation syntax proposal:
|
||||
https://docs.google.com/document/d/1But-hjet8-djv519HEKvBN6Ik2lW3yu0ojZo6pG9osY/edit
|
||||
|
||||
.. [#zip-sig] ``typeshed/builtins.pyi``: https://github.com/python/typeshed/blob/27dfbf68aaffab4f1ded7dc1b96f6f82f536a09d/stdlib/2and3/builtins.pyi#L1710-L1733
|
||||
|
||||
.. [#asyncio] ``typeshed/asyncio/tasks.pyi``: https://github.com/python/typeshed/blob/193c7cb93283ad4ca2a65df74c565e56bfe72b7e/stdlib/3/asyncio/tasks.pyi#L45-L154
|
||||
|
||||
.. [#arbitrary_len] Discussion on Python typing-sig mailing list: https://mail.python.org/archives/list/typing-sig@python.org/thread/SQVTQYWIOI4TIO7NNBTFFWFMSMS2TA4J/
|
||||
|
||||
|
||||
Acknowledgements
|
||||
================
|
||||
|
||||
Thank you to **Alfonso Castaño**, **Antoine Pitrou**, **Bas v.B.**, **David Foster**, **Dimitris Vardoulakis**, **Guido van Rossum**, **Jia Chen**, **Lucio Fernandez-Arjona**,
|
||||
**Nikita Sobolev**, **Peilonrayz**, **Pradeep Kumar Srinivasan**, **Rebecca Chen**, **Sergei Lebedev** and **Vladimir Mikulik** for helpful feedback and suggestions on drafts of this PEP.
|
||||
|
||||
Thank you especially to **Pradeep** for numerous key contributions, including pointing
|
||||
out that unexpanded type tuples allow for clean concatenation of multiple type tuples,
|
||||
and to **Lucio**, for suggesting the star syntax, which has made multiple aspects of
|
||||
this proposal much more concise and intuitive.
|
||||
|
||||
Resources
|
||||
=========
|
||||
|
||||
Discussions on variadic generics in Python started in 2016 with `Issue 193`__
|
||||
on the python/typing GitHub repository.
|
||||
|
||||
__ https://github.com/python/typing/issues/193
|
||||
|
||||
Inspired by this discussion, **Ivan Levkivskyi** made a concrete proposal
|
||||
at PyCon 2019, summarised in `Type system improvements`__
|
||||
and `Static typing of Python numeric stack`__.
|
||||
|
||||
__ https://paper.dropbox.com/doc/Type-system-improvements-HHOkniMG9WcCgS0LzXZAe
|
||||
|
||||
__ https://paper.dropbox.com/doc/Static-typing-of-Python-numeric-stack-summary-6ZQzTkgN6e0oXko8fEWwN
|
||||
|
||||
Expanding on these ideas, **Mark Mendoza** and **Vincent Siles** gave a presentation on
|
||||
`Variadic Type Variables for Decorators and Tensors`__ at the 2019 Python
|
||||
Typing Summit.
|
||||
|
||||
__ https://github.com/facebook/pyre-check/blob/ae85c0c6e99e3bbfc92ec55104bfdc5b9b3097b2/docs/Variadic_Type_Variables_for_Decorators_and_Tensors.pdf
|
||||
|
||||
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:
|
||||
|
Loading…
Reference in New Issue