今天早上升级项目到 Python 3.12,突然发现代码里写类型别名的方式可以更清爽了。以前我们定义一个类型别名得用赋值语句,比如 UserId = int,看起来是个人都会写,但一旦涉及到泛型、联合类型,就总觉得少了点“声明”的感觉。
Python 3.12 终于引入了 type 语句,专门用来定义类型别名。它不仅仅是语法糖,还让类型定义在代码里更像一种“声明”,而不是一个普通的变量赋值。今天就用三个小案例,带你把它的用法摸清。
旧写法有什么问题
拿最常见的回调函数来举例。以前你想表示一个回调类型,可能会写成:
from typing import Callable
# 旧写法:用赋值语句定义别名
Handler = Callable[[int, str], bool]
def setup(h: Handler):
...
# 也还行,但一多就乱
如果是一个泛型别名,那就更考验人了:
T = TypeVar("T")
OldPaginated = tuple[list[T], int] # 看得懂,但缺少“宣言感”
这种写法本身没问题,只是可读性一般。而且当你希望类型别名只存在于类型检查阶段,不产生运行时对象的时候,赋值语句依然会生成一个真实的变量,在运行时占用名称空间。虽然实际影响有限,但总归不够干净。
type 语句就是为了解决这些问题来的。
第一个案例:普通类型别名
在 Python 3.12 里,你可以直接这样写:
type UserID = int
type UserName = str
def get_user(user_id: UserID) -> dict[str, UserName]:
return {"id": user_id}
print(get_user(3))
在类型检查器眼里,UserID 就是 int 的别名。区别在于,type 定义的这个别名在运行时不会产生一个叫做 UserID 的变量,不信你在交互式环境试试:
>>> type UserID = int
>>> UserID
NameError: name 'UserID' is not defined
这就是一种“纯粹”的类型别名。它只服务于类型检查,不污染运行时命名空间。不过注意,如果是在 .py 文件里运行,type 语句是不是真的一点运行时效果都没有?目前来看,它不会绑定任何值,但会在模块的 __annotations__ 里留下一些记号,不过这不是我们要关心的。
第二个案例:带泛型的类型别名
再说个实在的。你的代码里经常会有“分页结果”这种结构,以前写:
T = TypeVar("T")
PageResult = tuple[list[T], int]
现在可以这样定义:
type PageResult[T] = tuple[list[T], int]
这里面 T 是类型参数,表示任意类型。你可以在后面直接使用 PageResult[str] 作为具体类型:
def fetch_pages() -> PageResult[str]:
return (["a", "b"], 2)
data, total = fetch_pages()
print(data, total)
泛型参数写在方括号里,直观清晰。相比原来 TypeVar + 赋值,现在这个语法更像在定义一个新的类型函数。也支持多个参数:
type Pair[A, B] = tuple[A, B]
还可以给类型参数加上约束或默认值,虽然日常用得不多,但如果你要给一个复杂库写类型,这个功能就能派上大用场。
比如我想表示一个键值对,键必须是字符串,值可以是任意类型,还可以给值一个默认的 str:
type StrKey[A = str] = dict[str, A]
x: StrKey[int] = {"age": 20} # 没问题
y: StrKey = {"name": "小林"} # 默认 A 是 str
不过要记住,默认类型参数语法是 3.13 才正式支持的,3.12 只能用基本泛型定义。
第三个案例:和 Self 类型配合,写出流畅的链式调用
Self 类型在 3.11 就引入了,但和 type 一起用,可以做出很多优雅的设计。先看一个经典的链式调用场景:
class Query:
def filter(self, condition: str) -> Self:
# 这里应该有实际逻辑
return self
def order_by(self, field: str) -> Self:
return self
def execute(self):
return ["结果1", "结果2"]
这样写没有任何问题。但如果你想在子类中让链式方法返回子类型,Self 本来就能做到。现在结合 type,我们可以把“构造器”的类型别名定义得更明确。
比如有个队列类,你希望提供一个方法能根据当前用户角色生成一个受限的查询版本。可以这样设计:
from typing import Self, Protocol, Any
class SupportsFilter(Protocol):
def filter(self, condition: str) -> Self: ...
type QueryFactory = Callable[[Any], Query]
class Query:
def __init__(self, table: str):
self.table = table
def filter(self, condition: str) -> Self:
print(f"过滤: {condition}")
return self
def where(self, condition: str) -> Self:
return self.filter(condition)
class AdminQuery(Query):
def all_users(self) -> Self:
print("查询所有用户")
return self
q = AdminQuery("auth_user").filter("status=1").all_users()
这里用到了 Self 保证链式调用返回的类型始终是当前类的实例,子类直接用,不会变成父类类型。type 在这里的角色是给一个复杂的回调函数起个别名:
type QueryFactory = Callable[[Any], Query]
就是定义了一个“接收一个参数并返回 Query 的可调用对象的类型”。你可以在函数参数里使用这个别名,让签名看起来更专业。
def create_query(factory: QueryFactory) -> Query:
return factory("some_table")
print(create_query(lambda t: Query(t)))
这个案例里 type 解决的问题是:把长长的 Callable 定义收拢成一个有业务含义的名字,顺便不产生运行时变量。
type 语句可以写在什么地方
它和 def 一样,是语句,所以你可以写在模块级别、函数内部,甚至类的内部。在类里定义类型别名时,它是隐式在类作用域里的,但要注意不支持泛型参数的类级别名。看个例子:
class User:
type ID = str
type Collection = list[ID]
def get(self, uid: ID) -> dict:
return {"uid": uid}
u = User()
print(u.get("abc"))
类的属性 User.ID 是类型别名,也可以像普通属性一样访问。不过类型检查器对类内部 type 的泛型支持还有点保守,建议先别在类里搞太复杂的泛型定义。
运行时能拿到 type 别名吗?
直接说结论:拿不到一个叫做那个别名的对象。看下面代码:
type MyList = list[int]
def process():
return MyList
process() # 会不会报错?
在 Python 3.12 上运行,process() 会抛出 NameError,因为根本没有 MyList 这个运行时变量。这是和旧写法最本质的区别。当然了,如果你只是在函数注解里用,它不会报错,因为注解默认不会立即求值。
有些库可能想通过 __annotations__ 来获取这些信息,但你要是希望类型别名能在运行时被动态使用,那就别用 type,继续用赋值语句。新语法是给类型检查器看的,不是给运行时留着玩的。
和 typing.TypeAlias 的区别又是什么
其实在 3.10 就有了 TypeAlias 标记,用它可能明确表示这是一个类型别名:
from typing import TypeAlias
UserID: TypeAlias = int
这玩意和 type 语句有什么不同?从类型检查角度看,它们都能提供别名语义。TypeAlias 赋值依然会创建一个实际的变量 UserID,但 type 语句不会(在运行时)。官方在 3.12 引入 type 就是为了提供一种更干净的语法,并且支持泛型参数。未来 TypeAlias 的作用可能会被逐步淡化,新代码直接用 type 就好了。
聊聊兼容性
type 语句只能在 Python 3.12 及以上的解释器里用。如果你想在旧版本项目里写,那就别想。不过你可以用 from __future__ import annotations 来延迟注解求值,但仍然不能把 type 当语句用在旧语法里。
如果你的项目还在用 3.10/3.11,又特别想用这个语法,可以把文件后缀改成 .pyi(存根文件),类型检查器会识别,但解释器不执行,所以也能用。不过这只能说是个hack。
在实际业务代码里,除非你确定部署环境是 3.12+,否则我还是建议用旧写法。大部分时候旧写法也不碍事,只是不够优雅。但如果你想在类库里把类型定义得足够清晰,并且只支持新版本,type 确实是很好的选择。
一个完整的小例子:结合 dataclass 和 type 语句
最后写一个小而完整的案例,把今天说到的点串起来。假设我们做一个简单的订单系统,需要定义一些类型别名,然后写一个函数处理订单数据:
from dataclasses import dataclass
from typing import Self
# 定义类型别名
type OrderID = int
type ItemName = str
type Quantity = int
type Row = dict[str, str | int]
type OrderTable = list[Row]
@dataclass
class Order:
order_id: OrderID
items: list[tuple[ItemName, Quantity]]
def add_item(self, name: ItemName, qty: Quantity) -> Self:
self.items.append((name, qty))
return self
def summary(self) -> OrderTable:
return [{"order_id": self.order_id, "item": n, "qty": q} for n, q in self.items]
def make_order(oid: OrderID) -> Order:
return Order(order_id=oid, items=[])
order = make_order(1)
order.add_item("鼠标", 2).add_item("键盘", 1)
print(order.summary())
这里 OrderTable 是一个类型别名,表示一个字典列表,每个字典是字符串键和字符串或整型值的组合。它让你一眼就知道 summary() 返回什么形状,比直接在注解里写一长串 list[dict[str, str | int]] 要舒服得多。
整个代码跑起来也没问题,除了 type 那几行,其余在 3.10 也能跑。如果你只是学新特性,这一段可以帮你看清它的使用场景。
最后一点感想
Python 的类型系统一直在进化,从 TypeVar 到 Self,再到 type 语句,每一点变化都让代码的意图表达更准确。虽然平时写业务代码时,不一定全都会用上,但了解这些新东西,能让你在需要设计复杂接口或类库时,比别人多一把趁手的工具。
如果你也正在升级 Python 3.12,可以试着把项目里的类型别名逐步替换为 type 语法,感受一下那种“被声明”的清爽。遇到不懂的地方,多看看官方文档,或者直接翻到 PEP 695(这是它对应的提案)。
今天的分享就到这。动手改一改,跑一跑,比光看这篇文章强十倍。

