# Type Hints 贡献指南(建设中)

学习使用 Type Hints，让代码更加规范整洁\~

你有碰到这样的情况吗：你打开自己前段时间写的代码或者翻开同伴的代码，你已经忘记或者压根不知道这个函数的原型是什么、应该传入什么类型的参数，这时候需要你费很大劲阅读代码才能确定每个参数的类型是什么；如果这时候你拿到的是一个并不完善的第三方库，压根不知道这个库应该如何使用，那就更崩溃了。以上是我们在使用 Python 进行编程时，可能会遇到的一个场景。Python 作为动态类编程语言，在定义变量时不需要预声明变量类型，这一特性为编程带来便利的同时，也引入了一些问题（例如代码可读性变差，容易引发各种各样的 TypeError）。为了解决这些问题，Python 3.5 引入了 [typing ](https://docs.python.org/3/library/typing.html)模块，并在 [PEP 483](https://peps.python.org/pep-0483/) 和 [PEP 484](https://peps.python.org/pep-0484/) 进一步介绍了如何进行 Type Hints（类型提示）。今天我们就一起来学习使用 Type Hints，让代码更加规范整洁。

## 1. 为什么要使用 Type Hints

### 1.1  增加代码可读性

要知道，我们阅读的代码数远远要多于我们写过的代码数，有了参数的类型信息，理解和维护代码库将会变得更加容易。例如：

```python
def add(a, b):
    return a + b
```

只看代码，你完全不知道应该传入什么类型的参数，来获得符合预期的结果（这里的 add 只是非常简单的例子，实际工程中可能函数实现非常复杂，只有传入了正确的类型才能获得正确的结果）。如果我们加上了 Type Hints：

```python
def add(a: int, b: int):
    return a + b
```

那么只看函数签名，我们就能知道传入的参数类型。

### 1.2 帮助代码补全或函数跳转

我们在 IDE 里写代码时，经常会使用 Tab 来补全类方法，而这个功能，也是借助 Type Hints 实现的。假设我们想实现将两个 `np.ndarray` 变量相加的函数：

```python
def np_add(a, b):
    c = a + b
    return c.sum()
```

这个时候我们会发现，c.sum() 这个函数是无法自动补全/跳转的，因为我们无法推导出 c 的类型，自然也无法补全 c 的方法。上图为 vscode 展示的效果（真的是 vscode，JetBrain 主题 (●'◡'●)），vscode 插件推导 sum 的类型为 Any，自然没法补全、跳转代码。如果我们补上 Type Hints：

```
def np_add(a: np.ndarray, b: np.ndarray):
    c = a + b
    return c.sum()
```

此时 sum 方法既能够补全，也能够跳转。

vscode 就能解析 ndarray.sum() 的函数签名了。

### 1.3 在 vscode 中自动生成 docstring

有了 Type Hints，生成 docstring 也会变得简单。安装 autoDocstring 插件：![](https://aicarrier.feishu.cn/space/api/box/stream/download/asynccode/?code=ZjllZTlkMTAwYjNiY2UxMWZmNmNkODUzMTRkMzlkM2JfZXVoblVjUTB0cWE3bmdHa1ZnVXdDSlpYYUFqMnBtNjBfVG9rZW46Ym94Y25YMTNzVUd0U1VhcDdJYm14MmVRZ0poXzE2ODQ2NjEyMDI6MTY4NDY2NDgwMl9WNA)写 docstring 时会有以下提示：![](https://aicarrier.feishu.cn/space/api/box/stream/download/asynccode/?code=ZDM1ZjYwMzIxMjJjNGYwYzRlN2NhMDdkYmY3OWE1M2ZfS1p1OWFya1pmaFFQSVlzaU51U1FIaVZRaTZKS1FkSlRfVG9rZW46Ym94Y25CYkNsbEFNZ2FsWTNCQWRYN0ZDbnplXzE2ODQ2NjEyMDI6MTY4NDY2NDgwMl9WNA)一键生成 docstring：![](https://aicarrier.feishu.cn/space/api/box/stream/download/asynccode/?code=YTYyYzU1NDhhYzcyOTcwYzUwZmU5ZDY0N2RmMTVhMTFfM1RmbnFDQzN4d1J3Tmx4eHVSMUdIWGNTdzZ5UHVnSVJfVG9rZW46Ym94Y253TzhXb1JBU1Mwa0JqQ05qd28xa0h5XzE2ODQ2NjEyMDI6MTY4NDY2NDgwMl9WNA)利用 Type Hints 生成 docstring 可以大大减少 docstring 的工作量（Pycharm 党的哀嚎，不会自动补全 docstring 里的变量类型）。

### 1.4 增加代码鲁棒性

Type Hints 仅仅是类型提示，对变量类型没有强制性要求。换句话说，我们给函数传入和 Type Hints 不一致的变量类型，程序也不会因此而报错。为了让 Python 代码能够像 C/C++ 一样对类型做静态检查（类型不符合就报错），可以使用 mypy + Type Hints 来检查我们的代码。

```python
import numpy as np

def np_add(a: np.ndarray, b: np.ndarray):
    return a + b

np_add(1, 2)
```

上述代码调用 np\_add 时，传入了类型不符的变量，使用 mypy 对其进行类型检查：

```sh
mypy learn_type_hint.py
learn_type_hint.py:8: error: Argument 1 to "np_add" has incompatible type "int"; expected "ndarray[Any, Any]"
learn_type_hint.py:8: error: Argument 2 to "np_add" has incompatible type "int"; expected "ndarray[Any, Any]"
```

执行 pre-commit 时，mypy 运行在独立的虚拟环境中，并没有你当前环境的依赖，因此无法对三方库的类型做检查。当你在本地环境执行 mypy 时，会导入三方库的 Type Hints，mypy 的类型检查会更加的严格。如果你想使用单独使用 mypy 检查某个文件，并得到和 pre-commit 相同的检查效果，则需要新建一个空的 python 环境，在空环境里执行 mypy。综上，使用 Type Hints 的主要目的就是为了引入静态类型检查，增加一道防线，及早发现 bug 提高代码的鲁棒性。

## 2. 基本用法

### 2.1 Type Hints 基本类型

对于 int、float、str 类型的 Python 内置类型，可以直接使用类型本身来写 Type Hints：

```python
# 声明类型 + 定义
a: int = 1
# 先声明
b: int
# 后定义
b = 1
# 函数中的 typehint, 输出类型用 -> 连接
def foo(a: int, b: int=1) -> int:
    return a + b
# str 类型的 Type Hints
repo: str = 'mmcv'
# float 类型的 Type Hints 
value: float = 0.1
```

### 2.2 复合型的 Type Hints

对于 list、tuple、dict 等容器类的实例，我们也可以用内置类型的 Type Hints：

```python
info: dict = dict(a=1, b=2)  # 等价于 Dict[Any, Any]
element: list = [1]  # 等价于 List[Any]
```

上述写法仅声明了变量本身的类型是 `list`/`dict`，而变量中的元素可以是任意类型（Any）。如果想进一步约束容器中元素的类型，则需要引入 typing 模块的 Dict 和 List：

```python
from typing import List， Tuple, Dict
int_list: List[int] = [1, 2]
int_tuple: Tuple[int] = (1, 2)
str_int_dict: Dict[str, int] = dict(name="lisa")
```

如果变量可能是多种类型，则需要引入 Union：

```python
from typing import Union
int_or_float: Union[int, float] = 1
int_or_float = 0.5
```

Optional 类来声明默认值为 None 的实例：

```python
from typing import Optional
default_none: Optional[str] = None
```

复杂一点的例子：

```python
from typing import Optional， Union
name: Optional[Union[int, float]] = None
```

如果变量的类型是生成器/迭代器，则需要导入容器类 Generator/Iterator，其写法和 List、Dict 相同：

```python
from typing import Generator, Iterator
# generator 接受一个迭代器参数，函数会返回一个生成器
def generator(iterator: Iterator[int]) -> Generator[int, None, None]:
    for element in iterator:
        yield element
```

### 2.3 Type Hints 别名

有些变量类型的 Type Hints 过于复杂，直接在函数中声明会影响接口的可读性，因此可以重命名特定的变量类型：

```python
complex_type = Optional[Union[List[int], int]]

def foo(comlex_arg: complex_type = None) -> None:
    pass
```

### 2.4 函数类型的 Type Hints

可以使用 `Callable` 来声明函数类型的变量。直接使用 `Callable` 表示函数接受任意个数、任意类型的参数，并返回任意个数、任意类型的变量。如果想进一步约束函数入参和返回值类型，可以使用 `Callable[[Arg1Type, Arg2Type], ReturnType]`：

```python
from typing import Callable

def foo(a: int, b: int) -> int:
    return a + b

# 不声明参数和返回值类型
def register_callback2(func: Callable)
    pass

# 声明入参类型和返回类型。入参为 （int, int），返回值类型为 int
def register_callback1(func: Callable[[int, int], int]):
    pass
    
```

### 2.5 Type Hints 对应的 docstring

* 参数有非 None 类型的默认值。直接在对应的 args 末尾加 `Defaults to xxx` 即可：

```python
def hello(name: str = 'heihei') -> None:
    """Say hello to someone.

    Args:
        name (str): name of people. Defaults to "heihei".
    """
```

* 参数有默认值，默认值为 None。此时（）内写（str, optional），且无需追加 `Defaults to xxx`：

```python
def hello(name: str = None) -> None:
    """Say hello to someone.

    Args:
        name (str, optional): name of people.
    """
    print(f'hello {name} ~')
```

* 参数可能是多种类型。可以写作 `（str or List[str]）`或者 `（str | List[str]）`：

```python
def hello(name: Uninon[List[str], str]):
    """Say hello to someone.

    Args:
        name (str or List[str]): name of people.
    """
    print(f'hello {name} ~')
```

* 参数可能是多种类型，且默认值为 None。可以写作 `(str or List[str], optional)`：

```python
def hello(name: Uninon[List[str], str] = None) - None:
    """Say hello to someone.

    Args:
        name (str or List[str], optional): name of people.
    """
    print(f'hello {name} ~')
```

以上四种情况覆盖了大部分的 args 类型。给代码添加了 Type Hints 后，我们就可以用 mypy 对其进行更加准确的静态检查了。接下来，我们和大家一起来认识一下 mypy。

## 3. mypy 介绍

mypy 是一种静态检查工具，可以帮助我们像静态语言一样在运行代码之前捕捉到一些错误。

* > mypy 配置项：<https://mypy.readthedocs.io/en/stable/config\\_file.html>
* > 常见问题汇总： [Common issues and solutions - mypy 0.950 documentation](https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases)

### 3.1 类型推导规则

#### 3.1.1 mypy 会对内置类型的表达式，做类型推导：

```python
a = 1
b = []  # Need type annotation for "b" (hint: "b: List[<type>] = ...")
a + b  # Unsupported operand types for + ("int" and "List[Any]")
```

上述代码 mypy 会报两个错误：

* 空列表 b 需要预声明变量类型，应该写作 `b: list = []`
* list 和 int 类型的数据不能相加

#### 3.1.2 源码实现/三方库的类型推导

mypy 可以根据我们源码中的 Type Hints，做类型推导：

```python
class MyClass:
    def foo(self):
        pass


def get_my_class() -> MyClass:
    return MyClass()


instance = get_my_class()
instance.foo()
instance.unknown()  # error: "MyClass" has no attribute "unknown"
```

mypy 能够检查出 instance 的类型是 `MyClass`，进而检查出第 12 行访问了不存在的属性。虚拟环境中 mypy 不会对三方库的类型的做检查：

```python
import numpy as np

array = np.array([1])
array.unknown()
```

mypy 默认 array 的类型为 Any，可以访问其任意属性。

#### 3.1.3 函数参数的类型推导

函数入参即使有默认值，mypy 也不会推断其类型，默认类型为 Any：

```python
class MypyDemo:

    def __init__(self, parent='parent'):
        self.get_value(parent)
    # 尽管 parent 的默认类型和 i 的变量类型不匹配，仍然能够通过静态检查
    def get_value(self, value: int):
        return value
```

如果我们给 parent 加上 Type Hints，mypy 就会报错：

```python
class MypyDemo:

    def __init__(self, parent: str = 'parent'):
        self.get_value(parent)

    def get_value(self, i: int):
        return i
# error: Argument 1 to "get_value" of "MypyDemo" has incompatible type "str"; expected "int"
```

### 3.2 常见问题

#### 3.2.1 变量类型改变引起的报错

示例如下：

```python
def parse_data(data: list):
    data = torch.stack(data)
    return data
```

这段代码是无法通过 mypy 检查的。入参 data 有 Type Hints，类型为 list，而 mypy 不允许复写变量类型。因此最好的解决方案是重命名变量：

```python
# 正确
def parse_data(data: list):
    batch_data = torch.stack(data)
    return batch_data
```

此外，我们也不应该复写 mypy 推导得到的变量类型：

```python
def my_func(condition) -> dict:
    result = {'success': False}

    if condition:
        result['success'] = True
        return result
    else:
        result['message'] = 'error message'
    return result
# error: Incompatible types in assignment (expression has type "str", target has type "bool")
```

mypy 推导得到的 results 类型为 `Dict[str, bool]`，而第 8 行复写为 `Dict[str, str]`，无法通过 mypy 检查。解决方案是预定义变量类型 :

```python
def my_func(condition) -> dict:
    result: Dict[str, Union[str, bool]] = {'success': False}

    if condition:
        result['success'] = True
        return result
    else:
        result['message'] = 'error message'
    return result
```

#### 3.2.2 入参类型不匹配

对于某些变量，例如字典。写代码的人可能知道每个 key 对应的 value 是什么类型的，就可能会写出这样的代码：

```python
from typing import Dict, Union

def count_chars(string) -> Dict[str, Union[str, bool, int]]:
    result = {}  # type: Dict[str, Union[str, bool, int]]

    if not isinstance(string, str):
        result['success'] = False
        result['message'] = 'Inavlid argument'
    else:
        result['success'] = True
        result['result'] = len(string)
    return result

def get_square(integer: int) -> int:
    return integer * integer

def validate_str(string: str) -> bool:
    check_count = count_chars(string)
    if not check_count['success']:
        print(check_count['message'])
        return False
    str_len_square = get_square(check_count['result'])
    return bool(str_len_square > 42)

result = validate_str("Lorem ipsum")
```

根据第 11 行和第 22 行我们知道 `check_count["result"]` 返回的类型为 int，是符合 `get_square` 的函数签名的。然而 mypy 是无法判断 `check_count["result"]` 的类型的。对 mypy 而言，get\_square 接收的参数类型是 `Union[str, bool, int]`，因此报错。对于这种情况，我们通常有 2 种解决方案：

* 类型窄化（[Type narrowing - mypy 0.950 documentation](https://mypy.readthedocs.io/en/stable/type_narrowing.html)）

使用 isinstance 和 assert 对 check\_count\["result"] 做类型限定。

```python
def validate_str(string: str) -> bool:
    check_count = count_chars(string)
    if check_count['success'] is False:
        print(check_count['message'])
        return False
    assert isinstance(check_count['result'], int)
    str_len_square = get_square(check_count['result'])
    return bool(str_len_square > 42)
```

mypy 会识别第六行代码，判断 check\_count\['result'] 的类型为 int。 因此在检查到第 7 行时不会报错。

* 类型忽视

在某行代码的末尾加上 `# type ignore`，mypy 就不会检查这行代码了。要慎重使用 `# type ignore`，只有在 mypy 静态检查不合理时才使用它。

```python
def validate_str(string: str) -> bool:
    check_count = count_chars(string)
    if check_count['success'] is False:
        print(check_count['message'])
        return False
    str_len_square = get_square(check_count['result'])  # type: ignore
    return bool(str_len_square > 42)
```

#### 示例参考

* MMCV: <https://github.com/open-mmlab/mmcv/pull/1950>
* MMCV: <https://github.com/open-mmlab/mmcv/pull/1946>
* MMCV: <https://github.com/open-mmlab/mmcv/pull/1942>

## 5. 参考资料

[PEP 483 – The Theory of Type Hints | peps.python.org](https://peps.python.org/pep-0483/#background)

[PEP 484 – Type Hints | peps.python.org](https://peps.python.org/pep-0484/#generics)

[Why You Should Consider Using Python Type Hints | by Fernando Souza | Vacatronics | Medium](https://medium.com/vacatronics/why-you-should-consider-python-type-hints-770e5cb1570f)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://mmsig.openmmlab.com/general/typehints.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
