|
1 |
| -Function overloading in stubs |
2 |
| -============================= |
| 1 | +Function Overloading |
| 2 | +==================== |
3 | 3 |
|
4 |
| -Sometimes you have a library function that seems to call for two or |
5 |
| -more signatures. That's okay -- you can define multiple *overloaded* |
6 |
| -instances of a function with the same name but different signatures in |
7 |
| -a stub file (this feature is not supported for user code, at least not |
8 |
| -yet) using the ``@overload`` decorator. For example, we can define an |
9 |
| -``abs`` function that works for both ``int`` and ``float`` arguments: |
| 4 | +Sometimes the types in a function depend on each other in ways that |
| 5 | +can't be captured with a ``Union``. For example, the ``__getitem__`` |
| 6 | +(``[]`` bracket indexing) method can take an integer and return a |
| 7 | +single item, or take a ``slice`` and return a ``Sequence`` of items. |
| 8 | +You might be tempted to annotate it like so: |
10 | 9 |
|
11 | 10 | .. code-block:: python
|
12 | 11 |
|
13 |
| - # This is a stub file! |
14 |
| -
|
15 |
| - from typing import overload |
16 |
| -
|
17 |
| - @overload |
18 |
| - def abs(n: int) -> int: pass |
19 |
| -
|
20 |
| - @overload |
21 |
| - def abs(n: float) -> float: pass |
22 |
| -
|
23 |
| -Note that we can't use ``Union[int, float]`` as the argument type, |
24 |
| -since this wouldn't allow us to express that the return |
25 |
| -type depends on the argument type. |
26 |
| - |
27 |
| -Now if we import ``abs`` as defined in the above library stub, we can |
28 |
| -write code like this, and the types are inferred correctly: |
| 12 | + from typing import Sequence, TypeVar, Union |
| 13 | + T = TypeVar('T') |
| 14 | +
|
| 15 | + class MyList(Sequence[T]): |
| 16 | + def __getitem__(self, index: Union[int, slice]) -> Union[T, Sequence[T]]: |
| 17 | + if isinstance(index, int): |
| 18 | + ... # Return a T here |
| 19 | + elif isinstance(index, slice): |
| 20 | + ... # Return a sequence of Ts here |
| 21 | + else: |
| 22 | + raise TypeError(...) |
| 23 | + |
| 24 | +But this is too loose, as it implies that when you pass in an ``int`` |
| 25 | +you might sometimes get out a single item and sometimes a sequence. |
| 26 | +The return type depends on the parameter type in a way that can't be |
| 27 | +expressed using a type variable. Instead, we can use `overloading |
| 28 | +<https://www.python.org/dev/peps/pep-0484/#function-method-overloading>`_ |
| 29 | +to give the same function multiple type annotations (signatures) and |
| 30 | +accurately describe the function's behavior. |
29 | 31 |
|
30 | 32 | .. code-block:: python
|
31 | 33 |
|
32 |
| - n = abs(-2) # 2 (int) |
33 |
| - f = abs(-1.5) # 1.5 (float) |
| 34 | + from typing import overload, Sequence, TypeVar, Union |
| 35 | + T = TypeVar('T') |
| 36 | +
|
| 37 | + class MyList(Sequence[T]): |
| 38 | +
|
| 39 | + # The @overload definitions are just for the type checker, |
| 40 | + # and overwritten by the real implementation below. |
| 41 | + @overload |
| 42 | + def __getitem__(self, index: int) -> T: |
| 43 | + pass # Don't put code here |
| 44 | +
|
| 45 | + # All overloads and the implementation must be adjacent |
| 46 | + # in the source file, and overload order may matter: |
| 47 | + # when two overloads may overlap, the more specific one |
| 48 | + # should come first. |
| 49 | + @overload |
| 50 | + def __getitem__(self, index: slice) -> Sequence[T]: |
| 51 | + pass # Don't put code here |
| 52 | +
|
| 53 | + # The implementation goes last, without @overload. |
| 54 | + # It may or may not have type hints; if it does, |
| 55 | + # these are checked against the overload definitions |
| 56 | + # as well as against the implementation body. |
| 57 | + def __getitem__(self, index): |
| 58 | + # This is exactly the same as before. |
| 59 | + if isinstance(index, int): |
| 60 | + ... # Return a T here |
| 61 | + elif isinstance(index, slice): |
| 62 | + ... # Return a sequence of Ts here |
| 63 | + else: |
| 64 | + raise TypeError(...) |
34 | 65 |
|
35 | 66 | Overloaded function variants are still ordinary Python functions and
|
36 |
| -they still define a single runtime object. The following code is |
37 |
| -thus valid: |
38 |
| - |
39 |
| -.. code-block:: python |
40 |
| -
|
41 |
| - my_abs = abs |
42 |
| - my_abs(-2) # 2 (int) |
43 |
| - my_abs(-1.5) # 1.5 (float) |
| 67 | +they still define a single runtime object. There is no automatic |
| 68 | +dispatch happening, and you must manually handle the different types |
| 69 | +in the implementation (usually with :func:`isinstance` checks, as |
| 70 | +shown in the example). |
44 | 71 |
|
45 | 72 | The overload variants must be adjacent in the code. This makes code
|
46 | 73 | clearer, as you don't have to hunt for overload variants across the
|
47 | 74 | file.
|
48 | 75 |
|
| 76 | +Overloads in stub files are exactly the same, except there is no |
| 77 | +implementation. |
| 78 | + |
49 | 79 | .. note::
|
50 | 80 |
|
51 | 81 | As generic type variables are erased at runtime when constructing
|
52 | 82 | instances of generic types, an overloaded function cannot have
|
53 | 83 | variants that only differ in a generic type argument,
|
54 |
| - e.g. ``List[int]`` versus ``List[str]``. |
| 84 | + e.g. ``List[int]`` and ``List[str]``. |
55 | 85 |
|
56 | 86 | .. note::
|
57 | 87 |
|
58 |
| - If you are writing a regular module rather than a stub, you can |
59 |
| - often use a type variable with a value restriction to represent |
60 |
| - functions as ``abs`` above (see :ref:`type-variable-value-restriction`). |
| 88 | + If you just need to constrain a type variable to certain types or |
| 89 | + subtypes, you can use a :ref:`value restriction |
| 90 | + <type-variable-value-restriction>`. |
0 commit comments