typing#

Python 在 3.5 开始全面拥抱类型系统, 以 typing 这个标准库的方式提供. 这个系统是可选的.

除了标准库, 还有两个社区维护的库也很重要:

  • typing, 跟标准库同名的库, 用于在 < 3.5 的 Python 中支持类型标注系统.

  • typing-extensions, 用于在低版本的 Python 中获得高版本 Python 中的官方 typing 标准库的特性. 例如在 3.8 后引入了 TypedDict, 如果你想在 Python 3.5 中用这个功能就可以用这个库.

Generics 泛型#

 1# -*- coding: utf-8 -*-
 2
 3"""
 4我们有一个函数 :func:`func`, 它的参数和返回值都是 BaseModel 的实例. 那如果我要扩展 BaseModel,
 5同时在不修改 ``func()`` 函数的情况下如何同时保留正确的类型提示呢?
 6
 7本例是一个错误示范.
 8"""
 9
10
11class BaseModel:
12    def base_model_method(self):
13        print("base_model_method")
14
15
16def func(
17    model: BaseModel,
18) -> BaseModel:
19    """
20    这个函数接受的参数是 :class:`BaseModel` 的子类,返回值也是 :class:`BaseModel` 的子类.
21    """
22    return model
23
24
25class MyModel(BaseModel):
26    def my_model_method(self):
27        print("my_model_method")
28
29
30model_out = func(MyModel())
31model_out.my_model_method() # 无法正确提示
 1# -*- coding: utf-8 -*-
 2
 3"""
 4我们有一个函数 :func:`func`, 它的参数和返回值都是 BaseModel 的实例. 那如果我要扩展 BaseModel,
 5同时在不修改 ``func()`` 函数的情况下如何同时保留正确的类型提示呢?
 6
 7答案是在定义 func 的入参出参时使用 `TypeVar <https://docs.python.org/3/library/typing.html#typing.TypeVar>`_
 8泛型.
 9"""
10import typing as T
11
12
13class BaseModel:
14    def base_model_method(self):
15        print("base_model_method")
16
17
18T_BASE_MODEL = T.TypeVar("T_BASE_MODEL", bound=BaseModel)
19
20
21def func(
22    model: T_BASE_MODEL,
23) -> T_BASE_MODEL:
24    """
25    这个函数接受的参数是 :class:`BaseModel` 的子类,返回值也是 :class:`BaseModel` 的子类.
26    """
27    return model
28
29
30class MyModel(BaseModel):
31    def my_model_method(self):
32        print("my_model_method")
33
34
35model_out = func(MyModel())
36model_out.my_model_method() # 能够正确提示
 1# -*- coding: utf-8 -*-
 2
 3"""
 4我们有一个函数 :func:`func`, 它的参数和返回值有的是 BaseModel 的实例, 有的是 BaseModel 的类本身.
 5那如果我要扩展 BaseModel, 同时在不修改 ``func()`` 函数的情况下如何同时保留正确的类型提示呢?
 6
 7答案是在定义 func 的入参出参时使用 `TypeVar <https://docs.python.org/3/library/typing.html#typing.TypeVar>`_
 8泛型.
 9"""
10
11import typing as T
12
13
14class BaseModel:
15    def base_model_method(self):
16        print("base_model_method")
17
18
19T_BASE_MODEL = T.TypeVar("T_BASE_MODEL", bound=BaseModel)
20
21
22def func(
23    model_klass: T.Type[T_BASE_MODEL],
24) -> T_BASE_MODEL:
25    """
26    这个函数接受的参数是 :class:`BaseModel` 的子类的类, 不是实例,返回值是 :class:`BaseModel` 的子类的实例.
27    """
28    return model_klass()
29
30
31class MyModel(BaseModel):
32    def my_model_method(self):
33        print("my_model_method")
34
35
36model_out = func(BaseModel)
37model_out.base_model_method()  # 能够正确提示
38
39model_out = func(MyModel)
40model_out.my_model_method()  # 能够正确提示
 1# -*- coding: utf-8 -*-
 2
 3"""
 4我们有一个类 :class:`BaseContainer`, 它的大部分参数和返回值有的是 BaseModel 的实例,
 5有的是 BaseModel 的类本身. 那如果我要扩展 BaseModel, 同时在不修改 ``BaseContainer`` 类
 6的情况下如何同时保留正确的类型提示呢?
 7
 8这是一个错误实现. 很多人会以为将类本身作为参数传入 ``__init__`` 中就可以了. 很遗憾, 这是 IDE
 9的功能的一部分, 强如 PyCharm 也无法正确提示.
10"""
11
12import typing as T
13
14
15class BaseModel:
16    def base_model_method(self):
17        print("base_model_method")
18
19
20T_BASE_MODEL = T.TypeVar("T_BASE_MODEL", bound=BaseModel)
21
22
23class BaseContainer:
24    """
25    这个类的初始化函数接受的参数是 :class:`BaseModel` 的子类的类, 不是实例.
26    get() 方法的返回值是 :class:`BaseModel` 的子类的实例.
27    """
28
29    def __init__(self, model_class: T.Type[T_BASE_MODEL]):
30        self.model_class = model_class
31
32    def get(self) -> T_BASE_MODEL:
33        return self.model_class()
34
35
36class MyModel(BaseModel):
37    def my_model_method(self):
38        print("my_model_method")
39
40
41model_out = BaseContainer(BaseModel).get()
42model_out.base_model_method()  # 能够正确提示
43
44model_out = BaseContainer(MyModel).get()
45model_out.my_model_method()  # 如何参数是子类则不能正确提示
 1# -*- coding: utf-8 -*-
 2
 3"""
 4我们有一个类 :class:`BaseContainer`, 它的大部分参数和返回值有的是 BaseModel 的实例,
 5有的是 BaseModel 的类本身. 那如果我要扩展 BaseModel, 同时在不修改 ``BaseContainer`` 类
 6的情况下如何同时保留正确的类型提示呢?
 7
 8答案是用 `TypeVar <https://docs.python.org/3/library/typing.html#typing.TypeVar>`_ + `Generic <https://docs.python.org/3/library/typing.html#typing.Generic>`_.
 9
10这是一个正确实现.
11"""
12
13import typing as T
14
15
16class BaseModel:
17    def base_model_method(self):
18        print("base_model_method")
19
20
21T_BASE_MODEL = T.TypeVar("T_BASE_MODEL", bound=BaseModel)
22
23
24class BaseContainer(T.Generic[T_BASE_MODEL]):
25    """
26    这个类的初始化函数接受的参数是 :class:`BaseModel` 的子类的类, 不是实例.
27    get() 方法的返回值是 :class:`BaseModel` 的子类的实例.
28    """
29
30    def __init__(self, model_class: T.Type[T_BASE_MODEL]):
31        self.model_class = model_class
32
33    def get(self) -> T_BASE_MODEL:
34        return self.model_class()
35
36
37class MyModel(BaseModel):
38    def my_model_method(self):
39        print("my_model_method")
40
41
42class MyContainer(BaseContainer[MyModel]):
43    pass
44
45
46model_out = MyContainer(MyModel).get()
47model_out.my_model_method()  # 能够正确提示
 1# -*- coding: utf-8 -*-
 2
 3"""
 4我们有一个类 :class:`BaseContainer`, 它的大部分参数和返回值有的是 BaseModel 的实例,
 5有的是 BaseModel 的类本身. 那如果我要扩展 BaseModel, 同时在不修改 ``BaseContainer`` 类
 6的情况下如何同时保留正确的类型提示呢? 这个例子和前一个例子的唯一区别是这个类是一个 dataclass.
 7
 8答案是用 `TypeVar <https://docs.python.org/3/library/typing.html#typing.TypeVar>`_ + `Generic <https://docs.python.org/3/library/typing.html#typing.Generic>`_.
 9
10这是一个正确实现.
11"""
12
13import typing as T
14import dataclasses
15
16
17@dataclasses.dataclass
18class BaseModel:
19    def base_model_method(self):
20        print("base_model_method")
21
22
23T_BASE_MODEL = T.TypeVar("T_BASE_MODEL", bound=BaseModel)
24
25
26@dataclasses.dataclass
27class BaseContainer(T.Generic[T_BASE_MODEL]):
28    """
29    这个类的初始化函数接受的参数是 :class:`BaseModel` 的子类的类, 不是实例.
30    get() 方法的返回值是 :class:`BaseModel` 的子类的实例.
31    """
32
33    model_class: T.Type[T_BASE_MODEL] = dataclasses.field()
34
35    def get(self) -> T_BASE_MODEL:
36        return self.model_class()
37
38
39class MyModel(BaseModel):
40    def my_model_method(self):
41        print("my_model_method")
42
43
44class MyContainer(BaseContainer[MyModel]):
45    pass
46
47
48model_out = MyContainer(MyModel).get()
49model_out.my_model_method()  # 能够正确提示
 1# -*- coding: utf-8 -*-
 2
 3import typing as T
 4import dataclasses
 5
 6T_DATA_LIKE = T.Union[dict, "T_BASE_MODEL", None]
 7
 8
 9@dataclasses.dataclass
10class BaseModel:
11    def base_model_method(self):
12        print("call base_model_method")
13
14    @classmethod
15    def from_dict(
16        cls: T.Type["T_BASE_MODEL"],  # <=== 这里的关键是给 cls 也加上 TypeHint, 并且用 TypeVar 标注
17        dct: T.Union[dict, "T_BASE_MODEL", None],
18    ) -> T.Optional["T_BASE_MODEL"]:
19        """
20        Construct an instance from dataclass-like data.
21        It could be a dictionary, an instance of this class, or None.
22        """
23        if isinstance(dct, dict):
24            return cls(**dct)
25        elif isinstance(dct, cls):
26            return dct
27        elif dct is None:
28            return None
29        else:
30            raise TypeError
31
32    @classmethod
33    def from_list(
34        cls: T.Type["T_BASE_MODEL"],  # <=== 这里的关键是给 cls 也加上 TypeHint, 并且用 TypeVar 标注
35        list_of_dct_or_obj: T.Optional[T.List[T_DATA_LIKE]],
36    ) -> T.Optional[T.List[T.Optional["T_BASE_MODEL"]]]:
37        """
38        Construct list of instance from list of dataclass-like data.
39        It could be a dictionary, an instance of this class, or None.
40        """
41        if isinstance(list_of_dct_or_obj, list):
42            return [cls.from_dict(item) for item in list_of_dct_or_obj]
43        elif list_of_dct_or_obj is None:
44            return None
45        else:  # pragma: no cover
46            raise TypeError
47
48
49T_BASE_MODEL = T.TypeVar("T_BASE_MODEL", bound=BaseModel)
50
51
52@dataclasses.dataclass
53class MyModel(BaseModel):
54    def my_model_method(self):
55        print("call my_model_method")
56
57
58my_model = MyModel.from_dict({})
59my_model.base_model_method()  # type hint OK
60my_model.my_model_method()  # type hint OK
61
62my_model_list = MyModel.from_list([{}])
63my_model = my_model_list[0]
64my_model.base_model_method()  # type hint OK
65my_model.my_model_method()  # type hint OK
 1# -*- coding: utf-8 -*-
 2
 3"""
 4我有一个 :class:`MyItem` 类. 是所有跟 Item 相关的类的基类, 并且用户可以扩展这个类.
 5我还有一个 :class:`MyClass` 的类. 大部分的参数和方法都是返回一个 :class:`MyItem` 的实例.
 6
 7我如何允许用户扩展 MyClass 和 MyItem 并且还能获得正确的类型提示呢?
 8
 9答案是用 `TypeVar <https://docs.python.org/3/library/typing.html#typing.TypeVar>`_ + `Generic <https://docs.python.org/3/library/typing.html#typing.Generic>`_.
10
11本实现是将 ``item_type`` 作为一个参数传入到 :class:`MyClass` 的初始化函数中.
12"""
13
14import typing as T
15
16T_ITEM = T.TypeVar("T_ITEM")
17
18
19class MyClass(T.Generic[T_ITEM]):
20    def __init__(self, item_type: T.Type[T_ITEM]):
21        self.item_type = item_type
22
23    def get_item(self) -> T_ITEM:
24        return None
25
26
27class MyItem:
28    def say_hi(self):
29        print("say hi")
30
31
32my_class = MyClass(MyItem)
33item = my_class.get_item()
34item.say_hi()  # 能够正确提示
 1# -*- coding: utf-8 -*-
 2
 3"""
 4我有一个 :class:`MyItem` 类. 是所有跟 Item 相关的类的基类, 并且用户可以扩展这个类.
 5我还有一个 :class:`MyClass` 的类. 大部分的参数和方法都是返回一个 :class:`MyItem` 的实例.
 6
 7我如何允许用户扩展 MyClass 和 MyItem 并且还能获得正确的类型提示呢?
 8
 9答案是用 `TypeVar <https://docs.python.org/3/library/typing.html#typing.TypeVar>`_ + `Generic <https://docs.python.org/3/library/typing.html#typing.Generic>`_.
10
11本实现是将 MyItem 的子类的类型作为一个 Generic 参数在定义 MyClass 的子类时传入.
12"""
13
14import typing as T
15
16T_ITEM = T.TypeVar("T_ITEM")
17
18
19class MyClass(T.Generic[T_ITEM]):
20    def get_item(self) -> T_ITEM:
21        return None
22
23
24class MyItem:
25    def say_hi(self):
26        print("say hi")
27
28
29class MyNewClass(MyClass[MyItem]):
30    pass
31
32
33my_new_class = MyNewClass()
34item = my_new_class.get_item()
35item.say_hi()  # 能够正确提示

Mixin 模式#

 1# -*- coding: utf-8 -*-
 2
 3"""
 4Mixin 模式是一个非常有用的设计模式. 你可以将一个类的方法分拆成很多个 Mixin 类. 然后在
 5主类中集成所有的 Mixin 类从而获得它们的方法. 使得代码更加清晰, 便于维护.
 6
 7如果你想要在主类和 Mixin 类中都获得 type hint, 那么你需要在 Mixin 类中大部分的方法中的
 8``self: "主类"`` 都做上这样的类型提示.
 9"""
10
11import typing as T
12import dataclasses
13
14
15@dataclasses.dataclass
16class AppMixin:
17    app_attr: str
18
19    def app_method(self: "Config"):
20        # type hint OK
21        _ = self.deploy_attr
22        print("app_method")
23
24
25@dataclasses.dataclass
26class DeployMixin:
27    deploy_attr: str
28
29    def deploy_method(self: "Config"):
30        _ = self.app_attr
31        print("deploy_method")
32
33
34@dataclasses.dataclass
35class ConstructorMixin:
36    # type hint NOT OK
37    @classmethod
38    def new(
39        cls: T.Type["Config"],
40        app_attr: str,
41        deploy_attr: str,
42    ):
43        return cls(
44            app_attr=app_attr,
45            deploy_attr=deploy_attr,
46        )
47
48
49@dataclasses.dataclass
50class Config(
51    AppMixin,
52    DeployMixin,
53    ConstructorMixin,
54):
55    pass
56
57
58# type hint OK
59config = Config.new(
60    app_attr="app_attr",
61    deploy_attr="deploy_attr",
62)
63print(config)
64
65# type hint OK
66config = Config(app_attr="app_attr", deploy_attr="deploy_attr")
67print(config)