Python 3.9 于 2020 年 10 月发布,是 2.x 彻底退出历史舞台之后、3.x 快速迭代期里一个容易被低估的版本。它没有 3.8 海象运算符那样抓眼球,却悄悄补齐了一批"写业务代码天天用得上"的能力:合并字典终于有了专属运算符 |,类型提示终于可以直接写 list[str] 而不是 List[str],字符串去前缀后缀有了专门方法,时区数据库也进了标准库。本文带你逐一上手这些特性,并给出务实的升级建议。
1. 字典合并运算符 | 与 |=
在 3.9 之前,合并两个字典最常用的写法是 {**a, **b} 或者 a.update(b)。前者能生成新字典但可读性一般,后者是就地修改且返回 None,想"合并出一个新字典"总得绕弯子。PEP 584 为字典引入了 | 与 |= 两个运算符,让合并表达得干净利落:
a = {"x": 1, "y": 2}
b = {"y": 3, "z": 4}
merged = a | b # 返回新字典,不修改 a、b
print(merged) # {'x': 1, 'y': 3, 'z': 4}
a |= b # 就地合并,等价于 a.update(b)
print(a) # {'x': 1, 'y': 3, 'z': 4}
键冲突时的规则与 update 完全一致:右边的值胜出。区别在于语义:| 生成全新对象,适合"合并配置、不污染原数据"的函数式写法;|= 就地修改,适合逐层叠加默认值。一个典型场景是配置合并——先定义默认配置,再用用户配置覆盖:
defaults = {"host": "localhost", "port": 3306, "debug": False}
user_cfg = {"port": 5432}
final = defaults | user_cfg
print(final) # {'host': 'localhost', 'port': 5432, 'debug': False}
注意:合并运算符在 3.9 中只支持 dict 本身,3.10 之后才放宽到其它映射类型。
还有一个容易忽略的点:| 是"浅合并"——如果两个字典的某个键都对应嵌套字典,结果会整体替换而不是递归合并。需要深合并时,仍要自己写递归或借助第三方库,这一点与 update 的行为一致。
| 写法 | 行为 | 返回值 |
|---|---|---|
| {**a, **b} | 合并出新字典(3.9 前常用) | 新字典 |
| a | b | 合并出新字典(3.9 起) | 新字典 |
| a |= b | 就地合并,等价于 a.update(b) | None |
2. 类型提示泛型:list[str] 时代到来
PEP 585 让内置容器类型可以直接携带类型参数:list[str] 就是"字符串列表",dict[str, int] 就是"键为字符串、值为整数的字典"。此前必须从 typing 导入大写的 List、Dict,写法啰嗦,而且运行时还要多做一次类型检查。3.9 起这些大写别名被标记为废弃,官方建议一律改用小写内建类型,IDE 与类型检查器(mypy、pyright)的体验也随之更好:
def first_word(words: list[str]) -> str:
return words[0] if words else ""
scores: dict[str, int] = {"math": 90, "english": 85}
from typing import Optional
def find_user(uid: int) -> Optional[dict]:
"""注意:dict | None 联合类型要等 3.10,3.9 请用 Optional"""
...
一个容易踩的坑:联合类型运算符 | 在类型标注里是 3.10 才支持的(PEP 604),3.9 中表达"可能是 A 也可能是 B"仍要写 Optional[...] 或 Union[...]。3.9 的泛型只覆盖"容器元素类型"这一层。
配合类型检查器使用,收益更大:mypy 能根据 list[str] 自动推断 words[0] 的类型是 str,拼错字符串方法名时 IDE 会第一时间划红线。从"运行时防御"转向"静态检查",是 3.9 之后写 Python 的主流姿势,值得尽早养成。
3. 字符串新方法:removeprefix 与 removesuffix
处理文件名、URL、日志前缀时,我们经常要"去掉开头/结尾的固定字符串"。以前得先 startswith 判断再切片,或者动用正则;PEP 616 增加了两个直白的方法,而且前缀不匹配时原样返回,不会误伤:
name = "HelloWorld.txt"
print(name.removeprefix("Hello")) # World.txt
print(name.removesuffix(".txt")) # HelloWorld
print(name) # HelloWorld.txt,原字符串不变
一个典型场景是批量重命名文件:遍历目录,对每个文件名去掉 .tmp 后缀再改名,逻辑从"切片加判断"变成一行方法调用,代码意图一目了然。相比 lstrip,removeprefix 按完整前缀匹配,不会误删字符串中间的字符。
4. zoneinfo:内置时区数据库
处理跨时区时间以前几乎必须装第三方库 pytz。3.9 的 zoneinfo 模块把 IANA 时区数据库直接内置,配合标准库 datetime 就能拿到带时区的时间:
from datetime import datetime
from zoneinfo import ZoneInfo
now = datetime.now(ZoneInfo("Asia/Shanghai"))
print(now) # 2026-08-05 13:06:00+08:00
tokyo = now.astimezone(ZoneInfo("Asia/Tokyo"))
print(tokyo) # 自动换算时区
注意 datetime.now() 默认返回本地时区的 naive 时间,不带时区信息;用 ZoneInfo 构造的时间是 aware 的,可以直接比较大小、做加减。跨时区调度、定时任务里用它,不会再出现"差八小时"的经典 bug。
5. 其它 3.9 更新速览
- functools.cache:一行装饰器给函数加缓存,取代手写
lru_cache(maxsize=None) - graphlib:标准库新增拓扑排序模块,处理依赖关系更省事
- random.randbytes:直接生成随机字节串,做 token、盐值很方便
- PEG 解析器(PEP 617):语法分析器全面重写,错误信息更友好,也为 match 等新语法铺路
- math.gcd:支持多个参数,一次算出多个数的最大公约数
6. 如何安装 Python 3.9
推荐用 pyenv 管理多版本,一条命令装好并切换:
# 安装并启用 3.9
pyenv install 3.9.21
pyenv local 3.9.21
# 验证并准备独立环境
python3.9 --version
python3.9 -m pip install --upgrade pip
python3.9 -m venv .venv
source .venv/bin/activate
7. 值不值得升级?
说句实在话:到 2026 年,Python 3.9 已经停止官方维护(安全更新于 2025 年 10 月结束),新项目千万不要选它。它的真正价值在于"历史坐标":如果你还在维护跑在 3.6/3.7 上的老代码,把版本升到 3.9 是性价比很高的一步——升级成本低、第三方库兼容性好,还能立刻用上字典合并、内建泛型、removeprefix 等特性,为之后跳到 3.11+ 铺平道路。换个角度看,3.9 引入的 PEP 617(PEG 解析器)虽然看不见摸不着,却让后续所有新语法(match、类型参数)的落地成为可能。一句话总结:3.9 是过渡版本,学它理解演进脉络,但别让它当生产主力。
实际操作上,从 3.6/3.7 升到 3.9,最常见的兼容性问题是 async/await 保留字冲突与旧式 f-string,但绝大多数第三方库早已适配;先跑一遍测试、再灰度上线,一天内就能完成。而如果你已经在 3.8 上,直接跳到 3.11 会更划算——3.9 的维护期已经结束,没必要再停一站。
💡 学习建议:把配置合并的代码改成defaults | user_cfg,再全局搜索项目里的List[、Dict[替换成小写内建泛型,你会直观感受到"新语法让代码更短、更接近人话"。