|
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