Typing 泛型--进阶类型
Typing 泛型--进阶类型
TypeVar
TypeVar(类型变量)是 Python 类型提示(Type Hints) 系统中用于定义泛型(Generics) 的核心工具。它允许你创建一个“占位符”类型,这个占位符可以在函数、类或方法被使用时,被具体的类型所替换,从而让代码在保持类型安全的同时,又能拥有灵活性
简单来说: TypeVar 就是类型变量,可以泛指任何类型,为任何类型进行标注。听着与 Any 一样,但是本质上是不同的
1. 为什么需要 TypeVar?
举一个简单的例子,想象有一个函数,它返回传入的列表第一个元素:
def get_first_item(items):
return items[0]
给它加类型提示,你会想到使用以下两种方法
def get_first_item(items: list[str]) -> str:def get_first_item(items: list) -> Any:
那假设,我需要传入别的列表数据呢,比如 [1,2,3] ,那么很显然第一种方式,就不适用了,而且第二种方式就会的类型概念就会很模糊,没有一种类型的逻辑连贯性,我的函数初衷,就是要表示无论传入的列表是什么类型的列表,[1,2,3] 或者 ['str',1,1.2,[1,2]] 等等数据都好,返回的数据都是列表中的对应元素的类型,所以这个时候就需要泛型
from typing import TypeVar, List
# 创建一个名为 'T' 的类型变量
T = TypeVar('T') # 名字可以任意,但通常用大写字母,如 T, Key, Value, etc.
def get_first_item(items: List[T]) -> T:
return items[0]
# 此时,类型检查器推断出 T = str
# 因此 items 是 List[str],返回值是 str
result_str = get_first_item(["hello", "world"])
reveal_type(result_str) # 类型为: str
# 此时,类型检查器推断出 T = int
# 因此 items 是 List[int],返回值是 int
result_int = get_first_item([1, 2, 3])
reveal_type(result_int) # 类型为: int
使用 TypeVar 定义泛型类型,泛指任何类型,类型检查器会自动根据传入数据吗,推断出对应的数据,而使用Any,类型检查是会直接跳过不进行检查过滤,而且 TypeVar 功能更加多元化和灵活。
2. 约束(Constraints)和绑定(Bound)
约束(
constraints):TypeVar必须是给定的几个具体类型之一。from typing import TypeVar # 定义类型变量 NumT,它只能是 int 或 float 类型 NumT = TypeVar('NumT', int, float) def add(a: NumT, b: NumT) -> NumT: return a + b add(1, 2) # 正确,T 是 int add(1.5, 2.3) # 正确,T 是 float add("a", "b") # 错误!类型检查器会报错,str 不在约束 (int, float) 中绑定(
bound):TypeVar必须是绑定类型的子类。这比约束更灵活from typing import TypeVar, List class Animal: def speak(self): ... class Dog(Animal): def bark(self): ... class Cat(Animal): def meow(self): ... # 任何 Animal 或其子类都可以作为 A 的值 A = TypeVar('A', bound=Animal) def make_animal_speak(animal: A) -> A: animal.speak() # 因为 bound=Animal,所以可以安全调用 Animal 的方法 return animal dog = Dog() returned_dog = make_animal_speak(dog) # 类型检查器知道 returned_dog 是 Dog 类型 returned_dog.bark() # 正确,因为返回的类型保持了具体的 Dog 类型,而不仅仅是 Animal
3. 类型的统一性
使用 TypeVar 定义泛型类型,也可以让我们的类型提示更加的具有统一性和复用性,可以对同样类型输出和输入进行归类和整合
例如:
from typing import TypeVar,Union
T = TypeVar('T')
NumT = TypeVar('NumT', int, float)
# T 表示 参数和返回可以为任何类型的值,但是他们的类型必须是一致的
def use_typevar(a: T, b: T) -> T:
return a + b # 这里可能会报错(如果T不支持+操作),但这是另一个问题
# NumT 表示只能 int和float 其中一种
def use_typevar_constraints(a: NumT, b: NumT) -> NumT:
return a + b
# 一个地方定义,可以在程序中所有位置任何使用的这个泛型
def use_typevar_(a:T,b:NumT) -> T
return a
# 不使用泛型
def unuse_typevar_int(a:int, b:int) -> int:
return a + b
def unuse_typevar_str(a:str, b:str) -> str:
return a + b
def unuse_typevar_constraints(a:Union[int,float], b:Union[int,float]) -> Union[int,float]:
return a + b
......
result = use_typevar(10, "20") # 类型检查器立即报错:类型 T 同时是 int 和 str,矛盾!
result = use_typevar(10, 20) # 正确
result = use_typevar("10", "20") # 正确
result = use_typevar([10], [20]) # 正确
在常用的编辑器 ide 中,也可以很好的支持

无需运行程序,就能清楚的知道,对应的返回值是什么类型的数据
Generic
Generic 是 Python typing 模块中的一个特殊基类,用于正式声明一个类为泛型类。它充当一个“锚点”,让你能够将类型参数(TypeVar) 与类关联起来,从而让类型检查器理解这个类的泛型结构。
为什么需要 Generic 基类
没有 Generic 基类,类型检查器就无法识别你自定义类中的 TypeVar。
例子:
from typing import TypeVar
T = TypeVar('T')
class Box: # 注意:这里没有继承 Generic
def __init__(self, content: T): # 这里使用 T
self.content = content
def get_content(self) -> T: # 这里也使用 T
return self.content
对于上面的代码,类型检查器(如 mypy)会感到困惑:
错误:T 在这个上下文中是无效的。 因为 Box 不是一个泛型类,所以它不能使用类型参数 T。
T 只是一个孤立的类型变量,它没有与任何类绑定。Generic 基类的作用就是提供这种绑定。
如何使用 Generic 基类?
1. 单个类型参数
正确的用法是让你的类继承 Generic,并在方括号中传入一个或多个类型参数。
from typing import TypeVar, Generic
T = TypeVar('T') # 步骤 1: 定义类型变量
class Box(Generic[T]): # 步骤 2: 继承 Generic[T],将类声明为泛型类
def __init__(self, content: T):
self.content = content
def get_content(self) -> T:
return self.content
# 步骤 3: 使用!现在可以参数化了
int_box: Box[int] = Box(123) # T 被具体化为 int
str_box: Box[str] = Box("hello") # T 被具体化为 str
2. 多个类型参数
泛型类可以有多个类型参数,非常适用于像字典这样的映射结构
from typing import TypeVar, Generic
K = TypeVar('K') # 键的类型变量
V = TypeVar('V') # 值的类型变量
class Pair(Generic[K, V]): # 继承 Generic[K, V]
def __init__(self, key: K, value: V):
self.key = key
self.value = value
def get_pair(self) -> tuple[K, V]:
return (self.key, self.value)
# 使用
name_id: Pair[str, int] = Pair("Alice", 1) # K=str, V=int
coord: Pair[float, float] = Pair(10.5, 20.3) # K=float, V=float
Generic 的工作原理和重要性
- 元编程魔法:Generic 是一个特殊类,它利用 Python 的元类机制。当你写
class Box(Generic[T]):时,Generic 的元类会记录下这个类有一个名为 T 的类型参数。 - 创建参数化类型:当你使用
Box[int]时,实际上发生的是:- Python 和类型检查器会查找 Box 的原始定义。
- 它们看到 Box 继承自
Generic[T],知道 T 是一个类型参数。 - 它们用你提供的具体类型 int 替换掉所有的 T
- 从而动态地创建(或缓存)了一个新的参数化类型
Box[int]。
- 类型检查器的依据:类型检查器依赖 Generic 基类来:
- 验证类型参数的个数:如果你声明了
Generic[K, V],却使用Pair[int](只提供了一个参数),检查器会报错。 - 追踪类型流:在
Box[int]的方法中,检查器知道所有出现 T 的地方现在都是 int。
- 验证类型参数的个数:如果你声明了
泛型总结
泛型(Generic) 是一种编程范式,它允许你编写不依赖于特定具体类型的代码(如函数、类),并在使用时再指定这些类型。其核心目标是实现代码的高度复用和类型安全。
简单来说:泛型就是一套规则,用于约定和描述具有广泛概念类型的编程规则,只要符合这个规则的类型都是泛型类型
泛型的关键组成部分
- 类型参数(Type Parameters):
- 这就是 TypeVar 发挥作用的地方。T 在 Box[T] 中就是一个类型参数。
- 它像一个形式上的占位符,在类或函数被定义时是抽象的。
- 具体类型参数(Concrete Type Arguments):
- 当使用泛型类或函数时,你为类型参数提供的具体类型。例如在 Box[str] 中,str 就是具体类型参数。
- 它替换了占位符,使得泛型代码特化为处理具体类型的代码
- 参数化(Parameterization):
- 整个过程被称为参数化。你通过提供类型参数来“参数化”一个泛型类型。
- Box 是泛型类型定义。
- Box[str] 是一个参数化的泛型类型。
泛型在哪些地方使用?
泛型容器(Collections):
这是最常见的使用场景。Python 标准库中的容器基本都是泛型的。list[str]:一个字符串列表dict[str, int]:一个键为字符串、值为整数的字典Optional[int]: 一个可以是 int 或 None 的值
泛型函数(Functions):
函数也可以被定义为泛型的,使其能够处理多种类型的输入并保持输出类型与输入关联。from typing import TypeVar, List T = TypeVar('T') def get_first_item(items: List[T]) -> T: return items[0]用户自定义的泛型类(Classes):
就像上面的 Box 例子一样,你可以创建自己的泛型类来构建灵活且类型安全的组件,如仓库(Repository)、处理器(Processor)、事件发射器(EventEmitter)等。
总结与概念对比
| 概念 | 描述 | 类比 |
|---|---|---|
| 泛型 (Generic) | 一个广泛的编程概念:指编写与类型无关的代码,稍后再指定类型 | 做饼干的模具。模具(泛型类)本身是空的,你可以用它做黄油饼干(Box[str])或巧克力饼干(Box[int])。 |
TypeVar | 实现泛型的工具:在 Python 中,它是用来创建类型参数的具体语法。 | 模具上的可替换标签。你可以在模具上插上“黄油”或“巧克力”的标签(T),来指定这次要做什么口味的饼干。 |
Generic 基类 | 另一个实现泛型的工具:在 Python 中,一个类需要继承 typing.Generic 才能成为一个正式的泛型类。 | 表明“我是一个模具”的认证标志。只有带有这个标志的类才能使用可替换标签(TypeVar)。 |
Protocol
协议(Protocol) 是一种定义接口或行为契约的类型。它只声明一个对象应该具有哪些方法(包括方法名、参数和返回类型),而不关心该对象的具体类是什么或它的继承关系。
这是一种支持 结构化类型(Structural Typing) 或 “鸭子类型”(Duck Typing) 的静态类型检查方式。
“鸭子类型” 的经典解释:当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。
注意
简单来说:Protocol 也是一种规则,用于约定和描述一个对象应该具有的属性和方法,只要具备了这个规则,那就是协议类型。就比如:只要你有钱,你就可以去饭店吃饭,不会因为你是大人还是小孩而拒绝你