一、注解中可用的类型
大部分 Python 类型可以在类型提示中使用,不过有一些限制和建议。另外,typing 模块引入的特殊结构,在语义上或许会让你惊讶
1.1 Any 类型
Any 类型是渐进式类型系统的基础,是人们熟知的动态类型。下面是一个没有类型信息的函数
def double(x):
return x * 2
在类型检查工具看来,假定其具有以下类型信息
def double(x: Any) -> Any:
return x * 2
也就是说,x 参数和返回值可以是任何类型,二者甚至可以不同。Any 类型支持所有可能的操作。
以下述签名为例,对比一下 Any 和 object。
def double(x: object) -> object:
这个函数也接受每一种类型的参数,因为任何类型都是 object 的子类型。
然而,类型检查工具拒绝以下函数。
def double(x: object) -> object:
return x * 2
这是因为 object 不支持 mul 操作。Mypy 报告的错误如下所示
.../birds/ $ mypy double_object.py
double_object.py:2: error: Unsupported operand types for * ("object" and "int")
Found 1 error in 1 file (checked 1 source file)
越一般的类型,接口越狭窄,即支持的操作越少。object 类实现的操作比 abc.Sequence少,abc.Sequence 实现的操作比 abc.MutableSequence 少,abc.MutableSequence 实现的操作比 list少。
但是,Any 是一种魔法类型,位于类型层次结构的顶部和底部。Any 既是最一般的类型(使用 n: Any 注解的参数可接受任何类型的值),也是最特定的类型(支持所有可能的操作)。至少,在类型检查工具看来是这样。
当然,没有任何一种类型可以支持所有可能的操作,因此使用 Any 不利于类型检查工具完成核心任务,即检测潜在的非法操作,防止运行时异常导致程序崩溃。
子类型与相容
传统的面向对象名义类型系统依靠的是子类型关系。对 T1 类及其子类 T2 来说,T2 是 T1 的子类型。
class T1:
...
class T2(T1):
...
def f1(p: T1) -> None:
...
o2 = T2()
f1(o2) # 有效
f1(o2)
调用运用了里氏替换原则(Liskov Substitution Principle,LSP)。其实,Barbara Liskov 是从受支持的操作角度定义子类型的:用 T2 类型的对象替换 T1 类型的对象,如果程序的行为仍然正确,那么 T2就是 T1 的子类型。
接着上一段代码,像下面这样做则违背了 LSP。
def f2(p: T2) -> None:
...
o1 = T1()
f2(o1) # 类型错误
从受支持的操作角度来看,这完全合理:作为子类,T2 继承了 T1 支持的所有操作。因此,在任何预期 T1实例的地方都可以使用 T2 实例。然而,反过来就不一定成立了,T2 可能实现了其他方法,因此在预期 T2实例的地方不一定都能使用 T1 实例。行为子类型(behavioral subtyping)这种说法更能体现关注的要点是受支持的操作。行为子类型也用于指代 LSP。
在渐进式类型系统中还有一种关系:相容(consistent-with)。满足子类型关系必定是相容的,不过对 Any还有特殊的规定。
相容规则如下。
-
1. 对 T1 及其子类型 T2,T2 与 T1 相容(里氏替换)。
-
2. 任何类型都与 Any 相容:声明为 Any 类型的参数接受任何类型的对象。
-
3. Any 与任何类型都相容:始终可以把 Any 类型的对象传给预期其他类型的参数。
1.2 简单的类型和类
像 int、float、str 和 bytes 这样的简单的类型可以直接在类型提示中使用。标准库、外部包中的具体类,以及用户定义的具体类(例如 FrenchDeck、Vector2d 和 Duck),也可以在类型提示中使用
抽象基类在类型提示中也能用到。
对类来说,相容的定义与子类型相似:子类与所有超类相容。
1.3 Optional 类型和 Union 类型
from typing import Optional
def show_count(count: int, singular: str, plural: Optional[str] = None) -> str:
Optional[str]
结构其实是Union[str, None]
的简写形式,表示 plural 的类型可以是 str 或None。
从 Python 3.10 开始,Union[str, bytes] 可以写成 str | bytes。这种写法输入的内容更少,也不用从 typing 中导入 Optional 或 Union。下面是以新旧两种句法编写的 show_count 函数的plural 参数的类型提示,可以比较一下。
plural: Optional[str] = None # 旧句法
plural: str | None = None # 新句法
|
运算符还可用于构建 isinstance 和 issubclass 的第二个参数,例如isinstance(x, int | str)
。内置函数 ord 的签名就用到了 Union,其接受 str 或 bytes 类型,并返回一个 int
def ord(c: Union[str, bytes]) -> int: ...
下面示例中的函数接受一个 str,但是可以返回一个 str 或 float。
from typing import Union
def parse_token(token: str) -> Union[str, float]:
try:
return float(token)
except ValueError:
return token
Union[]
至少需要两种类型。嵌套的 Union 类型与扁平的 Union 类型效果相同。因此,下面的类型提示
Union[A, B, Union[C, D, E]]
与下面的类型提示作用一样:
Union[A, B, C, D, E]
Union 所含的类型之间不应相容。例如,Union[int, float]
就“画蛇添足”了,因为 int 与 float 相容。仅使用 float 注解的参数也接受 int 值。
1.4 泛化容器
大多数 Python 容器是异构的。例如,在一个 list 中可以混合存放不同的类型。然而,实际使用中这么做没有什么意义。存入容器的对象往往需要进一步处理,因此至少要有一个通用的方法
泛型可以用类型参数来声明,以指定可以处理的项的类型。
def tokenize(text: str) -> list[str]:
return text.upper().split()
在 Python3.9 及以上版本中,类型提示的意思是 tokenize 函数返回一个 list,而且各项均为 str 类型。stuff: list
和 stuff: list[Any]
这两个注解的意思相同,都表示 stuff 是一个列表,而且列表中的项可以是任何类型的对象。
最初,为了支持泛化类型提示,PEP 484 的作者在 typing 模块中创建了几十种泛型。下图列出了其中一部分。完整的列表参阅 typing 模块文档

1.5 元组类型
元组类型的注解分 3 种形式说明:
-
用作记录的元组;
-
带有具名字段,用作记录的元组;
-
用作不可变序列的元组。
用作记录的元组
元组用作记录时,使用内置类型 tuple
注解,字段的类型在 []
内声明。
举个例子:一个内容为城市名、人口数和所属国家的元组,例如 ('Shanghai', 24.28, 'China')
,类型提示为 tuple[str, float, str]
。
假如有一个函数,其接受的参数是一对地理坐标,返回值是一个 Geohash。该函数的用法如下所示。
>>> shanghai = 31.2304, 121.4737
>>> geohash(shanghai)
'wtw3sjq6q'
带有具名字段,用作记录的元组
如果想注解带有多个字段的元组,或者代码中多次用到的特定类型的元组,强烈建议使用typing.NamedTuple
)。
from typing import NamedTuple
from geolib import geohash as gh # type: ignore
PRECISION = 9
class Coordinate(NamedTuple):
lat: float
lon: float
def geohash(lat_lon: Coordinate) -> str:
return gh.encode(*lat_lon, PRECISION)
用作不可变序列的元组
如果想注解长度不定、用作不可变列表的元组,则只能指定一个类型,后跟逗号和 …(Python 中的省略号,3 个点,不是 Unicode 字符 U+2026,即 HORIZONTAL ELLIPSIS)。
例如,tuple[int, ...]
表示项为 int 类型的元组。
省略号表示元素的数量 ≥ 1。可变长度的元组不能为字段指定不同的类型。stuff: tuple[Any, ...]
和 stuff: tuple
这两个注解的意思相同,都表示 stuff 是一个元
组,长度不定,可包含任意类型的对象。
1.6 泛化映射
泛化映射类型使用MappingType[KeyType, ValueType]
形式注解。在 Python 3.9 及以上版本中,内置类型dict
及collections
和collections.abc
中的映射类型都可以这样注解。更早的版本必须使用 typing.Dict
和 typing
模块中的其他映射类型。
1.7 Iterable
标准库中的 math.fsum 函数,其参数的类型提示用的就是 Iterable。
def fsum(__seq: Iterable[float]) -> float:
1.8 参数化泛型和 TypeVar
参数化泛型是一种泛型,写作 list[T],其中 T 是类型变量,每次使用时会绑定具体的类型。这样可在结果的类型中使用参数的类型。
1.9 静态协议
在 Python 中,协议通过typing.Protocol
的子类定义。然而,实现协议的类不会与定义协议的类建立任何关系,不继承,也不用注册。类型检查工具负责查找可用的协议类型,施行用法检查。
1.10 Callable
collections.abc 模块提供的 Callable 类型(尚未使用 Python 3.9 的用户在 typing 模块中寻找)用于注解回调参数或高阶函数返回的可调用对象。Callable 类型可像下面这样参数化。
Callable[[ParamType1, ParamType2], ReturnType]
参数列表,即这里的 [ParamType1, ParamType2],可以包含零或多个类型。
def repl(input_fn: Callable[[Any], str] = input]) -> None:
常规使用过程中,repl 函数使用 Python 内置函数 input 读取用户输入的表达式。然而,如果是做自动化测试或与其他输入源集成,则 repl 函数会接受一个可选的参数 input_fn。这是一个 Callable,参数类型和返回值类型都与 input 相同。
1.11 NoReturn
这个特殊类型仅用于注解绝不返回的函数的返回值类型。这类函数通常会抛出异常。标准库中有很多这样的函数。
例如,sys.exit() 会抛出 SystemExit,终止 Python 进程
原文始发于微信公众号(Python之家):Python深入-8-函数中的类型提示
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/198393.html