相比于 Java 的每半年一个版本, 跟踪学习 Python 每年一版本要轻松一些. 虽然实际上 Java 是每两年一个 LTS 版, 但它的新特性却是逐个版本释放出来. 也终于赶在 Python 3.14 将在预计的 2025-10-07 发布之前能够学习总结一下当前的 Python 3.13 的新特性.
还是老办法, 从官方的 What's New In Python 3.13 中学习, 所以写作本文的目的就是阅读 What's New In Python 3.13 的学习笔记.
Python 3.13 最大的变化就是 REPL(Read-Evaluate-Print Loop) Python 控制台交互界面, 还有实验性的支持自由线程模型(free-threaded mode) - 即所谓的可禁用全局解释锁(Global Interpreter Lock), 和JIT(Just-In-Time) 编译器. 禁用 GLI 和使用 JIT 都可以让 Python 的执行性能得到提升.
更友好的 REPL 交互界面
Python 3.13 的 python 控制台一下子把早先的 bpython 和 ipython 的饭碗给抢了, 虽然 bpython 和 ipython 比 Python 3.13 的 REPL 要强很多, 但毕竟控制台下只是用来随手简单测试 Python 代码, 也就更不太可能单独安装第三方的 bpython 和 ipython 了.
看看 Python 3.13 的控制台比之前的有哪些改进, 首先输入 python 进入后看到的提示符是紫色的 >>>
, 续行时的提示符 ...
也是紫色的.
自动退格
比如输入 def foo(): 回车后下行自动退四格
def foo():
# 光标自动退到这里
历史代码以多行呈现, 并可进行多行编辑
比如之前输入过代码
1 2 3 4 |
>>> def foo(): ... print("hello") ... print("world") >>> |
后按上方向键后呈现的是上面方法 foo() 的全部三行代码, 并可对该多行代码整体编辑, 而不是像原来那样每次上方向键逐行呈现. 但是 exit 退出之后, 再 python3.13 进到 REPL 后, 上方向键也变成逐行显示了.
REPL 中直接支持 help
, exit
, 和 quit
命令
括号可以省去, 不用写成 help()
, exit()
, quit()
函数调用, 想想我之前不知道多少次碰到输入 exit 后得到提示
>> exit
Use exit() or Ctrl-D (i.e. EOF) to exit
现在只要 exit 了. 不过在 Python 3.13 还未普及之前 exit() 更保险.
错误提示中有颜色
下图左边为 Python 3.13, 右面为 Python 3.12
有更多的快捷键可用
在 REPL 界面中, 按 F1 进入 help>
, 按 F2
进浏览历史输入, 按 F3
是到 (paste)
, 可贴入更大段的代码, 在 Python 3.12 中 F1
, F2
, 和 F3
键是不起任何效果的.
用环境变量 PYTHON_COLORS=0 或 NO_COLOR=1 可以禁用 REPL 的颜色, 不过何必呢, 除非某些时候(如远端执行或子进程)不想得到颜色控制符号时会用到.
改进了错误消息显示
和 REPL 时一样, 假如用 python test.py 执行脚本时, 也能用颜色高亮显示错误信息. 同时显示的错误信息也越来越有利于我们快速定位到问题所在, 这是一个谁用谁知道的好处, 当然如果用 AI 来分析错误信息的话, 好处就没那么明显了.
自由线程 CPython
这是一个实验性的特性, 在下一个版本 Python 3.14 中正式支持. 注意只在 CPython 中有, 除了 CPython, 相信很少人用 PyPy. 可能有一个单独的执行程序 python3.13t 或 python3.13t.exe 支持自由线程, 检查了一下我本机 macOS 下的 Python3.13 不存在 python3.13t 这个执行文件. 如果从 CPython 源代码编译, 加 --disable-gil
选项编译就会生成 python3.13t 或 python3.13t.exe.
GIL 可通过环境变量 PYTHON_GIL=0, 或参数 -X gil=0
关闭, 我在本地试了下
$ ~ export PYTHON_GIL=0
$ ~ python3.13
Fatal Python error: config_read_gil: Disabling the GIL is not supported by this build
Python runtime state: preinitialized
没法关闭 GIL, 所有 sys._is_gil_enabled() 总是 True
那找一个别人做好的 Docker 镜像还试下吧
docker run -it ikeszvpvb9ye/python3.13-nogil-jit:latest
然后
root@402ff43a34a0:/# python3.13 -VV
Python 3.13.0 experimental free-threading build (main, Oct 16 2024, 22:43:49) [GCC 13.2.0]
root@402ff43a34a0:/# python3.13t -VV
Python 3.13.0 experimental free-threading build (main, Oct 16 2024, 22:43:49) [GCC 13.2.0]
root@402ff43a34a0:/# python3.13
Python 3.13.0 experimental free-threading build (main, Oct 16 2024, 22:43:49) [GCC 13.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys._is_gil_enabled()
False
那要开始体验一下关闭 GIL 后多线程产生的竞争问题, 用以下代码 test.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
from threading import Thread count = 0 if __name__ == '__main__': def add_numbers(): global count for i in range(100000): count += 1 thread1 = Thread(target=add_numbers) thread2 = Thread(target=add_numbers) thread1.start() thread2.start() thread1.join() thread2.join() print(count) |
用传统的开启了 GIL 的方式执行
for i in {1..5}; do python3.13 -X gil=1 test.py; done
200000
200000
200000
200000
200000
不管多少次, 结果永远是 200000, 因为同一时候只有一个线程获取 count 的值, 并加 1, 所以 count 是线程安全的.
而禁用 GIL 会如何呢? 再次执行
for i in {1..5}; do python3.13 -X gil=0 test.py; done
109640
109229
114238
106383
109404
简直惨不忍睹, 可以想见很多次线程操作都是取了一个旧的 count, 再加 1, 等于是白加了.
当然, 这是一个负面的例子, 并不是用来证明关闭了 GIL 的破坏性, 而是用以说明我们确实能够关闭掉 GIL, 从而在某些时候需要真正利用到多核 CPU 时 Python 能为我们带来显著的性能提升.
实验性的 JIT(Just In Time) 编译器
这是一个在许多非本地二进制代码语言早已存在的机制, 如 Java, C# 早就有 JIT, 不明白为何 Python 时至今日才想到加入 JIT, 因为 JIT 又不像关闭 GIL 那样会带来巨大的兼容性, 也许 Python 认为靠 JIT 提升那点性能不值当.
这也是要求在编译 CPython 阶段用 --enable-experimental-jit
选项开启, 官方发行的 CPython 二进制版都未开启 JIT. 如果要从源码构建, 可以参考如下方式
1 2 3 4 5 6 7 8 9 10 11 |
$ docker run -it ubuntu # apt update # apt install build-essential zlib1g-dev libncurses5-dev libgdbm-dev libnss3-dev libssl-dev libreadline-dev libffi-dev libsqlite3-dev libbz2-dev # apt install -y wget python3 clang wget https://www.python.org/ftp/python/3.13.7/Python-3.13.7.tgz tar -xf Python-3.13.7.tgz cd Python-3.13.7 ./configure --disable-gil --enable-experimental-jit make make altinstall |
需要 LLVM, 所以得事先安装 clang
完后用
python3.13 -m sysconfig | grep _Py_JIT
可以看到 JIT 相关的信息, 未启 JIT 的话看不到任何输出. Python 没有类似于 sys._is_jit_enabled() 函数
同样可以用前面那个 Docker 镜像来体验
docker run -it ikeszvpvb9ye/python3.13-nogil-jit:latest
不过怎么知道 Python 执行中是否有 JIT 在参考就不得而知了, 这完全是 Python 解释器的内部行为. 通过环境变量 PYTHON_JIT 为 0 或 1 来禁用或启用 JIT.
支持移动平台
支持像 arm64-apple-ios
, arm64-apple-ios-simulator
, aarch64-linux-andrioid
, x86_64-linux-android
, 苹果和 Android 目标平台, 以及 32 位的 arm-linux-androideabi
, i686-linux-android
移动平台.
其他主要的语言特性变化
docstring 会去除掉不必要前导空格
对于下面同样的代码
1 2 3 4 5 6 7 8 9 |
>>> def spam(): ... """ ... This is a docstring with ... leading whitespace. ... ... It even has multiple paragraphs! ... """ ... ... spam.__doc__ |
在 Python 3.13 的 REPL 中显示为
1 '\nThis is a docstring with\n leading whitespace.\n\nIt even has multiple paragraphs!\n'
而在 Python 3.12 的 REPL 中显示为
1 '\n This is a docstring with\n leading whitespace.\n\n It even has multiple paragraphs!\n '
有点像 Java 的 """ 多行字符串那样, 会找到某条左对齐线去掉每行中不必要的前导空格.
类作用域的注解可包含 Lambda 或推导
以下代码在 Python 3.13 可通过, 但在 Python 3.12 中出错
1 2 |
class C[T]: type Alias = lambda: T |
Python 3.12 中的错误为
type Alias = lambda: T
^^^^^^^^^
SyntaxError: Cannot use lambda in annotation scope within class scope
REPL 存储历史代码的 .python_history 文件可以用环境变量 PYTHON_HISTORY 修改
str.replace() 支持 count 参数, 用于替代多少次, str.replace(ol, new, /, count=-1)
新增了 dbm.sqlite3 SQLite 后端模块
base64 新增了 z85encode() 和 z85decode() 函数
compileall, concurrent.futures, multiprocessing 默认 worker 线程数由 os.process_cpu_count() 而不再是 os.cpu_count(). 这两者有何区别呢? os.cpu_count() 返回物理 CPU 总内核数, 而 os.process_cpu_count() 是返回当前进程可用的 CPU 内核数, 比如为 Docker 容器指定的 CPU 内核数时, os.cpu_count() 总是返回缩主机的 CPU 内核数目, 大于所分配的资源, 反而可能会引起性能问题.
新增了 random 命令, 用法是
python3.13 -m random --help
usage: random.py [-h] [-c CHOICE [CHOICE ...] | -i N | -f N] [input ...]
比如产生 1 到 100 之间的随机数 python3.13 -m random -i 100
新的 ReadOnly 标记 TypedDict 中的成员为只读属性
1 2 3 4 5 6 7 8 9 |
from typing import NotRequired, ReadOnly, TypedDict class Movie(TypedDict): name: ReadOnly[str] year: ReadOnly[NotRequired[int | None]] movie: Movie = {"name": "Inception", "year": 2010, } movie["name"] = "changed" |
用 mypy 校验的话显示
error: ReadOnly TypedDict key "name" TypedDict is mutated [typeddict-readonly-mutated]
ReadOnly 只能用于 TypeDict 的成员, 不能应用于 @dataclass 的成员, 下面的代码用 mypy 校验错误为
1 2 3 4 5 6 |
from dataclasses import dataclass @dataclass() class Point: x: ReadOnly[int] y: int |
错误
error: ReadOnly[] can be only used in a TypedDict definition [valid-type]
注: 除了用 mypy 外, pyright 也能做类似的事.
新增类型收窄注解 TypeIs
可以说是 TypeGuard 的增强版本. TypeGuard 从来也没用过, 需要时再深入研究.
新增 is_protocol() 方法
Python 的鸭子类型不需要显式的继承, 只要看起来像就行, 但也能用 typing.Protocol 先定义一只鸭子, 看下面的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
from typing import Protocol, is_protocol class Drivable(Protocol): def draw(self) -> None: ... class Circle: def draw(self) -> None: print("Drawing a circle") class Square(Protocol): def draw(self) -> None: print("Drawing a square") print(is_protocol(Drivable)) # True print(is_protocol(Circle)) # False print(is_protocol(Square)) # True |
is_protocol() 全局函数用来判断类是否继承自 Protocol
类中可用 typing.Final 字段
1 2 3 4 5 6 7 8 |
from typing import Final, final @final class Connection: TIMEOUT: Final[int] = 10 class X(Connection): # This will raise a mypy error TIMEOUT = 20 # This will raise a mypy error |
把 @final 也一并学了, 它和 Final 各表示什么意思, 只要往 Java 语言特性靠就行了.
用 mypy 检查的错误是
error: Cannot inherit from final class "Connection" [misc]
error: Cannot assign to final name "TIMEOUT" [misc]
看来 typing 中的每一个类型都值得看一看, 应大致了解一下它们是干什么的, 对于做 SDK 会很有帮助.
移除了 2to3, 新时代接触 Python 3 的人早已不知道 Python 2 为何物了
可以开始准备迎接 Python 3.14 了.
本文链接 https://yanbin.blog/python-3-13-new-features/, 来自 隔叶黄莺 Yanbin Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。