Skip to main content

Python3.13 介绍

wKevin

摘要:

!python -V
Python 3.13.1

基本信息

6 个新特性

  1. 解释器吸收了 PyPy 项目 REPL 的一些特性
  2. 错误提示改进:使用彩色、位置更精准
  3. 自由线程:不受 GIL 管理的 —— 实验特性,默认关闭
  4. JIT 终于来了:即时编译器,编译机器码 —— 实验特性,需打开编译开关
  5. locals() 修改语义
  6. iOS 和 Android 开始支持

新的交互式解释器(interpreter)

  • Interpreter(解释器):负责解析、编译(将代码转换为字节码)并运行代码,解析的代码可以从 .py 文件,也可以是通过 REPL 得到的 stdin。
  • REPL(read-eval-print loop,读取-求值-打印-循环):与 Interpreter 交互,即通过命令行或终端启动时的交互式编程环境。终端输入 pythonpython3ipython 等可启动 REPL
    • pythonpython3 是 Interpreter 的一部分,即:Interpreter 的交互模式。
    • ipython 是 3rd 的独立、增强 REPL,所以有独立的版本号。
$ python -c "import IPython;print(IPython.__version__)"
8.30.0

日常还会提到 shell 或 python shell,这个概念与 REPL 有一些细微差别:

  • shell 一个更广泛的概念,可以包括图形界面或其他类型的命令行接口(比如 zsh)。
  • REPL 具体描述与 python interpreter 其操作过程、交互流程。
  • 两个概念大部分语境下是相同的。

来看看新的 interpreter 的交互模式,即:interactive shell 有什么新功能:

1、 具有历史保存的多行编辑

首先多行编辑更友好了:

$ python
Python 3.13.1 (main, Dec 11 2024, 10:33:32) [GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> def foobar():
...
...
... print("hello")
...
>>>

foobar(): 后回车会直接缩进 —— 3.12 版本是没有缩进的。

连续回车并不会退出函数定义,直到 print... 写下函数体之后,再 2 次换行才退出 —— 3.12 是始终无法多次换行的

其次是有历史记录的多行编辑:

同样操作在 3.12 中是不行的。

2、 对 REPL 专属的命令如 help, exit 和 quit 的直接支持,无需以函数形式调用它们。

$ python
Python 3.13.1 (main, Dec 11 2024, 10:33:32) [GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> quit

以前 3.0+ 则必须 quit() 加上括号,这又回退到 2.x 版本的方式了,呵呵。

3、 Prompts and tracebacks 彩色显示

五彩斑斓的 output,而 3.12 则都是黑白色的:

最后,如果你不喜欢 3.13 中新的 REPL,还想用以前的,可以通过环境变量实现:

$ export PYTHON_BASIC_REPL=1
$ python

Free-threaded CPython

Free-threaded 即禁用 GIL(全局解释器锁)的模式,该模式在 3.13 中仍是实验性质,默认关闭,需要 --disable-gil 来手动打开。

$ ./configure --disable-gil
$ make -j$(nproc)
$ ./python -VV
Python 3.13.1 experimental free-threading build (main, Dec 23 2024, 18:23:38) [GCC 9.4.0]

这样编译得到 Free-threaded 模式的 python,python -VV 中的 experimental free-threading build 也是对此的说明。

使用 --disable-gil 编译出的 python 在运行中也可以使用 GIL,通过环境变量或参数来实现:

$ PYTHON_GIL=1
$ python foobar.py

$ python -X gil=1 foobar.py

gil=[0|1] 0 表示禁用 GIL,只能用在 --disable-gil 的 python 中;1 表示使能 GIL,可以用在任何 python 中。

我们用一个计算密集型函数验证一下 Free-threaded 模式的性能:

import sys
import threading
import time


def compute_heavy_task(n):
total = 0
for i in range(n):
total += i * i # 简单的平方和计算
return total


# 测试多线程性能的函数
def test_multithreading(num_threads, iterations_per_thread):
threads = []
start_time = time.time()

for _ in range(num_threads):
thread = threading.Thread(
target=compute_heavy_task, args=(iterations_per_thread,)
)
threads.append(thread)
thread.start()

for thread in threads:
thread.join() # 等待所有线程完成

end_time = time.time()
print(
f"Total execution time with {num_threads} threads: {end_time - start_time:.4f} seconds"
)


if __name__ == "__main__":
num_threads = 8
iterations_per_thread = 10_000_000
print(f"{sys.version}\n{num_threads}\n{iterations_per_thread}")
test_multithreading(num_threads, iterations_per_thread)
3.13.1 (main, Dec 11 2024, 10:33:32) [GCC 9.4.0]
8
10000000
Total execution time with 8 threads: 13.5425 seconds
$ python test_multithreading.py
3.13.1 (main, Dec 11 2024, 10:33:32) [GCC 9.4.0]
8
10000000
Total execution time with 8 threads: 11.5728 seconds
$ Python-3.13.1--disable-gil/python test_multithreading.py
3.13.1 experimental free-threading build (main, Dec 23 2024, 18:23:38) [GCC 9.4.0]
8
10000000
Total execution time with 8 threads: 2.5263 seconds

有 GIL 11.5s,无 GIL 的 2.4s —— 性能提示非常明显。

对于单线程(num_threads = 1),性能提升就不明显了,甚至还有些微增长:

$ python test_multithreading.py
3.13.1 (main, Dec 11 2024, 10:33:32) [GCC 9.4.0]
1
10000000
Total execution time with 1 threads: 1.4707 seconds
$ Python-3.13.1--disable-gil/python test_multithreading.py
3.13.1 experimental free-threading build (main, Dec 23 2024, 18:23:38) [GCC 9.4.0]
1
10000000
Total execution time with 1 threads: 1.4772 seconds

去掉 GIL 是很多年前就提出的,一直都被诟病为 python 性能瓶颈主要杀手,但 3.13 才开始实验性质去掉,说明这个 GIL 还是有一些棘手的,比如去掉 GIL 后可能存在的问题:

  • 内存管理问题:在自由线程模式下,一些对象可能永生,即这些对象不会被重新分配,其引用计数也不会被修改。这种设计是为了避免引用计数争夺,但可能导致内存使用量增加。
  • 不安全的并发访问:自由线程模式下,对某些对象的并发访问是不安全的,脏数据等以前不存在的问题都会回来,需要用户来保障脏数据问题。
  • C 扩展兼容性问题:python 调用 C 的库函数时,并发所有 C 函数都是并发安全的,这一块是 python 库开发者保障,还是交给用户保障 —— 这会是个很难纠扯清楚的事情,python 库开发者肯定会因为这个被骂好多年。被骂的狠了,说不定 3.14 就把这个 GIL 又强制加上了。

更多的关于去掉 GIL 的设计思路和方案,可参考 PEP-703

JIT 编译器

从 Python 创始至今,运行效率一直是短板和被人诟病, 除了 GIL 影响多线程效率,就是解释运行而不是编译运行,即:CPython 解释器将 Python 源代码编译成字节码,然后逐条解释执行 —— 这种简单的方式给 python 带来了跨操作系统、易用、繁荣的场景,但性能已一直被诟病。

从用户的代码到最终 CPU 上运行有几种方式:

  1. AOT(Ahead-Of-Time)编译:提前编译,即:编译阶段就编译成机器码,运行时直接运行机器码 —— C/C++ 就是典型的 AOT。
  2. JIT(Just-In-Time)编译:即时编译,即:编译阶段输出中间码,运行时即时编译成机器码进行执行(首次运行会慢点,但多次运行则效率与 AOT 相同)—— Java、C# 是这种。
  3. 无机器码编译:即:编译阶段输出中间码,运行时解释器逐行解释并运行中间码,没有机器码生成和执行过程 —— 3.12 及之前的 Python 是这种。

没有 JIT 与 执着于 GIL 一样,都被喷了很多年了,今天他俩一起来了!

仍然也是实验性质,默认关闭,需要自己通过 --enable-experimental-jit 打开:

./configure --enable-experimental-jit
make -j$(nproc)
make install

其实,Python 多年来也出现了一些 3rd 的 JIT 将 python 字节码翻译成机器码,比如 PyPy、GraalPy、Numba、Cython…… 但 python 官方的 CPython 一直没有出手,虽然前几年也预告过要做,但始终不见发布。

但其实也并不是直接编译成机器码这个方式,因为在运行时编译成机器码——没有完整的 GCC、Clang 等 ToolChain 是不现实的,所以 cpython JIT 还加入了:局部优化、分级特化、拷贝补丁式机器码

其内部流程大致如下:

  • 将从特化的 第 1 层级字节码 开始。—— 参阅 3.11 新特性了解详情。
  • 当第 1 层级字节码达到足够热度,它将被翻译为新的纯内部的中间表示形式 (IR),称为 第 2 层级 IR,有时也称为微操作码 ("uops")。
  • 第 2 层级 IR 使用与第 1 层级相同的基于栈的虚拟机,但其指令格式更适合被翻译为机器码。
  • 在第 2 层级 IR 被解释或翻译为机器码之前,我们会预先应用一些优化通路。
  • 虽然第 2 层级解释器存在,但它主要用于对优化管线的先前阶段进行调试。可通过为 Python 配置 --enable-experimental-jit=interpreter 选项启用第 2 层级解释器。
  • 启用 JIT 时,经优化的第 2 层级 IR 将被翻译为机器码后再执行。
  • 这个机器码翻译过程使用了名为 拷贝并打补丁 的技巧。 它没有运行时依赖,但增加了构建时对 LLVM 的依赖。

即使如此,对特定硬件或 OS 的支持也是困难的,所以官方做了平台优先级分级,一级平台:

  • aarch64-apple-darwin/clang
  • aarch64-pc-windows/msvc [1]
  • aarch64-unknown-linux-gnu/clang [2]
  • aarch64-unknown-linux-gnu/gcc [2]
  • i686-pc-windows-msvc/msvc
  • x86_64-apple-darwin/clang
  • x86_64-pc-windows-msvc/msvc
  • x86_64-unknown-linux-gnu/clang
  • x86_64-unknown-linux-gnu/gcc

其他平台的可能就算是二级公民了,可能永远不会得到 JIT 支持。

缺陷也有,比如:

  • C 代码的分析器和调试器目前似乎无法回溯 JIT 帧,导致在 debug 过程中跟踪不住 JIT 帧。
  • JIT 在运行时生成大量可执行数据。这为 CPython 引入了一个潜在的新攻击面。

更多的内容参考:PEP-744

网上也有网友对早期的 3.13.0b1 版本做过 JIT、GIL 的性能测试,比如:

https://github.com/lip234/python_313_benchmark

name--disable-gil--enable-experimental-jit
stockNN
jitNY
nogilYN
jit+nogilYY

测试的结果和我的想象有一些不一致,可能是 3.13 的早期版本与正式版本有差异,也可能是测试用例有问题,还有可能是这两个特性可能还不成熟。

iOS、Android 的支持

两个 PEP 都已经在 3.13 中实现。

在 Android 上,没有将安装作为系统资源的概念。软件分发的唯一单位是“应用程序”。此外,也没有控制台可供您运行可执行文件,或与 Python REPL 进行交互。

因此,在 iOS/Android 上使用 Python 的唯一方法是 embedded mode:即编写一个原生 Android 应用程序,使用 libpython 嵌入一个 Python 解释器,并使用 Python embedding API 调用 Python 代码。然后,完整的 Python 解释器、标准库和所有的 Python 代码都会被打包到您的应用程序中,供其自己使用。

简单说:就是实现 C/C++ 调用 Python —— 这与我们平常在 Python 中调用 C/C++ 趁好相反。

特别要注意:Python 标准库对 Android 有一些明显的遗漏和限制。

但 iOS 还有一些特殊:

  • 有更友好的 3rd 开发环境:
    • Pythonista :一个完整的开发环境,可以在 iPad 或 iPhone 上编写包括第三方库和系统集成在内的 Python 脚本。
    • Pyto :为运行 Python3 提供了完整的开发环境,包括许多第三方库和 iPad 或 iPhone 上的系统集成。
  • 跨平台的开发框架:一套代码,在 iOS、Android、Windows、MacOS、Linux、Web 和 tvOS 上编译、发布、运行。
    • BeeWare: GUI 库使用 Toga 或 macOS、Android 的原生 GUI 库。
    • Kivy:使用 OpenGL 渲染,自定义 UI 控件,所以看起来没有 BeeWare 原生。

一些语言特性修改

  • 去除 docstr 中共有的前导空格,节省字节码大小
$ python
Python 3.13.1 (main, Dec 11 2024, 10:33:32) [GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> def spam():
... """
... This is a docstring with
... leading whitespace.
... It even has multiple paragraphs!
... """
...
>>> spam.__doc__
'\nThis is a docstring with\n leading whitespace.\nIt even has multiple paragraphs!\n'
$ python
Python 3.12.8 (main, Dec 11 2024, 10:54:36) [GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> def spam():
... """
... This is a docstring with
... leading whitespace.
...
... It even has multiple paragraphs!
... """
...
>>> spam.__doc__
'\n This is a docstring with\n leading whitespace.\n\n It even has multiple paragraphs!\n

请注意 spam.__doc__ 两次打印中一次没有空格,一次有。

1 个新模块

dbm.sqlite3

先来回顾一下 python 标准库中的 sqlite3 模块:提供一种轻量级的基于磁盘的数据库 DB-API 2.0 接口。

DB-API 2.0 接口是由 PEP 249 定义的关系型数据库接口规范,使得开发者可以使用统一的接口与多种数据库进行交互,而无需关心具体的数据库实现细节。

下面是一段最简的 sqlite3 典型使用:

import sqlite3

# 连接到 SQLite 数据库(如果数据库不存在,则会自动创建)
conn = sqlite3.connect("example.sqlite3")

try:
# 创建一个游标对象,用于执行 SQL 命令
cursor = conn.cursor()

# 创建一个名为 'users' 的表
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
age INTEGER NOT NULL
)
"""
)

# 插入一些数据
cursor.execute("INSERT INTO users (name, age) VALUES (?, ?)", ("Alice", 30))
cursor.execute("INSERT INTO users (name, age) VALUES (?, ?)", ("Bob", 25))

# 提交事务(确保数据被写入数据库)
conn.commit()

# 查询所有用户
cursor.execute("SELECT * FROM users")
users = cursor.fetchall()

for user in users:
print(user)

finally:
# 关闭游标和连接
cursor.close()
conn.close()
(1, 'Alice', 30)
(2, 'Bob', 25)

这样就创建了一个 example.sqlite3 文件,用其他 sqlite 软件打开可以看到 Alice、Bob 的数据已经加入,比如:

$  sqlite3 example.sqlite3
SQLite version 3.37.2 2022-01-06 13:25:41
Enter ".help" for usage hints.
sqlite> .tables
users
sqlite> select * from users;
1|Alice|30
2|Bob|25
sqlite> .quit

dbm.sqlite3sqlite3 是完全不同的 2 个模块。

DBM(Database Manager)是一种简单且高效的键值对存储系统:

  • 每个键都是唯一的,并且与一个对应的值相关联
  • DBM 数据通常以文件的形式存储在磁盘上,这使得数据持久化并且可以在程序之间共享。
  • DBM 有多种具体的实现,每种实现可能使用不同的底层技术来优化性能或适应特定的需求。包括:
    • GNU DBM (gdbm):一种流行的实现,广泛用于 Unix 和类 Unix 系统。
    • NDBM (ndbm):一种较旧的实现,通常与 Berkeley DB 相关联。
    • DumbDBM:一个纯 Python 实现,适用于所有平台。
import dbm.gnu as dbm  # 使用 gdbm 后端

# 打开数据库文件,如果文件不存在则创建一个新的数据库文件
with dbm.open('example.gdbm', 'c') as db:
# 插入数据
db['API_KEY'] = b'123456' # 键和值都必须是字节类型
db['API_AUTH'] = b'yes'

# 遍历所有键值对
for key in db.keys():
print(f"{key}: {db[key]}")
b'API_KEY': b'123456'
b'API_AUTH': b'yes'

上面代码会创建 example.gdbm 文件,用 file 命令可以查看:

$ file example.gdbm
example.gdbm: GNU dbm 1.x or ndbm database, little endian, 64-bit

很多工具可以查看 gdbm 文件,比如 Linux 上的 gdbmtool(sudo apt-get install gdbmtool

$ gdbmtool example.gdbm

Welcome to the gdbm tool. Type ? for help.

gdbmtool> fetch API_KEY
123456
import dbm.sqlite3 as dbm  # 使用 gdbm 后端

# 打开数据库文件,如果文件不存在则创建一个新的数据库文件
with dbm.open('example.sqlitedbm', 'c') as db:
# 插入数据
db['API_KEY'] = b'123456' # 键和值都必须是字节类型
db['API_AUTH'] = b'yes'

# 遍历所有键值对
for key in db.keys():
print(f"{key}: {db[key]}")
b'API_AUTH': b'yes'
b'API_KEY': b'123456'

键值对数据会存入到 sqlite3 的 Dict 表中,example.sqlitedbm 文件也可以使用 sqlite 工具打开:

$ sqlite3 example.sqlitedbm
SQLite version 3.37.2 2022-01-06 13:25:41
Enter ".help" for usage hints.
sqlite> .tables
Dict
sqlite> select * from Dict;
API_KEY|123456
API_AUTH|yes

改进模块

移除、废弃模块

移除了 19 个模块,不能再 import ;但都是非常不常用的:aifc、audioop、chunk……就不一一细说了。

废弃了十多个模块中的部分函数或方法,可以 import,但会得到废弃提示。

3.14 展望

https://docs.python.org/zh-cn/3.14/whatsnew/3.14.html

新特性

  • 标注(annotation)不再急于求值,改为必要时求值。—— 提升大多数情况下的性能
  • 继续改进错误提示 —— 每个版本都在持续改进,python 尚且如此,我们自己的代码的错误提示也要由此精神啊。
  • 统一初始化配置方式:以前 C 的配置使用 C 风格的方式,现在通过 PyInitConfig C API 方式与 python 统一。

仍在持续修订中……