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)