[Python] Python 内置标准库 typing + type 函数 :Python 的类型系统

1 概述:Python type 函数

1.1 首先澄清一个概念

严格来说,type 在 Python 中是一个内置函数,而不是一个独立的模块。它有两个主要用途:

  1. 查看对象的类型(最常用的用法)
  2. 动态创建类(高级用法)

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 总结要点

  1. type(obj) → 返回对象的类型(最常用)
  2. type(name, bases, dict) → 动态创建类
  3. 所有类都是 type 的实例,type 是默认元类
  4. 自定义元类 → 控制类的创建过程(高级)

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 annotations
  • Annotated: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))

常见场景:

  1. FastAPI:用Annotated绑定参数描述、校验
from typing import Annotated
from fastapi import Query

def demo(q: Annotated[str | None, Query(max_length=10)] = None):
    ...
  1. Pydantic:字段约束、自定义校验标记
  2. 自己写序列化/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})

核心参数

  1. total=False:字段非必选,不要求全部key存在
class User(TypedDict, total=False):
    id: int
    name: str

默认total=True:所有字段都必须提供。

  1. 继承扩展
class Base(TypedDict):
    id: int

class User(Base):
    name: str

关键区分: TypedDict ≠ dataclass

  • TypedDict ≠ dataclass

    • TypedDict:描述dict结构,实例就是【普通字典】,无__init__;
    • dataclass:真正类,实例是【数据对象】。
  • 推荐文献

2.2.6 typing.io.BinaryIO(): 打开和读取二进制文件

  • 参考文献 & 推荐文献

https://github.com/python/cpython/tree/3.13/Lib/typing.py

定义

  • 使用场景: 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、pyright strict 模式会把“未注解定义/隐式 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,静态类型提示

常见坑

  1. TypedDict只是提示,写错key/值类型运行不会报错,仅静态检查工具生效;
  2. Annotated的metadata不会自动校验,需要代码手动读取.__metadata__处理;
  3. 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}")
  • 要点提示
  1. include_extras=True 传给 get_type_hints(),必须开启,否则会剥掉 Annotated,拿不到元数据;
  2. TypedDict 只是类型提示,运行时没有对象实例,靠 get_type_hints 反射拿字段注解;
  3. 本示例是玩具实现,生产环境一般直接用 Pydantic,不用手写这套;但这套机制就是 Pydantic/FastAPI 解析 Annotated 的底层原理。

CASE Annotated, get_args, get_origin : Annotated 元数据读取 + 简易校验示例

要点:

  1. 通过 get_args() 解析 Annotated 的真实类型和元数据;
  2. get_origin() 判断是否是 Annotated;
  3. 示例约定元数据格式:Annotated[int, {"min":0, "max":120, "desc":"年龄"}];
  4. 仅静态类型提示不会校验,校验逻辑完全自己实现。
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] 但传入 int
  • c3:运行时通过,但类型检查警告,float 不是 int
  • S = 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 参考文献

posted @ 2026-04-09 12:22  千千寰宇  阅读(105)  评论(0)    收藏  举报