Python 3.10新加入的四个和类型系统相关的新特性
/ / / 阅读数:7891本文详细介绍在 Python 3.10 新加入的四个和类型系统相关的新特性。
PEP 604: New Type Union Operator
在之前的版本想要声明类型包含多种时,这么写:
from typing import Union def square(number: Union[int, float]) -> Union[int, float]: return number ** 2 |
这么写其实比较麻烦,每次都要from typing import Union
再使用Union[]
,现在直接可以使用|
来表示:
def square(number: int | float) -> int | float: return number ** 2 |
可以说很方便了。事实上这个新的用法也可以用在isinstance
和issubclass
里面。之前确认是不是某些类型中的一种这么写:
In : isinstance('s', (int, str)) Out: True In : isinstance(1, (int, str)) Out: True |
只要是 int 或者 str 都返回 True,现在这么写就可以:
In : isinstance('s', int | str) Out: True In : isinstance(1, int | str) Out: True |
PEP 613: TypeAlias
有时候我们想要自定义一个类型,那么就创建一个别名 (Alias),之前的版本通常这么写:
Board = List[Tuple[str, str]] |
但是对于类型检查器 (Type Checker) 来说,它无法分辨这是一个类型别名,还是普通的赋值。现在引入 TypeAlias 就很容易分辨了:
from typing import TypeAlias Board:TypeAlias = List[Tuple[str, str]] # 这是一个类型别名 Board = 2 # 这是一个模块常量 |
PEP 647: User-Defined Type Guards
在当代静态检查工具 (如 typescript、mypy 等) 里面会有一个叫做【Type narrowing】功能,如其名字,中文叫做【类型收窄】。就是当某个参数类型本来可以符合多个类型,但是在特定的条件里可以让类型范围缩小,直接限定到更小范围的某个 (些) 类型上。
在 mypy 官方里面有多个例子 (延伸阅读链接 4),这里拿一个来举例:
In : def show_type(obj: int | str): # 参数obj是int或者str ...: if isinstance(obj, int): # 实现Type narrowing,mypy确认obj是int ...: return 'int' ...: return 'str' # 由于前面限定了int,所以这里mypy会确认obj是str ...: In : show_type(1) Out: 'int' In : show_type('1') Out: 'str' |
更准确的了解对象的类型对于 mypy 是非常友好的,检查的结论也会更准确。
类型收窄在一些场景下会有问题,再举个例子:
In : def is_str(obj: object) -> bool: ...: return isinstance(obj, str) ...: ...: ...: def to_list(obj: object) -> list[str]: ...: if is_str(obj): ...: return list(obj) ...: return [] ...: In : def is_str(obj: object) -> bool: ...: return isinstance(obj, str) ...: ...: ...: def to_list(obj: object) -> list[str]: ...: if is_str(obj): ...: return list(obj) ...: return [] ...: In : to_list('aaa') Out: ['a', 'a', 'a'] In : to_list(111) Out: [] |
这段代码比 PEP 里面提到的更简单,它们都是正确的代码,类型注释也有问题。但是运行 mypy:
➜ mypy wrong_to_list.py wrong_to_list.py:7: error: No overload variant of "list" matches argument type "object" wrong_to_list.py:7: note: Possible overload variants: wrong_to_list.py:7: note: def [_T] list(self) -> List[_T] wrong_to_list.py:7: note: def [_T] list(self, Iterable[_T]) -> List[_T] Found 1 error in 1 file (checked 1 source file) |
分析一下问题。在 2 个函数中 obj 由于不确定对象类型,所以用了 object,事实上to_list
只会对 obj 为 str 类型做处理。本来if is_str(obj)
会让类型收窄,但是由于被拆分成函数,isinstance 并没有在这里成功收窄。
怎么解决呢?在新版本提供了用户自定的Type Guards
:
from typing import TypeGuard def is_str(obj: object) -> TypeGuard[str]: return isinstance(obj, str) |
本来返回值的类型是 bool,现在我们指定成了TypeGuard[str]
,让 mypy 能理解它的类型。其实换个角度,你可以理解为TypeGuard[str]
是一个带着类型声明的 bool 的别名,请仔细理解这句话。
现在就好啦:
➜ mypy right_to_list.py Success: no issues found in 1 source file |
PEP 612 – Parameter Specification Variables
Python 的类型系统对于 Callable 的类型 (例如函数) 的支持很有限,它只能注明这个 Callable 的类型但是对于函数调用时的参数是无法传播的。这个问题主要存在于装饰器用法上,看一下例子:
from collections.abc import Callable from typing import Any, TypeVar R = TypeVar('R') def log(func: Callable[..., R]) -> Callable[..., R]: def inner(*args: Any, **kwargs: Any) -> R: print('In') return func(*args, **kwargs) return inner @log def join(items: list[str]): return ','.join(items) print(join(['1', '2'])) # 正确用法 print(join([1, 2])) # 错误用法,mypy应该提示类型错误 |
join 接收的参数值应该是字符串列表。但是 mypy 没有正确验证最后这个print(join([1, 2]))
。因为在 log 装饰器中 inner 函数中 args 和 kwargs 的类型都是 Any,这造成调用时选用的参数的类型没有验证,说白了怎么写都可以。在新的版本中可以使用ParamSpec
来解决:
from typing import TypeVar, ParamSpec R = TypeVar('R') P = ParamSpec('P') def log(func: Callable[P, R]) -> Callable[P, R]: def inner(*args: P.args, **kwargs: P.kwargs) -> R: print('In') return func(*args, **kwargs) return inner |
通过使用typing.ParamSpec
,inner 的参数类型直接通过 P.args 和 P.kwargs 传递进来,这样就到了验证的目的。现在再检查就正确了:
➜ mypy right_join.py right_join.py:22: error: List item 0 has incompatible type "int"; expected "str" right_join.py:22: error: List item 1 has incompatible type "int"; expected "str" Found 2 errors in 1 file (checked 1 source file) |
typing.ParamSpec
帮助我们方便【引用】位置和关键字参数,而这个 PEP 另外一个新增的typing.Concatenate
是提供一种添加、删除或转换另一个可调用对象的参数的能力。
我能想到比较常见的添加参数是指【注入】类型的装饰器。比如:
import logging from collections.abc import Callable from typing import TypeVar, ParamSpec, Concatenate logging.basicConfig(level=logging.NOTSET) R = TypeVar('R') P = ParamSpec('P') def with_logger(func: Callable[Concatenate[logging.Logger, P], R]) -> Callable[P, R]: def inner(*args: P.args, **kwargs: P.kwargs) -> R: logger = logging.getLogger(func.__name__) return func(logger, *args, **kwargs) return inner @with_logger def join(logger: logging.Logger, items: list[str]): logger.info('Info') return ','.join(items) print(join(['1', '2'])) print(join([1, 2])) |
join 函数虽然有 2 个参数,但是由于第一个参数logger
是在with_logger
装饰器中【注入】的,所以在使用时只需要传递 items 参数的值即可。
除了添加,删除和转换参数也可用Concatenate
。再看一个删除参数的例子:
from collections.abc import Callable from typing import TypeVar, ParamSpec, Concatenate R = TypeVar('R') P = ParamSpec('P') def remove_first(func: Callable[P, R]) -> Callable[Concatenate[int, P], R]: def inner(a: int, *args: P.args, **kwargs: P.kwargs) -> R: return func(*args, **kwargs) return inner @remove_first def add(a: int, b: int): return a + b print(add(1, 2, 3)) |
使用 remove_first 装饰器后,传入的第一个参数会被忽略,所以add(1, 2, 3)
其实是在计算add(2, 3)
。注意理解这个Concatenate
在的位置,如果是新增,那么Concatenate
加在装饰器参数的 Callable 的类型声明里,如果是删除,加在返回的 Callable 的类型声明里。
注意:Concatenate
目前只在作为Callable
的第一个参数时有效。Concatenate
的最后一个参数必须是一个ParamSpec
。
代码目录
本文代码可以在 mp 项目找到