Python 3.11 关键新特性之其他

前面整一篇只讲了 Python 3.11 的新语法特性,异常组与 except*, 这篇说其他的。

可为异常添加备注

在 BaseException 上新加了一个实例方法 add_note(self, note),在捕获到异常后可进一步润色而无需创建一个新的异常再次抛出。

内置 tomllib 支持 TOML 配置文件

TOML 是 Tom's Obvious Minimal Language, 像 INI 文件,但表述性更强,支持丰富的数据类型。现代新型的 Python 项目依赖管理构建工具都使用了 pyproject.toml 文件,如 Poetry, uv, 以及 PDM。pyproject.toml 在 Python  3.6 就引入了,见 PEF 518,但似乎一直被顽固的 Python -m venv 忽略。

从 Python 3.11 开始新加了对 TOML 配置文件的编程接口,像使用 JSON 和 Pickle 一样的 load() 和 loads() 反序列方法,没有序列化方法
1import tomllib
2
3with open("pyproject.toml", 'rb') as f:
4    data =  tomllib.load(f)
data 得到的是一个 dict[str, Any]。tomllib.loads("<toml_str>") 是从一个 TOML 配置字符串加载为字同典。

tracebacks 中用波浪线显示更精确的错误代码的位置,而不仅指示到错误代码行,这个不影响到编程,方便于 Debug

新的 -P 命令行参数和环境变量 PYTHONSAFEPATH 避免了潜在不安全的路径添加到 sys.path 中

sys.path 中的列表告诉 Python 解析器从哪里加载脚本或包,模块,见 Python 的模块搜索路径 中有对它详尽的描述, 以及可通过环境变量 PYTHONPATH 附加模块的搜索路径。

首先看新加的参数 -P 的效果,假如有一段打印  sys.path 内容的代码 demo.py
1import sys
2print('\n'.join(sys.path))
不带 -P 参数执行 python demo.py
1venv_311/bin/python demo.py
2/Users/yanbin/Workspaces/test-python
3/opt/homebrew/Cellar/python@3.11/3.11.13/Frameworks/Python.framework/Versions/3.11/lib/python311.zip
4/opt/homebrew/Cellar/python@3.11/3.11.13/Frameworks/Python.framework/Versions/3.11/lib/python3.11
5/opt/homebrew/Cellar/python@3.11/3.11.13/Frameworks/Python.framework/Versions/3.11/lib/python3.11/lib-dynload
6/Users/yanbin/Workspaces/test-python/venv_311/lib/python3.11/site-packages
看到当前路径添加在了 sys.path 的最前端

带上 -P 参数执行  python -P demo.py
1venv_311/bin/python -P demo.py
2/opt/homebrew/Cellar/python@3.11/3.11.13/Frameworks/Python.framework/Versions/3.11/lib/python311.zip
3/opt/homebrew/Cellar/python@3.11/3.11.13/Frameworks/Python.framework/Versions/3.11/lib/python3.11
4/opt/homebrew/Cellar/python@3.11/3.11.13/Frameworks/Python.framework/Versions/3.11/lib/python3.11/lib-dynload
5/Users/yanbin/Workspaces/test-python/venv_311/lib/python3.11/site-packages
这样就防止了把当前路径添加进 sys.path, python 解析器只读取当前路径上的 demo.py,而避免了加载当前路径中的其他可能不安全的模块,比如在当前路径中创建与其他库同名的模块,以达到替换实现的目的。

对于 python -m module 和 python -c code 或 python 进入 REPL 时加上 -P 参数也能实现一样的阻止当前目录作为模块搜索路径。

设置了 PYTHONSAFEPATH 为非空值时,相当于是 python 带了  -P 参数,例如
1PYTHONSAFEPATH=xx venv_311/bin/python demo.py
2/opt/homebrew/Cellar/python@3.11/3.11.13/Frameworks/Python.framework/Versions/3.11/lib/python311.zip
3/opt/homebrew/Cellar/python@3.11/3.11.13/Frameworks/Python.framework/Versions/3.11/lib/python3.11
4/opt/homebrew/Cellar/python@3.11/3.11.13/Frameworks/Python.framework/Versions/3.11/lib/python3.11/lib-dynload
5/Users/yanbin/Workspaces/test-python/venv_311/lib/python3.11/site-packages
相当于是 python -P demo.py

新增了 TypeVarTuple 支持 Variadic 泛型

TypedDict 中可表示某个 key 是必须还是可选 

 1from typing import TypedDict, NotRequired
 2
 3class Movie(TypedDict):
 4    title: str
 5    year: NotRequired[int]
 6    
 7    
 8m1: Movie = {"title": "Black Panther", "year": 2018}
 9m2: Movie = {"title": "Star Wars"}
10m3: Movie = {"year": 2022} # TypedDict 'Movie' has missing key: 'title'
默认 key 是 Required, 如 title: str 相当于是 title: Required[str]. 但 TypedDict 毕竟是 typing 的范畴,Python 解析器执行时没有强制,只是通不过像 mypy 那种工具的关。

这里学到了一个新技能,以前想要一个模型类的话,选择是 @dataclass, 或 NamedTuple, 显然用 TypeDict 要方便。

用 Self 来表示方法返回一个自身实例

比如一个方法返回自身实例 self, 试图下面那样写
1class MyLock:
2    def __enter__(self) -> MyLock:
3        self.lock()
4        return self
Python 解析不过
1    def __enter__(self) -> MyLock:
2                           ^^^^^^
3NameError: name 'MyLock' is not defined
因为此时 MyLock 还不存在,但可以用引号扩起来
1def __enter__(self) -> "MyLock":
但我们尽量避免用字符串作为类型注解,因为字符串是动态的,在 Python 3.11 中引入的 Self 来解决
1from typing import Self
2
3class MyLock:
4    def __enter__(self) -> Self:
5        self.lock()
6        return self

LiteralString 注解规定函数参数只接受字面常量字符串

字符串变量就不行
 1import sys
 2from typing import LiteralString
 3
 4def run_query(name: LiteralString):
 5    pass
 6
 7run_query("select * from students")
 8str_var=f"select * from students where {sys.argv[1]}"
 9run_query(str_var)  # Expected type 'LiteralString', got 'str' instead
10
11def bar(xxx: LiteralString):
12    run_query(xxx)  # 这是合法的,因为 xxx 也是一个 LiteralString
这有什么意义呢?在静态代码扫描时确保传入的字符串是安全的,比如不允许通过动态的方式构建一个不安全的 SQL 语句

select * from students where 1=1; delete from students

PEP 681: Data class transforms

dataclass_transform 可用过创建某些共性的模型类,应该可增强 dataclass 的行为。这里打个标记,以后真有需求时再详细研究。

最后学一个 Python 3.9 就支持的在 for 语句中用 * 星号的语法

1for a, b, c in [(1, 2, 3), (4, 5, 6)]:
2    print(a, b, c)
3
4print('---------')
5
6for a, *b in [(1, 2, 3), (4, 5, 6)]:
7    print(a, b)
输出
1 2 3
4 5 6
---------
1 [2, 3]
4 [5, 6]

总结

Python 3.11 从语言上好像没带来太亮眼的新特性,只是到了发布周期必须出一个版本而已。最主要的就是增加了 Exception Group 的 except* 语法,然而似乎应用性不大,如果 except 和 except* 搞混了,不会有语法错误,但极有可能引入替在的 Bug。

我们在阅读 What's New In Python 3.11 的时候, Improved Modules 部分虽不是重头戏,最好也别放过。其中描述了哪些 API 已删除,在使用 IDE 写 Python 代码时根据选择的 Python 版本会被识别出来。但新加的 API 还是很有用处的,如

datetime.UTC,再也不用写成  datetime.timezone.utc 了

EnumMeta 改成了 EnumType

新增了 StrEnum 用了专门表示字符串类型的枚举

使用  gzip.compress() 时指定 mtime=0 的话(即 gzip.compress(data=..., mtime=0)),压缩变快,因为它用调用 zlib.compress() 操作

math 库方面,新增了 math.exp2() 平方操作,因为太常用了。新增了 math.cbrt() 立方根.

新增了 operator.call 操作,如果一个 obj 是 callable, obj(*args, **kwargs) 可以写成 operator.call(obj, *args, **kwargs)

pathlib 的 glob() 和 rglob() 在 pattern 以路径分隔符(os.sep 或 os.altsep) 结尾的情况只列出目录, 比如下面的代码
1from pathlib import Path
2
3for p in Path(".").glob('*/'):
4    print(p, p.is_dir())
Python 3.10 执行
venv_310/bin/python demo.py
venv_311 True
venv_310 True
test-python.iml False
test.py False
.mypy_cache True
.gitignore False
venv_312 True
demo.py False
venv_313 True
.idea True
Python 3.11 执行
venv_311/bin/python demo.py
venv_311 True
venv_310 True
.mypy_cache True
venv_312 True
venv_313 True
.idea True
增加了 ZipFile.mkdir() 在 ZIP 文件内部创建目录

Python 3.11 还没有使用 JIT 编译 永久链接 https://yanbin.blog/python-3-11-key-new-features-others/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。