[Python] Python 内置标准库 typing + type 函数 :Python 的类型系统
1 概述:Python type 函数
1.1 首先澄清一个概念
严格来说,type 在 Python 中是一个内置函数,而不是一个独立的模块。它有两个主要用途:
- 查看对象的类型(最常用的用法)
- 动态创建类(高级用法)
1.2 基础用法:查看类型
# 基本用法
print(type(123)) # <class 'int'>
print(type("hello")) # <class 'str'>
print(type([1, 2, 3])) # <class 'list'>
print(type({"a": 1})) # <class 'dict'>
为什么用 type() 而不是 isinstance()?
# type() 返回的是精确类型
print(type([]) == list) # True
print(type([]) == object) # False(虽然 list 继承自 object)
# isinstance() 会考虑继承关系
print(isinstance([], list)) # True
print(isinstance([], object)) # True
经验法则:判断具体类型用 type(),判断"是不是某种类型"用 isinstance()。
1.3 高级用法:动态创建类
这是 type() 的"隐藏功能",也是理解 Python 元类(metaclass)的关键。
1.3.1 类创建的底层原理
# 你平时写的类
class Person:
name = "unknown"
def say_hello(self):
return f"Hello, I'm {self.name}"
# 等价于用 type() 动态创建
Person = type('Person', # 类名
(object,), # 父类元组
{ # 属性字典
'name': 'unknown',
'say_hello': lambda self: f"Hello, I'm {self.name}"
})
1.3.2 type() 创建类的语法
type(name, bases, namespace)
# name: 类名字符串
# bases: 父类元组(可以为空)
# namespace: 包含属性和方法的字典
1.3.3 实际应用示例
def create_class(class_name, fields):
"""
动态创建数据类(类似简单的 dataclass)
"""
def __init__(self, **kwargs):
for field in fields:
setattr(self, field, kwargs.get(field))
def __repr__(self):
attrs = ", ".join(f"{f}={getattr(self, f)!r}" for f in fields)
return f"{class_name}({attrs})"
# 用 type 动态创建类
return type(class_name, (), {
'__init__': __init__,
'__repr__': __repr__,
'__slots__': fields # 限制属性,节省内存
})
# 使用
Student = create_class('Student', ['name', 'age', 'major'])
s = Student(name="张三", age=20, major="CS")
print(s) # Student(name='张三', age=20, major='CS')
1.4 type 与元类(Metaclass)
1.4.1 一切都是对象,类也是
# 在 Python 中,类也是对象!
class Foo:
pass
print(type(Foo)) # <class 'type'>
# 这意味着:
# 1. Foo 是 type 的实例
# 2. type 是 Foo 的"类"
1.4.2 元类:类的类
# type 是所有类的默认元类
print(type(int)) # <class 'type'>
# 补充: print(type(123)) # <class 'int'>
print(type(str)) # <class 'type'>
# 补充: print(type("123")) # <class 'str'>
print(type(list)) # <class 'type'>
# 补充: print(type([1,2,3])) # <class 'list'>
# 补充: print(type({"a": 1})) # <class 'dict'>
print(type(type)) # <class 'type'> (type 也是自己的实例!)
1.4.3 自定义元类
class SingletonMeta(type):
"""单例元类:确保类只有一个实例"""
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class Database(metaclass=SingletonMeta):
def __init__(self, url):
self.url = url
# 测试
db1 = Database("mysql://localhost")
db2 = Database("postgres://remote")
print(db1 is db2) # True(同一个对象)
print(db1.url) # mysql://localhost(第一个创建的)
1.5 实际应用场景
| 场景 | 用法 |
|---|---|
| 调试/日志 | print(type(x)) 快速查看变量类型 |
| 类型检查 | if type(x) is not int: raise TypeError |
| 序列化/反序列化 | 根据类型名动态创建类 |
| ORM 框架 | 动态生成模型类 |
| 插件系统 | 运行时动态注册和创建类 |
| 单例/工厂模式 | 用元类控制类的创建行为 |
1.6 总结要点
type(obj)→ 返回对象的类型(最常用)type(name, bases, dict)→ 动态创建类- 所有类都是
type的实例,type是默认元类 - 自定义元类 → 控制类的创建过程(高级)
1.7 思考题(供练习)
# 这段代码输出什么?为什么?
class A: pass
class B(A): pass
print(type(A))
print(type(B))
print(type(A) == type(B))
print(A.__class__)
print(B.__class__.__class__)
点击查看答案
<class 'type'>
<class 'type'>
True
<class 'type'>
<class 'type'>
所有类的 __class__ 都是 type(默认元类),所以 type(A) == type(B) 为 True。
type 是理解 Python 动态特性的关键,掌握它对后续学习元编程、框架设计都很有价值。
2 概述:Python typing 模块 = Python3 的类型提示系统 [Python 3.5 - ]
Python 的
typing模块,这是 Python 类型提示系统的核心,对于写清晰、可维护的代码非常重要。
模块简介
- Python 的
typing模块自Python 3.5版本引入,为静态类型注解提供了支持。
这个模块主要用于增强代码的可读性和维护性,尽管 Python 是一种动态类型语言,类型注解使得开发者能够更清晰地了解函数和变量的预期类型。
类型别名和新类型
- 类型别名,是使用 type 语句定义的,它创建一个 TypeAliasType 的实例。例如,Vector 和 list[float] 将被静态类型检查器等同处理。类型别名适用于简化复杂的类型签名。NewType 助手则用于创建与原类型不同的新类型,这对于捕捉逻辑错误很有用。
可调用对象的标注
- 函数或其他可调用对象可以使用
collections.abc.Callable或typing.Callable来标注。
例如,Callable[[int], str] 表示一个接受 int 类型的单个参数并返回 str 的函数。
- 推荐文献
Callable[参数类型列表, 返回值类型]组件 = 表示“任何可调用的对象”(函数、lambda、类、实现了__call__的实例等)
泛型(Generic)
- 泛型,允许通过使用类型形参语法来实现参数化,从而为容器元素添加预期的类型。例如,Sequence[Employee] 表示序列中的所有元素都必须是 Employee 实例。
标注元组
- 元组在 Python 的类型系统中是特殊情况,可以接受任意数量的类型参数。例如,
tuple[int, str]表示一个长度为 2 的元组,第一个元素是 int,第二个是 str。
类对象的类型
- 用 C 注解的变量可以接受类型 C 的值。而用类型 type[C] 注解的变量则可以接受 C 的类对象。
用户定义的泛型类型
用户定义的类可以定义为泛型类。例如,LoggedVar[T] 表示类 LoggedVar 是围绕单个类型变量 T 实现参数化的。
Any 类型
- Any 是一种特殊的类型,表示没有约束的类型。所有类型都与 Any 兼容,反之亦然。
名义子类型 vs 结构子类型
- Python 静态类型系统最初被定义为使用名义子类型,这意味着类 A 必须是 B 的子类才能使用。PEP 544 允许结构子类型,也称为静态鸭子类型。
模块内容
typing 模块定义了一系列的类、函数和装饰器,如 Any、Union、Tuple、Callable、TypeVar 和 Generic 等。
特殊类型原语
- typing 模块中的特殊类型原语,如 Any、Union、Optional、Literal、Final 和 TypeAlias,用于在注解中表示类型。
特殊形式
- typing 模块中的特殊形式,如 Union、Optional、Concatenate 和 Literal,支持下标用法,并具有唯一的语法。
构造泛型类型与类型别名
typing.Generic用于泛型类型的抽象基类,而 typing.TypeVar 和 typing.ParamSpec 用于构造类型变量和形参专属变量。
其他特殊指令
- typing 模块中的其他特殊指令,如 NamedTuple、NewType、Protocol、runtime_checkable 和 TypedDict,用于创建和声明类型。
类型检查工具
- 可以使用 mypy 等静态类型检查工具进行类型检查,以确保代码符合类型注解。
注意事项
- 静态类型检查工具辅助,不会影响 Python 的动态特性,可以选择性地使用类型注解。类型注解应该让代码更易于理解,但不应使代码变得过于复杂。
2.1 为什么需要 typing?
背景:Python 是动态类型语言
# 动态类型的"自由"也带来了问题
def add(a, b):
return a + b
add(1, 2) # 3 ✓
add("1", "2") # "12" ✓(可能不是想要的)
add([1], [2]) # [1, 2] ✓(也可能不是想要的)
问题:函数签名没有表达设计意图,调用者容易用错。
解决方案:类型提示(Type Hints)
from typing import List, Dict, Optional
def add(a: int, b: int) -> int:
return a + b
# 现在 IDE 会提示错误,代码也更易读
add("1", "2") # IDE 警告:期望 int,实际为 str
注意:Python 解释器不强制类型检查,类型提示是给开发者、IDE 和类型检查工具(如 mypy)看的。
2.2 typing 模块的核心内容
2.2.1 基础类型别名: List, Dict, Tuple, Set
from typing import List, Dict, Tuple, Set, Optional, Union, Callable
# 容器类型(Python 3.9+ 可直接用 list, dict 等,不需要导入)
numbers: List[int] = [1, 2, 3]
scores: Dict[str, int] = {"Alice": 90, "Bob": 85}
point: Tuple[int, int] = (10, 20) # 固定长度元组
record: Tuple[int, str, float] = (1, "a", 3.14) # 异构元组
tags: Set[str] = {"python", "typing"}
2.2.2 Optional:可能为 None
from typing import Optional
# 老写法(繁琐)
def find_user(user_id: int) -> Union[User, None]:
...
# 推荐写法:Optional[X] 等价于 Union[X, None]
def find_user(user_id: int) -> Optional[User]:
"""返回 User 或 None(找不到时)"""
...
2.2.3 Union:多种可能类型
简介
- 作用
在 Python 的类型提示(Type Hints)中,
Union是一个非常重要的工具,用于指定一个变量或函数参数可以接受多种类型中的任意一种。
其来自typing模块。
Union 的主要作用是允许一个变量或参数具有多种可能的类型,而不是单一的类型。这在实际开发中非常有用,因为某些函数可能需要处理多种类型的输入,而 Union 可以明确地表达这种需求。
使用方法 & 简单示例
- 使用方法
Union的语法是Union[Type1, Type2, ...],其中Type1、Type2等是可能的类型。
从 Python 3.10 开始,Union 也可以通过|操作符来表示,这种方式更简洁。
- 简单示例1
from typing import Union
# 使用 Union 指定变量可以是 int 或 str
def add_or_concatenate(a: Union[int, str], b: Union[int, str]) -> Union[int, str]:
if isinstance(a, int) and isinstance(b, int):
return a + b
elif isinstance(a, str) and isinstance(b, str):
return a + b
else:
raise TypeError("Both arguments must be of the same type: int or str")
# 使用 | 操作符(Python 3.10+)
def add_or_concatenate(a: int | str, b: int | str) -> int | str:
if isinstance(a, int) and isinstance(b, int):
return a + b
elif isinstance(a, str) and isinstance(b, str):
return a + b
else:
raise TypeError("Both arguments must be of the same type: int or str")
- 简单示例2
from typing import Union
# 参数可以是 int 或 float
def process(value: Union[int, float]) -> float:
return float(value)
# Python 3.10+ 新语法(更简洁)
def process(value: int | float) -> float: # 用 | 代替 Union
return float(value)
特点、注意事项
- 优点
- 类型安全:通过明确指定变量或参数可以接受的类型,可以减少运行时错误。
- 代码可读性:让代码的意图更加清晰,其他开发者更容易理解函数的输入和输出。
- 静态类型检查:工具(如 mypy)可以利用这些类型提示进行静态类型检查,提前发现潜在的类型错误。
- 注意事项
- 尽量避免过度使用:虽然 Union 提供了灵活性,但过多使用可能会使代码变得复杂。如果可能,尽量将函数或变量的类型限制为单一类型。
- 兼容性:在 Python 3.10 之前,必须使用 Union,而在 Python 3.10 及以上版本中,可以使用
|操作符,但需要确保代码的兼容性。
使用场景: 当函数需要接受多种类型的入参/出参或变量时,Union 可明确地表达这一点
- 使用场景
场景1 - 函数参数:当函数需要接受多种类型的参数时,使用 Union 可以明确地表达这一点。
def process_data(data: Union[int, float, str]) -> None:
if isinstance(data, int):
print(f"Processing integer: {data}")
elif isinstance(data, float):
print(f"Processing float: {data}")
elif isinstance(data, str):
print(f"Processing string: {data}")
场景2 - 返回值:当函数的返回值可能有多种类型时,也可以使用 Union。
def get_value(condition: bool) -> Union[int, str]:
if condition:
return 42
else:
return "Hello"
场景3 - 变量类型:在定义变量时,也可以使用 Union 来指定变量可以是多种类型。
from typing import Union
value: Union[int, str] = 10
value = "Hello" # 也可以是字符串
2.2.4 Annotated[T, *metadata]: 给类型T【附加】任意【元数据】
缘起
- 缘起:在基于 LangGraph 开发AI应用时,常有这种使用方法:
from typing import Annotated
from typing import TypedDict
# 或 from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages # 预构建的 reducer 函数,用于将新消息【附加】到列表中,而不是【覆盖】它。
class MyState(TypedDict):
# Messages have the type "list". The `add_messages` function
# in the annotation defines how this state key should be updated
# (in this case, it appends messages to the list, rather than overwriting them)
messages: Annotated[list, add_messages]
graph_builder = StateGraph(MyState)
typing.Annotated/typing|typing_extensions.TypedDict均属于类型提示(typing)工具,不改变运行时行为,只给类型检查器、IDE、文档工具提供信息。- Python版本:
TypedDict:3.8+ 内置,3.7可from future import annotationsAnnotated:3.9+ 加入typing,3.8需pip install typing-extensions
定义
Annotated[T, *metadata]:给类型T【附加】任意【元数据】,元数据可以是注释、校验规则、字段信息、UI提示等,运行时保存在__metadata__属性。
核心用法
from typing import Annotated
# 基础:int类型,附带描述、校验元数据
Age = Annotated[int, "年龄", (0, 120)]
def set_age(age: Age) -> None:
pass
# 读取元数据
print(Age.__metadata__) # ('年龄', (0, 120))
常见场景:
- FastAPI:用
Annotated绑定参数描述、校验
from typing import Annotated
from fastapi import Query
def demo(q: Annotated[str | None, Query(max_length=10)] = None):
...
- Pydantic:字段约束、自定义校验标记
- 自己写序列化/ORM:携带字段标签、长度、备注
类型检查器只认第一个参数
T,后面metadata不参与类型校验,需要自己代码读取__metadata__做业务校验。
2.2.5 TypedDict: 给字典穿上类型的外衣 : 定义Dict字典的键名+对应值类型,给普通 Dict 做类型提示
- 推荐文献
定义
TypedDict:定义字典的键名+对应值类型,用于给普通Python dict做类型提示。
注意:它不是真正类,运行时还是普通dict,不会做运行时校验;mypy/pyright做静态检查。
两种写法
方式1:类继承写法(推荐,可读性好)
from typing import TypedDict, NotRequired
#from typing_extensions import TypedDict
class User(TypedDict, total=False): # total=False(所有字段是否必填: False/可选)
name: str = 'unknwn' # 默认值
age: int
email: NotRequired[str] # 可选字段
is_active: bool = True # 默认值
# 使用 - 创建实例
user = User(name="张三", age=30) # 运行时,`User(...)` 创建的就是一个【普通 dict】(`{'name': '张三', 'age': 30}`),不是带属性的对象
print("user:", user) # (不继承 TypedDict Class 时) user: {'name': '张三', 'age': 30} | 反之(不继承 TypedDict Class 时): User() takes no arguments
# 使用 - 获取属性值
print(user['name']) # 或: print(user.get('name')) # 张三 | dict 只支持下标访问 `user['name']`,不支持属性访问 `user.name`(属性访问是 `object` 子类的特性)
print(user.name) # 报错:AttributeError: 'dict' object has no attribute 'name' | 同理,类体里的 `name: str = 'unknown'` 这种默认值【不会生效*】—— 它只是注解,不是实例属性;`is_active` 也不会被默认设为 `True`
# 运行时类型检查 | Python 3.12+
from typing import is_typeddict
print(is_typeddict(User)) # True
print(is_typeddict({"name": "Alice"})) # False
方式2:函数式构造
User = TypedDict("User", {"id": int, "name": str, "email": str | None})
核心参数
total=False:字段非必选,不要求全部key存在
class User(TypedDict, total=False):
id: int
name: str
默认
total=True:所有字段都必须提供。
- 继承扩展
class Base(TypedDict):
id: int
class User(Base):
name: str
关键区分: TypedDict ≠ dataclass
-
TypedDict ≠ dataclass
- TypedDict:描述dict结构,实例就是【普通字典】,无
__init__; - dataclass:真正类,实例是【数据对象】。
- TypedDict:描述dict结构,实例就是【普通字典】,无
-
推荐文献
2.2.6 typing.io.BinaryIO(): 打开和读取二进制文件
- 参考文献 & 推荐文献
- Python中的typing.io.BinaryIO()函数:如何打开和读取二进制文件 - 6qe.net
- Python中的typing.io.BinaryIO():什么是BinaryIO()函数 - 6qe.net
定义
- 使用场景:
typing.io.BinaryIO(): 打开和读取二进制文件
- 在
Python中,typing.io.BinaryIO()是一个类型提示函数,它表示二进制输入/输出流。
它可以用于函数或方法参数类型注释,以指示预期的输入或输出类型。
这对于静态类型检查和代码文档生成非常有用。
- 要打开和读取二进制文件,我们可以使用内置的
open()函数,并指定打开模式为二进制模式('rb')。
from typing import BinaryIO
def read_binary_file(fileBinBytes: BinaryIO, byteSize: int) -> bytes:
data = fileBinBytes.read(byteSize)
return data
# 打开二进制文件
with open('example.blf', 'rb') as fileBinBytes:
# 读取文件内容
binary_data = read_binary_file(fileBinBytes, 10)
# 处理二进制数据
print(binary_data)
byte_count = len(binary_data)
print(f"字节数为: {byte_count} bytes")
out
b'LOGG\x90\x00\x00\x00\x947'
分析:
b'LOGG':4 个字节(每个字符占用 1 个字节)
\x90:1 个字节(十六进制表示的字节)
\x00\x00\x00:3 个字节(3 个零字节)
\x94:1 个字节
b'7':1 个字节
CASE: 读写数据到文件
from typing import BinaryIO
def read_file(file: BinaryIO) -> None:
# 读取文件内容
data = file.read()
# 打印文件内容
print(data)
def write_file(file: BinaryIO, data: bytes) -> None:
# 写入数据到文件
file.write(data)
# 打开二进制文件进行读取
with open('example.bin', 'rb') as file:
read_file(file)
# 打开二进制文件进行写入
with open('example.bin', 'wb') as file:
write_file(file, b'Hello, World!')
2.2.7 Protocol : 表示“结构化接口”、鸭子接口:不要求类显式继承,只要有匹配的方法/属性,就算兼容
定义
Protocol表示“结构化接口”:不要求类显式继承,只要有匹配的方法/属性,就算兼容。
它主要由
mypy/pyright做静态类型检查,运行时默认不校验。
- Protocol 被戏谑为“鸭子接口”的原因
typing.Protocol是“结构性类型”(structural typing):只要一个类长出了指定属性/方法(签名匹配),就自动满足协议,不需要显式继承、注册或声明实现。
这正是鸭子类型的编译期/工具期表达——“走路像鸭子、叫得像鸭子,它就是鸭子”。
例如:
class Quacks(Protocol):
def quack(self) -> str: ...
class Duck:
def quack(self) -> str: # 未继承 Quacks
return "quack"
def greet(x: Quacks): ...
greet(Duck()) # 类型检查通过
传统接口(Java interface、Python ABC 继承)是“名义类型”:类必须声明“我实现了谁”。
而Protocol取消了这一声明,只看结构是否吻合,所以说它是“鸭子接口”。
代码示例
from typing import Protocol
class LLMProvider(Protocol):
def generate(self, messages: list[ChatMessage]) -> str:
...
LLMProvider定义了一个 LLM 提供者的接口。- 任何有
generate方法、入参是list[ChatMessage]、返回str的对象,都可以当作LLMProvider使用。- 真实 provider 和测试替身都不需要继承
LLMProvider,只要“长得像”即可。
- 例如:
class FakeLLM:
def generate(self, messages: list[ChatMessage]) -> str:
return "mock response"
def run(provider: LLMProvider, messages: list[ChatMessage]) -> str:
return provider.generate(messages)
run(FakeLLM(), [])
这里
FakeLLM并没有继承LLMProvider,但因为实现了匹配的generate方法,所以类型检查器会认为它是LLMProvider。
特别补充
- 在
Protocol方法里,...是常见写法,表示“这是接口声明,不提供实现”。 - 运行时
isinstance默认不可用于Protocol,除非加@runtime_checkable,且它也只检查方法名是否存在,不检查签名。
2.2.8 Any: 表示任意类型,放弃对其做静态类型检查,容许问题推迟到运行时暴露
缘起
- ai_chat_bot/store.py
...
from typing import Any
...
from chromadb import PersistentClient
...
...
class ChatStore:
"""对话存储: 负责会话、消息与知识片段的 Chroma 持久化存储。"""
...
def _message_records(self, result: dict[str, Any], session_id: str) -> list[MessageRecord]: # Any 入参
"""把 Chroma 查询行转换为顺序稳定的消息记录。"""
records = [
MessageRecord(
message_id=message_id,
session_id=metadata["session_id"],
role=metadata["role"],
content=content,
sequence=int(metadata["sequence"]),
created_at=str(metadata.get("created_at", "")),
)
for message_id, content, metadata in zip(
result["ids"], result["documents"], result["metadatas"], strict=True
)
if metadata["session_id"] == session_id
]
return sorted(records, key=lambda record: record.sequence)
...
...
定义
typing.Any表示“任意类型”,用来表达“这个值可以是任何东西,并且我放弃对它做静态类型检查”。
它在 PEP 484 里引入,Python 3.5 起存在于
typing模块,本质是动态类型的“逃生舱”。
- 基本语义
from typing import Any
def dump(x: Any) -> None:
print(x + 1) # mypy/pyright 不报错
接受
Any后,检查器对它后续的所有操作都放行:
def f(x: Any) -> None:
x.whatever() # 放行
y: int = x # 放行
z: str = x # 放行
代价
- 代价:
- 类型错误被静默吞掉,问题推迟到运行时才暴露;
- 而且
Any会沿数据流“传染”——只要某个表达式沾上Any,结果通常也是Any,检查继续被削弱,直到离开Any区域或显式收窄。
Any vs object
这是最容易混淆、也最有学习价值的对比:
- Any
def f(x: Any):
return x.upper() # 放行:不检查
def g(x: object):
return x.upper() # 报错:object 上没有 upper
object同样表示“任何对象”,但对它的操作仍受检查,必须先收窄:
def g(x: object) -> str:
if isinstance(x, str):
return x.upper()
return ""
所以,经验法则:想“任何值,但用之前必须证明类型” →
object;想“直接放弃检查” →Any。优先object。
隐式 Any (strict模式下编译时会报错)
- 没有注解的参数/变量默认就是
Any,只是没有写出来:
def f(x): # 隐式 Any
return x + 1
mypy --strict、pyrightstrict 模式会把“未注解定义/隐式 Any”当错误,这正是防止Any无声扩散的护栏。
合理使用场景
- 老的第三方库没有类型存根,在其边界临时用
Any+cast收窄后进入内部逻辑。 - JSON 等序列化/反序列化的入口边界:先
Any接住,校验后立刻转成【具体模型】。 - 大型无类型代码库渐进迁移,先不强推。
更好的替代品
- 有限集合 →
int | str(3.10+)或Union[int, str] - 鸭子接口 →
Protocol - 需要保留调用处泛型关系 →
TypeVar - 装饰器保留被装饰函数签名 →
ParamSpec - “任何值但必须收窄使用” →
object
总结:把
Any当成边界胶水,用完立刻收窄;不要让它流进核心业务逻辑。静态检查器视角下,每多一个Any,类型系统就多一处盲区。
2.3 复杂类型场景
2.3.1 Callable:函数类型
from typing import Callable
# 定义回调函数类型
# Callable[[参数类型...], 返回类型]
def execute_callback(
callback: Callable[[int, int], int],
x: int,
y: int
) -> int:
return callback(x, y)
# 使用
def add(a: int, b: int) -> int:
return a + b
result = execute_callback(add, 1, 2) # 3
2.3.2 Generic/泛型:类型参数化
from typing import TypeVar, Generic, List
T = TypeVar('T') # 定义类型变量
class Stack(Generic[T]):
"""泛型栈:可以存任意类型,但实例化后固定"""
def __init__(self) -> None:
self._items: List[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
# 使用
int_stack: Stack[int] = Stack()
int_stack.push(1) # ✓
int_stack.push("a") # IDE 警告:期望 int
str_stack: Stack[str] = Stack()
str_stack.push("hello") # ✓
2.3.3 TypeVar: 有界类型变量
from typing import TypeVar
# T 必须是 Number 或其子类
class Number:
def value(self) -> float: ...
T = TypeVar('T', bound=Number)
def sum_numbers(items: List[T]) -> float:
return sum(item.value() for item in items)
2.4 高级特性
2.4.1 TypeAlias:类型别名(Python 3.10+)
from typing import TypeAlias
# 简化复杂类型
Vector: TypeAlias = list[float]
Matrix: TypeAlias = list[Vector]
def dot_product(v1: Vector, v2: Vector) -> float:
return sum(x * y for x, y in zip(v1, v2))
# 更清晰的函数签名
def matrix_multiply(a: Matrix, b: Matrix) -> Matrix:
...
2.4.2 Protocol:结构子类型(鸭子类型)
from typing import Protocol
class Drawable(Protocol):
"""只要实现了 draw() 方法,就符合这个协议"""
def draw(self) -> None: ...
def render(items: list[Drawable]) -> None:
for item in items:
item.draw()
# 不需要【显式继承】!
class Circle:
def draw(self) -> None:
print("Drawing circle")
class Square:
def draw(self) -> None:
print("Drawing square")
# Circle 和 Square 都"符合" Drawable 协议
render([Circle(), Square()]) # ✓
2.4.3 @overload:函数重载
from typing import overload, List
class Processor:
@overload
def process(self, data: int) -> str: ...
@overload
def process(self, data: List[int]) -> List[str]: ...
# 实际实现
def process(self, data):
if isinstance(data, int):
return str(data)
return [str(x) for x in data]
p = Processor()
p.process(42) # IDE 知道返回 str
p.process([1, 2, 3]) # IDE 知道返回 List[str]
2.5 实际项目应用
CASE 数据模型定义(typing 的 Optional, List 等配合 dataclasses)
from dataclasses import dataclass
from typing import Optional, List
from datetime import datetime
@dataclass
class User:
id: int
name: str
email: Optional[str] = None
created_at: datetime = field(default_factory=datetime.now)
@dataclass
class Post:
id: int
title: str
content: str
author: User # 引用其他类型
tags: List[str] = field(default_factory=list)
CASE 基于 TypedDict 的 API 响应类型
from typing import TypedDict
class ApiResponse(TypedDict):
code: int
message: str
data: dict
class UserInfo(TypedDict):
id: int
name: str
is_active: bool
# 函数明确返回结构
def get_user(user_id: int) -> ApiResponse:
return {
"code": 200,
"message": "success",
"data": {"id": user_id, "name": "Alice", "is_active": True}
}
CASE Annotated + TypedDict 组合使用 (必读)
- 给
TypedDict的字段属性附加元数据(pydantic常用)
from typing import TypedDict, Annotated
class User(TypedDict):
id: Annotated[int, "用户ID", 1000]
name: Annotated[str, "用户名"]
对比小结
| 工具 | 用途 | 运行时实体 |
|---|---|---|
Annotated[T, ...] |
给类型附加自定义元数据 | 原类型T,元数据存__metadata__ |
TypedDict |
规定dict的key与value类型 | 普通dict,静态类型提示 |
常见坑
TypedDict只是提示,写错key/值类型运行不会报错,仅静态检查工具生效;Annotated的metadata不会自动校验,需要代码手动读取.__metadata__处理;Python3.10+可以直接用|做联合类型;低版本需要Union。
CASE Annotated 和 TypedDict 结合:遍历 TypedDict 字段做校验
- TypedDict 在运行时就是普通字典,需要通过
typing.get_type_hints获取字段注解。
from typing import TypedDict, get_type_hints
class User(TypedDict):
user_id: Annotated[int, {"min":1}]
age: Annotated[int, {"min":0, "max":120}]
name: Annotated[str, {"min_len": 2}]
def check_typed_dict(data: dict, td_cls):
hints = get_type_hints(td_cls, include_extras=True)
for field_name, field_annotated_tp in hints.items():
val = data[field_name]
inner_t, meta_tuple = get_annotated_meta(field_annotated_tp)
# 基础类型校验
if not isinstance(val, inner_t):
raise TypeError(f"字段[{field_name}]类型错误,期望 {inner_t}")
for meta in meta_tuple:
if isinstance(meta, dict):
if "min" in meta and val < meta["min"]:
raise ValueError(f"字段[{field_name}]不能小于 {meta['min']}")
if "max" in meta and val > meta["max"]:
raise ValueError(f"字段[{field_name}]不能大于 {meta['max']}")
if "min_len" in meta and len(val) < meta["min_len"]:
raise ValueError(f"字段[{field_name}]长度至少 {meta['min_len']}")
return True
# 测试
good_user = {"user_id": 10, "age": 22, "name": "jack"}
check_typed_dict(good_user, User)
print("user 校验OK")
bad_user = {"user_id": 0, "age": 200, "name": "a"}
try:
check_typed_dict(bad_user, User)
except (ValueError, TypeError) as err:
print(f"校验错误:{err}")
- 要点提示
include_extras=True传给get_type_hints(),必须开启,否则会剥掉Annotated,拿不到元数据;- TypedDict 只是类型提示,运行时没有对象实例,靠
get_type_hints反射拿字段注解;- 本示例是玩具实现,生产环境一般直接用 Pydantic,不用手写这套;但这套机制就是 Pydantic/FastAPI 解析 Annotated 的底层原理。
CASE Annotated, get_args, get_origin : Annotated 元数据读取 + 简易校验示例
要点:
- 通过
get_args()解析Annotated的真实类型和元数据; get_origin()判断是否是 Annotated;- 示例约定元数据格式:
Annotated[int, {"min":0, "max":120, "desc":"年龄"}]; - 仅静态类型提示不会校验,校验逻辑完全自己实现。
from typing import Annotated, get_args, get_origin
def get_annotated_meta(tp):
"""
解析Annotated类型:返回 (原始类型, 元数据tuple)
非Annotated类型返回 (tp, ())
"""
if get_origin(tp) is Annotated:
args = get_args(tp)
inner_type = args[0]
meta = args[1:]
return inner_type, meta
return tp, ()
def validate_by_annotated(value, annotated_type):
"""
根据Annotated携带的元数据做简单校验
约定:元数据里放dict,包含 min/max
"""
inner_t, meta_tuple = get_annotated_meta(annotated_type)
# 先校验基础类型
if not isinstance(value, inner_t):
raise TypeError(f"类型错误,期望 {inner_t.__name__},实际 {type(value).__name__}")
# 遍历元数据,取出校验规则dict
for meta in meta_tuple:
if isinstance(meta, dict):
min_val = meta.get("min")
max_val = meta.get("max")
if min_val is not None and value < min_val:
raise ValueError(f"值 {value} 不能小于 {min_val}")
if max_val is not None and value > max_val:
raise ValueError(f"值 {value} 不能大于 {max_val}")
return True
# ---------------- 使用演示 ----------------
AgeType = Annotated[int, {"min": 0, "max": 120, "desc": "用户年龄"}]
# 读取元数据
inner, meta = get_annotated_meta(AgeType)
print(f"底层类型: {inner}")
print(f"附加元数据: {meta}")
# 合法
validate_by_annotated(25, AgeType)
print("25 校验通过")
# 非法:超出范围
try:
validate_by_annotated(150, AgeType)
except ValueError as e:
print(f"校验失败: {e}")
# 非法:类型不对
try:
validate_by_annotated("20", AgeType)
except TypeError as e:
print(f"校验失败: {e}")
output:
底层类型: <class 'int'>
附加元数据: ({'min': 0, 'max': 120, 'desc': '用户年龄'},)
25 校验通过
校验失败: 值 150 不能大于 120
校验失败: 类型错误,期望 int,实际 str
2.6 版本演进与最佳实践
| Python 版本 | 重要变化 |
|---|---|
| 3.5 | typing 模块引入 |
| 3.7 | from __future__ import annotations 延迟求值 |
| 3.9 | 内置类型支持泛型:list[int] 替代 List[int] |
| 3.10 | X | Y 替代 Union[X, Y],TypeAlias |
| 3.11 | Self 类型(类方法返回自身类型) |
现代推荐写法(Python 3.9+)
# 不再需要 from typing import List, Dict, Optional, Union
def process(
items: list[int | str], # 替代 List[Union[int, str]]
config: dict[str, float] | None # 替代 Optional[Dict[str, float]]
) -> tuple[int, str]:
...
2.7 思考题
from typing import TypeVar, Generic
T = TypeVar('T')
S = TypeVar('S', bound=int)
class Container(Generic[T]):
def __init__(self, value: T) -> None:
self.value = value
def get(self) -> T:
return self.value
# 下面哪些用法是正确的?IDE/mypy 会怎么提示?
c1 = Container(42) # 类型推断为 Container[int]
c2: Container[str] = Container(42) # ?
c3 = Container[int](3.14) # ?
# 如果改成 S 呢?
class IntContainer(Generic[S]): ...
答案
c2:错误,声明Container[str]但传入intc3:运行时通过,但类型检查警告,float不是intS = TypeVar('S', bound=int)限制S必须是int或其子类,float不符合
- 掌握
typing能让你的 Python 代码在团队协作和大型项目中更加可靠。建议配合 VS Code + Pylance 或 PyCharm 使用,获得即时的类型检查和智能提示!
S 学习路径建议
1. 基础:给函数加类型提示
↓
2. 进阶:容器类型、Optional、Union
↓
3. 实战:配合 IDE/mypy 检查项目
↓
4. 高级:泛型、Protocol、TypedDict
↓
5. 深入:自定义泛型类、协变逆变(Covariant/Contravariant)
X 参考文献
本文链接: https://www.cnblogs.com/johnnyzen
关于博文:评论和私信会在第一时间回复,或直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
日常交流:大数据与软件开发-QQ交流群: 774386015 【入群二维码】参见左下角。您的支持、鼓励是博主技术写作的重要动力!

浙公网安备 33010602011771号