第八章 函数中的类型提示

第 8 章 函数中的类型提示

类型提示是可选的,Python 仍将保持其动态类型语言的本质,类型提示不会被强制使用,即使是通过约定俗成的方式。

渐进式类型系统具有可选性(Optional),类型检查器默认不对无类型提示的代码发出警告。当无法推断类型时,对象被隐式视为 Any 类型,Any 与所有类型兼容,允许灵活交互。同时其无运行时影响,类型提示仅用于静态分析(如类型检查器、linter、IDE)。不会在运行时阻止传入错误类型的参数或赋值不兼容的值。

常见类型注解错误

def hex2rgb(color=str) -> tuple[int, int, int]:

问题color=str 将参数默认值设为 str 类型对象本身,不是类型注解

正确写法color: str

使用 None 作为默认值

当可选参数预期为可变类型(如 listdict)时,必须用 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,则称 T2T1 的子类型

里氏替换原则

Barbara Liskov提出:
如果类型 T2 的对象可以无条件替换类型 T1 的对象,且程序行为依然正确,则 T2T1 的子类型。

这强调的是行为兼容性,而非仅仅是语法继承。

行为子类型

子类型必须支持父类型的所有操作(方法、属性等)。子类型可以扩展功能(新增方法),但不能削弱父类型契约(如改变方法语义、缩小前置条件、扩大后置条件等)。

# 合法替换 符合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 的 mypypyright)中,除了子类型关系,还引入了更宽松的 “一致性”(consistent-with)关系,以支持动态语言的灵活性。

  • T2T1 的子类型,则 T2T1 一致(即子类型关系 ⇒ 一致性关系)。
  • 所有类型都与 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): ...DogAnimal 的子类型 ⇒ DogAnimal 一致。

但 Python 的内置数值类型彼此之间没有继承关系,都是 object 的直接子类。按“纯名义规则”,int 不应与 floatcomplex 一致。

>>> int.__bases__
(<class 'object'>,)
>>> float.__bases__
(<class 'object'>,)
>>> complex.__bases__
(<class 'object'>,)

**尽管 Python 的 intfloatcomplex 在类继承上互不相关,但类型检查器会特殊处理它们,允许 int → float → complex 的隐式一致性,以符合数学直觉和日常编程习惯。**这是对“实用性胜过纯粹性”(Practicality beats purity)这一 Python 哲学的体现。

数学上:整数 ⊂ 实数 ⊂ 复数。操作上所有 float 支持的操作(如 +, -, *, /, .real, .imag 等),int 都支持。虽然 int 还有额外操作(如位运算),但这不影响它能当 float 用。

现实代码中,我们经常写:

def compute(x: float) -> float:
    return x * 2.5

compute(3) 

如果类型检查器报错,反而会阻碍可用性

一般情况下:类之间的一致性 = 子类型关系但数值类型是例外,即使没有继承,也认为它们一致。这是渐进式类型系统对动态语言现实的妥协与优化,也是“一致性关系”比“子类型关系”更灵活的体现。

OptionalUnion

在静态类型检查中,有时一个变量、参数或返回值可能属于多个不同类型之一。Python 的类型系统通过以下两种机制支持这种“多选一”的语义:

  • Union[T1, T2, ...]:表示值可以是 T1T2 等类型中的任意一种。
  • 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 模块。更简洁、可读性更强,接近自然语言“或”的表达。支持运行时使用,可用于 isinstanceissubclass

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 的“数值塔”规则intfloat 一致;floatcomplex 一致;因此 intcomplex 也一致。所以以下写法是冗余的

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 的原生集合(如 listset)本质上是异构的(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,它是一个单字符字符串,用于指定数组中元素的底层数据类型

typecodeC 类型Python 类型取值范围字节大小
'B'unsigned charint0 到 2551 字节
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)

具有可读性强,字段有名字,代码自文档化;以及兼容元组的优势。Coordinatetuple 的子类,因此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:键的类型(如 strint
  • 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 实现:dictdefaultdictChainMapUserDict 子类等;不强制调用者使用特定具体类型;符合“接收时宽容”。

# 不推荐操作
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.IterableSequence,而非具体类型(如 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]

这种方式在处理前向引用或复杂类型时可避免歧义,并提供更准确的错误提示。

IterableSequence 的使用场景对比

类型特点适用场景
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、自定义可哈希类),同时命名易误导(如 NumberTstr 不合理)。

有界类型变量

使用 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)

用于编写同时支持 strbytes 的函数,且保证输入输出类型一致:

# 保证输入输出类型一致
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]])。

协议与抽象基类
特性协议(ProtocolABC(如 Hashable
实现方式结构化子类型:只要方法签名匹配即兼容名义子类型:必须显式继承或注册
侵入性无侵入:可对第三方/内置类型生效需修改类定义或调用 register()
类型检查支持静态验证同样支持,但需显式声明关系

例如:strtupleint 等无需任何改动,即可用于 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 ⊑ BG[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)

返回值要更具体(协变),参数要更宽泛(逆变)

位置变型类型要求例子
返回类型协变实际返回类型 ⊑ 声明类型intfloat
参数类型逆变声明参数类型 ⊑ 实际参数类型floatcomplex
列表应该是不变的
# 这应该是不变的
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:
    ...
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值