第 8 章 函数中的类型提示
类型提示是可选的,Python 仍将保持其动态类型语言的本质,类型提示不会被强制使用,即使是通过约定俗成的方式。
渐进式类型系统具有可选性(Optional),类型检查器默认不对无类型提示的代码发出警告。当无法推断类型时,对象被隐式视为 Any 类型,Any 与所有类型兼容,允许灵活交互。同时其无运行时影响,类型提示仅用于静态分析(如类型检查器、linter、IDE)。不会在运行时阻止传入错误类型的参数或赋值不兼容的值。
常见类型注解错误
def hex2rgb(color=str) -> tuple[int, int, int]:
问题:color=str 将参数默认值设为 str 类型对象本身,不是类型注解。
正确写法:color: str
使用 None 作为默认值
当可选参数预期为可变类型(如 list、dict)时,必须用 None 作为默认值(避免可变默认值陷阱);即使对不可变类型(如 str),None 有时也更具语义清晰性。
若将 plural 默认值设为 None,需使用 Optional[str]:
from typing import Optional
# Optional[str] 等价于 Union[str, None],表示类型可以是 str 或 None
# Optional 不会使参数可选,真正使其可选的是提供默认值(如 = None)
def show_count(count: int, singular: str, plural: Optional[str] = None) -> str:
if count == 1:
return f'1 {singular}'
count_str = str(count) if count else 'no'
if plural is None:
plural = singular + 's'
return f'{count_str} {plural}'
类型由所支持的操作定义
类型是一组值,以及可对该组值施加的一组函数。在实践中,一个类型的本质特征是它所支持的操作集合,而非其名称或继承关系。Python 运行时只关心对象是否实际支持某操作;类型检查器(如 Mypy)只关心类型注解是否显式声明支持该操作。
实践案例
# double(x) 函数
def double(x):
return x * 2
其合法输入是任何支持 * 2 操作的对象,包括:数值类型:int, complex, Fraction, numpy.uint32 等;序列类型:str, list, tuple, numpy.ndarray 等以及任何实现了 __mul__(self, int) 方法的自定义类型。
# 带 abc.Sequence 注解的版本
from collections import abc
def double(x: abc.Sequence):
return x * 2
此时Mypy 报错:abc.Sequence 未定义 __mul__ 方法,因此不支持 * 操作;但是运行时仍可工作:对 str, list 等具体类型调用成功;关键矛盾在于静态检查基于声明的接口,而运行时基于实际行为。这体现了渐进式类型系统中两种“类型观”的冲突。
鸭子类型(Duck Typing)
核心思想是“如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子。”特点为变量无类型,对象类型由其支持的操作决定;仅在运行时验证操作是否支持;更灵活,但错误可能延迟暴露。其本质是一种隐式的结构类型(structural typing);是Python 原生行为。
名义类型(Nominal Typing)
核心思想是类型由名称和继承关系决定;特点为变量和对象均有显式类型;类型检查基于源码中的类型注解,不执行代码;即使运行时对象支持某操作,若其名义类型未声明,静态检查仍报错;优势在于可在编码或 CI 阶段提前捕获设计错误;常实现于典型语言如Java, C++, C#,Python + 类型注解等。
class Bird:
pass
class Duck(Bird):
def quack(self):
print('Quack!')
# 无类型提示 → 鸭子类型
def alert(birdie):
birdie.quack()
# 名义类型:Duck
def alert_duck(birdie: Duck) -> None:
birdie.quack()
# 名义类型:Bird
# 静态检查会报错 birds.py:16: error: "Bird" has no attribute "quack"
# 因为Bird 类型未声明 .quack() 方法。
def alert_bird(birdie: Bird) -> None:
birdie.quack()
# 运行时三个方法都正常
from birds import *
daffy = Duck()
alert(daffy) # 成功(鸭子类型)
alert_duck(daffy) # 成功(类型匹配)
alert_bird(daffy) # 成功(daffy 是 Bird 子类,运行时有 quack)
# 但是可能存在未来隐患
from birds import *
woody = Bird()
alert(woody) # 运行时报错(无类型提示,Mypy 无法检测)
alert_duck(woody) # Mypy + 运行时报错(类型不匹配)
alert_bird(woody) # Mypy + 运行时报错(函数体逻辑错误)
类型提示可用类型
Any
Any 是渐进式类型系统的基石,代表“动态类型”。未标注类型的函数默认参数和返回值为 Any。
def double(x):
return x * 2
# 等价于
def double(x: Any) -> Any:
return x * 2
Any 支持所有操作,既是最通用类型(可接受任意值),又是最特化类型(可调用任意方法)。
# Any 与 object对比
def double(x: object) -> object:
return x * 2 # MyPy 报错:object 不支持 __mul__
子类型关系
在名义类型系统(nominal type system)中,子类型关系通过类继承显式声明。 若 T2 继承自 T1,则称 T2 是 T1 的子类型。
里氏替换原则
Barbara Liskov提出:
如果类型T2的对象可以无条件替换类型T1的对象,且程序行为依然正确,则T2是T1的子类型。
这强调的是行为兼容性,而非仅仅是语法继承。
行为子类型
子类型必须支持父类型的所有操作(方法、属性等)。子类型可以扩展功能(新增方法),但不能削弱父类型契约(如改变方法语义、缩小前置条件、扩大后置条件等)。
# 合法替换 符合lsp(里氏替换法则)
class T1:
def method(self) -> int:
return 1
class T2(T1):
def extra_method(self) -> str:
return "extra"
def f1(p: T1) -> None:
print(p.method())
o2 = T2()
f1(o2) # 合法:T2 是 T1 的子类型,可替换
# 非法替换 违反 LSP 方向
def f2(p: T2) -> None:
p.extra_method()
o1 = T1()
f2(o1) # 类型错误:T1 不是 T2 的子类型,无法保证支持 extra_method
一致性关系
在渐进式类型系统(如 Python 的 mypy、pyright)中,除了子类型关系,还引入了更宽松的 “一致性”(consistent-with)关系,以支持动态语言的灵活性。
- 若
T2是T1的子类型,则T2与T1一致(即子类型关系 ⇒ 一致性关系)。 - 所有类型都与
Any一致:可将任意类型传给Any参数。 Any与所有类型一致:可将Any类型值传给任意类型参数。
def f3(p: Any) -> None:
...
o0 = object()
o1 = T1()
o2 = T2()
f3(o0) #
f3(o1) #
f3(o2) # 全部合法:规则 #2
def f4(): # 无返回类型注解 → 推断为 Any
return "hello"
o4 = f4() # o4 的类型为 Any
f1(o4) #
f2(o4) #
f3(o4) # 全部合法:规则 #3(Any 可当作任何类型使用)
注意:Any 会绕过类型检查,虽方便但削弱类型安全性。
简单类型与类
在标准的名义类型系统(nominal typing)中,类型关系由显式继承决定,例如:class Dog(Animal): ... ⇒ Dog 是 Animal 的子类型 ⇒ Dog 与 Animal 一致。
但 Python 的内置数值类型彼此之间没有继承关系,都是 object 的直接子类。按“纯名义规则”,int 不应与 float 或 complex 一致。
>>> int.__bases__
(<class 'object'>,)
>>> float.__bases__
(<class 'object'>,)
>>> complex.__bases__
(<class 'object'>,)
**尽管 Python 的 int、float、complex 在类继承上互不相关,但类型检查器会特殊处理它们,允许 int → float → complex 的隐式一致性,以符合数学直觉和日常编程习惯。**这是对“实用性胜过纯粹性”(Practicality beats purity)这一 Python 哲学的体现。
数学上:整数 ⊂ 实数 ⊂ 复数。操作上所有 float 支持的操作(如 +, -, *, /, .real, .imag 等),int 都支持。虽然 int 还有额外操作(如位运算),但这不影响它能当 float 用。
现实代码中,我们经常写:
def compute(x: float) -> float:
return x * 2.5
compute(3)
如果类型检查器报错,反而会阻碍可用性。
一般情况下:类之间的一致性 = 子类型关系;但数值类型是例外,即使没有继承,也认为它们一致。这是渐进式类型系统对动态语言现实的妥协与优化,也是“一致性关系”比“子类型关系”更灵活的体现。
Optional 与 Union
在静态类型检查中,有时一个变量、参数或返回值可能属于多个不同类型之一。Python 的类型系统通过以下两种机制支持这种“多选一”的语义:
Union[T1, T2, ...]:表示值可以是T1、T2等类型中的任意一种。Optional[T]:是Union[T, None]的简写,表示“可能是T,也可能是None”。
旧语法
# 旧语法 必须从 `typing` 模块导入 `Union` 和 `Optional`。
from typing import Union, Optional
def parse_token(token: str) -> Union[str, float]:
try:
return float(token)
except ValueError:
return token
def show_count(n: int, plural: Optional[str] = None) -> None:
...
# 语法冗长,尤其在嵌套泛型中
def process(items: List[Union[str, int]]) -> Dict[str, Union[float, None]]:
...
新语法
- 使用
T1 | T2代替Union[T1, T2] - 使用
T | None代替Optional[T]
| 旧语法 | 新语法 |
|---|---|
Optional[str] | `str |
Union[str, bytes] | `str |
List[Union[int, str]] | `List[int |
Dict[str, Union[float, None]] | `Dict[str, float |
新语法无需导入,| 是内置语法,不依赖 typing 模块。更简洁、可读性更强,接近自然语言“或”的表达。支持运行时使用,可用于 isinstance 和 issubclass。
x = "hello"
print(isinstance(x, str | bytes)) # True
y = 42
print(isinstance(y, int | str)) # True
print(issubclass(bool, int | float)) # True
int | str 在运行时会创建一个特殊的 types.UnionType 对象,与 typing.Union[int, str] 等价。
何时应避免使用 Union
存在一致性关系
当类型之间已存在一致性关系时应该进行避免。
例如,根据 PEP 484 的“数值塔”规则int 与 float 一致;float 与 complex 一致;因此 int 与 complex 也一致。所以以下写法是冗余的
def f(x: Union[int, float]) -> float: ... # 冗余
应简化为:
def f(x: float) -> float: ... # 因为 int 可自动传入
协变场景
当返回类型依赖输入类型时(协变场景)应避免使用。
例如:函数接受 str 返回 str,接受 bytes 返回 bytes。
# Union 无法表达“输入输出类型绑定”关系
def normalize(s: str | bytes) -> str | bytes:
return s.lower() # 但 .lower() 对两者都有效
但调用者无法知道返回值具体是 str 还是 bytes,必须手动检查。
正确做法是使用 TypeVar 或 @overload
from typing import TypeVar, overload
# 方案1:TypeVar(泛型)
AnyStr = TypeVar('AnyStr', str, bytes)
def normalize(s: AnyStr) -> AnyStr:
return s.lower()
# 方案2:重载
@overload
def normalize(s: str) -> str: ...
@overload
def normalize(s: bytes) -> bytes: ...
def normalize(s):
return s.lower()
Union 的语义规则
Union至少需要两个类型。Union[int] 是非法的(应直接写 int),int | int 会被自动简化为 int。同时嵌套 Union 会自动扁平化
Union[A, B, Union[C, D, E]] ≡ Union[A, B, C, D, E]
A | B | (C | D | E) ≡ A | B | C | D | E
泛型集合
Python 的原生集合(如 list、set)本质上是异构的(heterogeneous)——可以混合任意类型对象:
mixed = [42, "hello", None, lambda x: x]
但在实际开发中,通常希望集合中的元素具有统一类型,以便安全地调用公共方法(如 .upper()、.append() 等)。 这正是泛型类型提示(Generic Type Hints)要解决的问题。
泛型允许声明: “这个列表只包含 str” → list[str] “这个集合只包含 int” → set[int]
现代语法
从 Python 3.9 起,内置集合类型直接支持泛型语法,无需导入 typing 模块。
def tokenize(text: str) -> list[str]:
return text.upper().split()
旧语法
from typing import List
def tokenize(text: str) -> List[str]:
return text.upper().split()
当前缺陷
# 未参数化的泛型默认元素类型为 Any
stuff: list # 等价于 list[Any]
items: list[str] # 明确限定元素为 str
import array
a = array.array('B', [1, 2, 3])
array.array 是 Python 内置模块 array 提供的一个类,用于创建存储单一类型数值的连续内存块。与 Python 内置的 list 不同:list 可以存储任意类型对象(异构),每个元素都是指针,内存开销大。array.array 只能存储同一种底层 C 类型的数值(如整数、浮点数),内存更紧凑,访问更快。适用于性能敏感或内存受限的场景(如科学计算、嵌入式、图像处理等)。
array.array(typecode, initializer) 的第一个参数是 typecode,它是一个单字符字符串,用于指定数组中元素的底层数据类型。
| typecode | C 类型 | Python 类型 | 取值范围 | 字节大小 |
|---|---|---|---|---|
'B' | unsigned char | int | 0 到 255 | 1 字节 |
| typecode | 含义 | 范围(典型) |
|---|---|---|
'b' | 有符号字节 | -128 ~ 127 |
'H' | 无符号短整型 | 0 ~ 65535 |
'i' | 有符号整型 | -2³¹ ~ 2³¹−1 |
'f' | 单精度浮点数 | IEEE 754 float |
'd' | 双精度浮点数 | IEEE 754 doublehttps://docs.python.org/3/library/array.html) |
第二个参数是一个可迭代对象(如 list、tuple、generator 等),用于初始化数组元素。元素必须能转换为 typecode 指定的类型,否则会报错。
# 合法初始化
a = array.array('B', [0, 100, 255]) # 全在 0~255 范围内
# 非法初始化
a = array.array('B', [256]) # OverflowError: unsigned byte integer is greater than maximum
a = array.array('B', [-1]) # OverflowError: unsigned byte integer is less than minimum
a = array.array('B', ['a']) # TypeError: an integer is required (got type str)
尽管知道 'B' 表示 0~255 的整数,但 Python 的静态类型系统(如 mypy)目前无法在类型层面表达这种“值域约束”。
def process(arr: array.array) -> None:
arr.append(300) # 运行时会抛 OverflowError,但类型检查器无法提前发现
元组
元组(tuple)在 Python 中用途广泛,可作为固定结构的记录、带命名字段的数据结构,或不可变序列。类型系统为此提供了三种不同的注解方式,分别对应不同使用场景。
作为记录的元组
用于元组表示一个固定结构的数据项,每个位置有明确语义(如坐标、人名+年龄+城市等)。字段数量和类型在编译时已知且固定。
tuple[Type1, Type2, ..., TypeN]
# 表示 (城市名, 人口(百万), 国家)
city_info: tuple[str, float, str] = ('Shanghai', 24.28, 'China')
# 地理坐标
def geohash(lat_lon: tuple[float, float]) -> str:
return gh.encode(*lat_lon, PRECISION)
版本兼容性
# >=3.9可以直接使用内置 tuple
# < 3.9需使用如下导入
from typing import Tuple
def geohash(lat_lon: Tuple[float, float]) -> str: ...
NamedTuple
用于元组结构被多次使用或字段语义需显式命名(提高可读性与维护性)。需要访问 .field_name 而非仅靠索引(如 coord.lat 而非 coord[0])。
from typing import NamedTuple
class Coordinate(NamedTuple):
lat: float
lon: float
def geohash(lat_lon: Coordinate) -> str:
return gh.encode(lat_lon.lat, lat_lon.lon, PRECISION)
具有可读性强,字段有名字,代码自文档化;以及兼容元组的优势。Coordinate 是 tuple 的子类,因此Coordinate 实例可传递给期望 tuple[float, float] 的函数;反之不成立,普通 tuple 不能当作 Coordinate 使用。
作为不可变序列的元组
适用于元组用作长度可变但元素类型统一的不可变列表(如函数返回多个同类型结果)。类似 list[T],但不可变。
tuple[ElementType, ...]
... 是 Python 的省略号字面量(Ellipsis),必须是三个英文句点,不能用 Unicode 省略号。表示“任意数量(≥0)的 ElementType 元素”。
# 任意长度的整数元组
numbers: tuple[int, ...] = (1, 2, 3, 4, 5)
# columnize 函数返回:每行是一个字符串元组
def columnize(
sequence: Sequence[str], num_columns: int = 0
) -> list[tuple[str, ...]]:
...
tuple(无参数) ≡ tuple[Any, ...]:任意长度、任意类型的元组。无法表达“异构但长度可变”的元组(如 (str, int, str, int, ...)),这是类型系统的限制。
注意:tuple[int, ...] 允许空元组 (),因为“任意数量”包含 0。
泛型映射
在类型提示中,映射类型(如字典)使用以下泛型语法:
MappingType[KeyType, ValueType]
KeyType:键的类型(如str、int)ValueType:值的类型(如set[str]、list[int])
# python 3.9及以上
dict[str, set[str]]
# python 3.8及以下
from typing import Dict, Set
Dict[str, Set[str]]
抽象基类
“发送时保守,接收时宽容。”
该原则指导我们在设计函数接口时:
- 参数类型应尽可能宽泛(使用抽象基类),以提高调用灵活性;
- 返回类型应具体明确,以提供清晰的契约。
参数类型使用抽象基类
# 推荐操作
from collections.abc import Mapping
def name2hex(name: str, color_map: Mapping[str, int]) -> str:
...
接受任意 Mapping 实现:dict、defaultdict、ChainMap、UserDict 子类等;不强制调用者使用特定具体类型;符合“接收时宽容”。
# 不推荐操作
def name2hex(name: str, color_map: dict[str, int]) -> str:
...
这会导致UserDict 及其子类会被 MyPy 拒绝,因为 UserDict 不是 dict 的子类(二者均为 MutableMapping 的子类型);限制了调用者的实现选择。
返回类型提示使用具体类型
def tokenize(text: str) -> list[str]:
return text.upper().split()
函数实际返回的是具体对象(如 list);具体类型提供更强的契约保证;符合“发送时保守”。
兼容性说明
Python ≥ 3.9可以直接使用内置类型和 collections.abc 的泛型形式,如 list[str]、Mapping[str, int]。Python ≤ 3.8则需使用 typing 模块中的泛型别名,如 List[str]、Dict[str, int]。
数字类型提示
numbers 模块实现了 PEP 3141 定义的数字塔(numeric tower):
Number
└─ Complex
└─ Real
└─ Rational
└─ Integral
其在运行时有效,可用于 isinstance(x, numbers.Real) 等检查;但是不支持静态类型检查。
如下是一些替代方案
# 使用具体内置类型
# 静态检查器自动允许 int 传入 float 参数,int/float 传入 complex 参数。
def sqrt(x: float) -> float: ...
# 使用联合类型
from typing import Union
from decimal import Decimal
from fractions import Fraction
def process_number(x: Union[float, Decimal, Fraction]) -> float: ...
# 使用协议
from typing import SupportsFloat
def convert(x: SupportsFloat) -> float: ...
Iterable
在函数参数的类型提示中,推荐使用抽象容器类型如 collections.abc.Iterable 或 Sequence,而非具体类型(如 list)。 例如,标准库函数 math.fsum 的签名如下:
from collections.abc import Iterable
# 参数名前的双下划线(如 `__seq`)是 PEP 484 中对**仅限位置参数**(positional-only)的命名约定
def fsum(__seq: Iterable[float]) -> float: ...
该函数接受任何可迭代的浮点数序列(包括生成器、列表、元组等),体现了对输入形式的灵活性。
from collections.abc import Iterable
# 类型别名:提升可读性
FromTo = tuple[str, str]
# 参数 changes 的类型为 Iterable[FromTo],允许传入列表、元组、生成器等。
def zip_replace(text: str, changes: Iterable[FromTo]) -> str:
for from_, to in changes:
text = text.replace(from_, to)
return text
l33t = [('a', '4'), ('e', '3'), ('i', '1'), ('o', '0')]
text = 'mad skilled noob powned leet'
result = zip_replace(text, l33t)
print(result) # 输出: m4d sk1ll3d n00b p0wn3d l33t
显示类型别名
Python 3.10 起,推荐使用 typing.TypeAlias 显式声明类型别名,以帮助类型检查器更好地区分变量赋值与类型定义:
from typing import TypeAlias
FromTo: TypeAlias = tuple[str, str]
这种方式在处理前向引用或复杂类型时可避免歧义,并提供更准确的错误提示。
Iterable 与 Sequence 的使用场景对比
| 类型 | 特点 | 适用场景 |
|---|---|---|
Iterable | 只需支持 for 循环遍历;可为无限(如生成器) | 函数需逐项处理输入,且不依赖长度或随机访问 |
Sequence | 支持 len()、索引访问(__getitem__) | 函数需预先知道长度或多次访问元素 |
注意:
- 若函数需遍历整个
Iterable才能返回结果,则传入无限可迭代对象(如itertools.cycle)会导致程序挂起或内存耗尽。
- 避免将
Iterable用作返回类型,因其过于宽泛。
参数化泛型与 TypeVar
参数化泛型(如 Sequence[T])允许函数的输入与输出类型保持一致,其中 T 是一个类型变量(TypeVar),在每次调用时绑定到具体类型。
from collections.abc import Sequence
from random import shuffle
from typing import TypeVar
T = TypeVar('T')
def sample(population: Sequence[T], size: int) -> list[T]:
if size < 1:
raise ValueError('size must be >= 1')
result = list(population)
shuffle(result)
return result[:size]
若传入 Sequence[int],返回 list[int];若传入 str(视为 Sequence[str]),返回 list[str]。这体现了类型一致性:输入与输出共享同一类型变量 T。
为何需要 TypeVar?
Python 的泛型语法(如 Sequence[T])依赖于 typing 模块的元编程机制。由于 Python 解释器本身不原生支持泛型语法,T 必须通过 TypeVar 显式声明,以便类型检查器识别其为类型占位符。其他语言(如 Java、TypeScript)无需显式声明类型变量,因其泛型是语言内建特性。
statistics.mode
标准库函数 statistics.mode 返回可迭代对象中最常见的元素。理想情况下,其返回类型应与输入元素类型一致。
# 误做法:固定类型
# 仅适用于 float,无法处理 int、str、Decimal 等。
def mode(data: Iterable[float]) -> float: ...
# 正确做法:使用 TypeVar
# 但此签名过于宽泛
T = TypeVar('T')
def mode(data: Iterable[T]) -> T: ...
约束 TypeVar
受限类型变量
通过位置参数限定 T 只能是指定类型之一:
NumberT = TypeVar('NumberT', float, Decimal, Fraction)
def mode(data: Iterable[NumberT]) -> NumberT: ...
优点是可以精确控制类型集合;缺点是无法覆盖所有合法类型(如 str、自定义可哈希类),同时命名易误导(如 NumberT 含 str 不合理)。
有界类型变量
使用 bound= 参数指定类型上界:
from collections.abc import Hashable
HashableT = TypeVar('HashableT', bound=Hashable)
def mode(data: Iterable[HashableT]) -> HashableT:
pairs = Counter(data).most_common(1)
if not pairs:
raise ValueError('no mode for empty data')
return pairs[0][0]
HashableT 可以是 Hashable 的任意子类型(如 int, str, tuple 等);返回类型保留具体类型(而非泛化的 Hashable),使类型检查更精确。
预定义类型变量
typing 模块内置了一个常用 TypeVar:
AnyStr = TypeVar('AnyStr', str, bytes)
用于编写同时支持 str 和 bytes 的函数,且保证输入输出类型一致:
# 保证输入输出类型一致
def concat(a: AnyStr, b: AnyStr) -> AnyStr:
return a + b
# 都是str
concat("a", "b") # OK → str
# 都是bytes
concat(b"a", b"b") # OK → bytes
# str与bytes混用
concat("a", b"b") # 类型错误
静态协议
Python 从诞生起就支持鸭子类型(Duck Typing):只要对象实现了所需方法,就能被使用,无需显式继承或声明。然而,这种“隐式协议”对静态类型检查器不可见。typing.Protocol提供了一种机制,使开发者能显式定义结构化接口,从而实现静态鸭子类型,即在保留 Python 动态灵活性的同时,让类型检查器也能验证接口兼容性。
案例
目标:实现一个泛型函数 top(series, n),返回可迭代对象中最大的 n 个元素。
def top(series: Iterable[T], length: int) -> list[T]:
ordered = sorted(series, reverse=True)
return ordered[:length]
但 T 不能是任意类型(如 object),因为 sorted() 要求元素支持 < 比较(即实现 __lt__ 方法)。若元素无 __lt__,调用 sorted() 会抛出 TypeError: '<' not supported...。因此,需约束 T 为“支持 < 比较”的类型。
由于标准库中没有现成的“可比较”抽象基类(Hashable 对应 __hash__,但无 __lt__ 的等价物),因此自定义一个协议
from typing import Protocol, Any
# 任何实现了 `__lt__` 且签名匹配的类,都**自动与该协议一致**,**无需继承或注册*
class SupportsLessThan(Protocol):
def __lt__(self, other: Any) -> bool: ...
from collections.abc import Iterable
from typing import TypeVar
from comparable import SupportsLessThan
LT = TypeVar('LT', bound=SupportsLessThan)
def top(series: Iterable[LT], length: int) -> list[LT]:
ordered = sorted(series, reverse=True)
return ordered[:length]
bound=SupportsLessThan 表示:LT 必须是 SupportsLessThan 的子类型(即实现 __lt__);返回类型 list[LT] 保留输入元素的具体类型(如 list[tuple[int, str]])。
协议与抽象基类
| 特性 | 协议(Protocol) | ABC(如 Hashable) |
|---|---|---|
| 实现方式 | 结构化子类型:只要方法签名匹配即兼容 | 名义子类型:必须显式继承或注册 |
| 侵入性 | 无侵入:可对第三方/内置类型生效 | 需修改类定义或调用 register() |
| 类型检查 | 支持静态验证 | 同样支持,但需显式声明关系 |
例如:str、tuple、int 等无需任何改动,即可用于 SupportsLessThan,因为它们天然实现 __lt__。
静态鸭子类型
传统鸭子类型在运行时靠异常暴露问题;静态鸭子类型通过 Protocol,在编码阶段就让类型检查器理解“只要实现这些方法即可”;这使得 Python 在保持动态语言灵活性的同时,获得接近静态语言的类型安全保障。
Callable
Callable 用于注解可调用对象(如函数、方法、lambda 等),特别适用于高阶函数或回调函数的类型提示。自 Python 3.9 起,应从 collections.abc 导入;旧版本可从 typing 导入。
# 参数列表 [...] 中包含零个或多个参数类型
# 返回类型为单个类型
Callable[[ParamType1, ParamType2, ...], ReturnType]
实例
from typing import Any
from collections.abc import Callable
# 默认使用内置 input 函数
# 为支持测试或集成,允许传入具有相同签名的替代函数
def repl(input_fn: Callable[[Any], str] = input) -> None:
...
input 的签名为 def input(__prompt: Any = ...) -> str,与 Callable[[Any], str] 兼容。
当回调函数签名不确定(如可变参数、关键字参数等),可使用:
Callable[..., ReturnType]
注意:Callable 无法精确表达带有可选参数、仅关键字参数或重载函数的签名。对于复杂签名,应使用 Protocol 定义带 __call__ 方法的协议。
Callable 中的变型
在类型系统中,变型描述的是:当类型 A 是类型 B 的子类型时,某种泛型容器 G[A] 和 G[B] 之间是否也存在子类型关系。
有三种情况:
| 变型类型 | 含义 |
|---|---|
| 协变(covariant) | 如果 A ⊑ B,那么 G[A] ⊑ G[B](方向相同) |
| 逆变(contravariant) | 如果 A ⊑ B,那么 G[B] ⊑ G[A](方向相反) |
| 不变(invariant) | 即使 A ⊑ B,G[A] 和 G[B] 也没有子类型关系 |
符号 ⊑ 表示“是……的子类型”,比如 int ⊑ float。
from collections.abc import Callable
def update(
probe: Callable[[], float], # 无参,返回 float
display: Callable[[float], None] # 接受 float,返回 None
) -> None:
temperature = probe()
display(temperature)
# 合法
# update 要求:Callable[[], float] probe_ok 实际:Callable[[], int]
# int 是 float 的子类型 根据协变规则 返回类型越具体 越安全 所以合法
def probe_ok() -> int:
return 42
# 非法
# update 要求 display: Callable[[float], None]
# display_wrong 实际:Callable[[int], None]
# 但是实际的调用过程中可能调用 display_wrong(3.14)
# 此时就会抛出 TypeError: 'float' object cannot be interpreted as an integer
# 可以用能处理 float 的函数代替只要求处理 int 的函数,但不能用只能处理 int 的函数代替需要处理 float 的函数
def display_wrong(temperature: int) -> None:
print(hex(temperature))
# 合法
# update 要求 Callable[[float], None]
# display_ok 是 allable[[complex], None]
# 因为 float ⊑ complex,
# 所以根据逆变规则:Callable[[complex], None] ⊑ Callable[[float], None]
def display_ok(temperature: complex) -> None:
print(temperature)
# 类型错误:display_wrong 不兼容
update(probe_ok, display_wrong)
# OK:probe_ok 和 display_ok 都兼容
update(probe_ok, display_ok)
返回值要更具体(协变),参数要更宽泛(逆变)
| 位置 | 变型类型 | 要求 | 例子 |
|---|---|---|---|
| 返回类型 | 协变 | 实际返回类型 ⊑ 声明类型 | int → float ✅ |
| 参数类型 | 逆变 | 声明参数类型 ⊑ 实际参数类型 | float → complex ✅ |
列表应该是不变的
# 这应该是不变的
scores: list[float]
# 错误操作
scores = [1, 2, 3] # list[int]
scores = [1+2j, 3+4j] # list[complex]
如果允许 list[int] → list[float](协变),则可能会往里面塞一个 3.14,但 list[int] 不能存 float → 运行时错误。如果允许 list[complex] → list[float](逆变),可能读出来一个 1+2j,但代码以为它是 float,结果调用 .real 可能没问题,但如果要排序(complex 不可比较),就崩溃了。所以为了安全,list[T] 被设计为不变:list[int] 和 list[float] 完全无关。
只读容器(如 Sequence[T])通常是协变的,因为不能写入,不会破坏类型安全。
NoReturn
这个特殊类型仅用于注解绝不返回的函数的返回值类型。这类函数通常会抛出异常。
def exit(__status: object = ...) -> NoReturn: ...
标注仅限位置参数和可变参数
新语法
# 案例
from typing import Optional
def tag(
# name: str:仅限位置参数,必须为 str 类型。
name: str,
/,
# *content: str:任意数量的位置参数,每个都必须是 str
# 函数体内 content 的类型为 tuple[str, ...]
*content: str,
# class_: Optional[str] = None 关键字参数,可为 str 或 None
class_: Optional[str] = None,
# **attrs: str:任意关键字参数,值必须为 str
# 函数体内 attrs 的类型为 dict[str, str]
**attrs: str,
) -> str:
...
兼容语法
根据约定,在不支持 / 语法的旧版本中,应将仅限位置参数命名为以双下划线开头(如 __name)
from typing import Optional
def tag(__name: str, *content: str, class_: Optional[str] = None,
**attrs: str) -> str:
...

被折叠的 条评论
为什么被折叠?



