Python版本更新历程

目前涉及项目从PY2, PY3.5到3.10一路升上来. 记录备忘下各版本有用到的新特性.

PY2

PY2TO3是个非常痛苦的过程, 目前业务上还有PY2的后台服务, 不做大的新功能功能开发的话, 没有动力去改. 此外, 系统服务永远不会默认PY3, 平常一些基础运维工具, 如ansible, supervisor等, 永远停留在了PY27. 不过换个角度想, 也是个好事情, PY2的相关依赖永远不会乱升级版本导致各种问题了, 原则上非必要不升级.

if not broken don’t fix it.

3.5

是非常重要的版本, 正式引入了类型注解 (type hint), 及协程机制 (coroutine / async / await), 此后每个版本, 都有非常大篇幅持续完善这两个方面.

当时业务很多历史PY2服务迁移目标确定为3.5, 方便顺便加上最基本的类型注解. 不过类型注解主要是文档作用, 没有执行期间的检查机制, 主要靠各大IDE集成提示警告. 需要调研下相关类型检查的工具.

3.6

字典结构实现优化, 除了性能提升, 业务上非常重要的影响是, 可以认为字典键值是保证插入顺序的, 业务逻辑可以直接废掉以往需要用collections.OrderedDict的处理步骤.

增加了f-str, 字符串格式化的语法优化逐步减少了打日志需要敲击键盘次数:

x, y = 1, .123
"x={x} y={y:.2}".format(x=x, y=y)
# after PY36
f"x={x} y={y:.2}"
# after PY38
f"{x=} {y=:.2}"

3.7

没有什么大的特性, 曾经用过time.time_ns做唯一ID生成. 不用time.monotonic原因是其结果和时间戳没有什么关系, 不好后续解析处理.

PY字典保障写入顺序遍历, 从语言层面得到了保障.

新增@dataclass方便构造类似POJO的数据对象, 可以用来替代使用namedtuple的场景. 不过目前不涉及这块儿数据结构的使用, 交互数据对象结构, 从方便角度统一字典, 需要考虑内存开销的时候用元组即可. 用PY就图个方便, 没必要OO的方式去做.

新增的几个环境变量/命令行参数比较有意思:

3.8

几个新特性都非常有用

支持条件语句中创建变量, 感觉从Go抄过来的语法. 虽然变量作用域仍然会逸出if语句, 但至少在形式上明确了变量作用域范围

a = get_file()
if f.endswith(".jpeg"):
    handle(a)
# a never used after this line

# PY38
if (a := get_file()).endswith(".jpeg"):
    handle(a)

函数签名约束可以明确/约束参数调用形式, 在业务代码里面我们鼓励甚至要求, 降低误传参数的风险, 保留接口作者腾挪的空间

def f(a, b, c=3):
    pass

def g(a, /, *, b, c=3):
    pass

# f调用方式太乱
f(1, 2, 3)
f(1, b=2)
f(a=1, b=2)
# g明确了参数调用方式
g(1, b=2)

f-str可以直接基于变量名显示, f"{duration=} VS f"duration={duration}", 极大减少了输入冗余, 对于我们打kv形式的日志格外方便, 也逼着大家把变量命名写好一些.

3.9

业务代码上经常误用str.lstrip / str.strip, 测试不充分的情况下很容易漏BUG, 如filename.rstrip(".jpeg"), url.lstrip("www.").

新增的str.removeprefix / str.removesuffix, 虽然只是s[len(prefix) :] if s.startswith(prefix) else s这样一个简单逻辑, 内置实现后速度压测会快2倍多, 对于高频调用的逻辑还是值得的.

类型注解的简化, 能够降低开发写的意愿 f(x: list[dict[str, int]]) VS f(x: typing.List[typing.Dict[str, int]]), 绝大多数情况都是内置数据类型的传递, 可以基本告别typing模块依赖.

3.10

匹配语法是个非常有用的特性, 它不仅仅是case ... when语法糖, 可以做模式匹配编程, 声明式的表达, 个人经验非常适合写业务逻辑, 可以让表达足够简洁有力. 把复杂的业务判断逻辑做精简, 平铺直叙, 不用头疼在现在多层elif嵌套里面.

类型注解支持默认的union简化表达 x: str | bytes VS x: typing.Union[str, bytes], 不过业务上尽量避免多类型参数/结果, 尽量往静态类型语言上去靠.

int.bit_count简化了之前需要bin(x).count("1")做位图统计逻辑, 性能上来说没有测到特别显著的提升.

zip(..., strict=True) 作为一个后知后觉的安全检查选项, 确保协走对象数据等长, 不过默认没有开, 可能是从兼容性的角度考虑.

@dataclass支持__slots__, 从而优化数据类的性能. __slots__明确约束了对象容许字段, 可以更好的优化内存布局, 加速对象属性访问, 并禁止了未声明字段的动态创建, 相对安全一些.

3.11

Guido”退休”后在微软”养老”的Faster CPython工作出有硕果, 声称大幅提高了速度.

从发布记录里面看, 主要是通过预加载编译代码提高启动速度; 运行时优化/复用调用栈, inline函数调用, 部分实现了尾递归优化(?), 以及类似JIT的执行机制. 感觉和JVM的优化手段思路一致.

https://github.com/faster-cpython/ideas/issues

3.12

爱写comprehension的有福了, PEP 709 将表达式内联, 不创造匿名的函数, 从而优化性能

PY性能优化

其实每次版本发布都有非常多的性能优化点记录在#optimizations章节, 这也是我们跟着版本升级后, 除了新特性外, 直接享受的改善.

拿一个jieba分词测试的结果

2.7.18 load_sec=0.77 calc_sec=8.84
3.5.10 load_sec=0.71 calc_sec=8.79
3.6.15 load_sec=0.61 calc_sec=8.53
3.7.15 load_sec=0.59 calc_sec=8.36
3.8.15 load_sec=0.58 calc_sec=8.04
3.9.15 load_sec=0.57 calc_sec=7.98
3.10.8 load_sec=0.51 calc_sec=7.34
3.11.0 load_sec=0.50 calc_sec=6.50

PY脚本主要图方便灵活, 或者说的不好听一些, 当作胶水语言. “正常”的程序, 主要瓶颈一定在于于外部IO交互, 涉及计算密集的一般委派到对应的库实现, 因此针对PY语言本省的性能优化其实大概不一定是个非常重要的事情.

PY使用者角度而言, 日常优化性能的一些手段

PY3版本升级之痛

PY3并不保证版本向前兼容, 基本每次升级, 都有一大堆依赖的各种兼容性问题需要解决, 需要调整依赖项目对新版本做兼容适配.

例如

# NOT OK since 3.10
from collections import Mapping
cannot import name 'Mapping' from 'collections'

在之前的3.9版本里面提了一嘴. 并在3.10里面正式移除. 这个其实在PY3.3里面就标记淘汰了, 但是以程序员的尿性, 没人关心DeprecationWarning.

同理pkgutil.ImpImporter, 3.3版本deprecated了, 3.12里面才正式去掉, 导致了一堆问题. 需要等各个依赖跟上.

因此每次发布, 需要特别关注#removed章节, 有责任心的三方库作者需要提前跟进#deprecated章节.

一些重要依赖的包, 如pytorch, onnxruntime等, 都是默认不支持相信版本的, 需要等打包显式支持才能用上. 这又拖慢了新版本的纳入节奏.

三方依赖的变更, 又涉及各种恶心的依赖地狱问题, 不展开.

对于我们业务代码维护的启示, DEPRECATION过程其实可以更加决绝一些, 除非有机制能够锁住并禁止新的代码调用DEPRECATED CODE, 否则DEPRECATION只是”防君子不防小人”.

依赖管理角度, 为了避免一定要锁到最细粒度, 也要声明所有间接依赖库, 目的一个是避免触发依赖检查回溯, 二是确保每次确定性的构建, 当然最好的情况是自建依赖镜像, 避免三方作者抽疯了.

非必要不要引入太多的依赖, 导致项目的脆弱性. 这点GO就做的不错, 项目没用到的依赖直接就自动删掉了. 目前我没有找到很好的办法确保PY项目最简依赖的办法.

Reference

HOME