在 AI FIRST 的年代到底还要不要对每个所用语言新特性有所了解呢?就像有了 AI 还需要升级 Python 吗?虽然如今在 ChatGPT 中问一句话就能出来一篇比我想要写的好的多的博客,但不多思考怕会退化。
Python 3.14 于 2025 年 10 月 7 日发布,也就是前天,比起 Java 的发布节奏还是慢半拍,所以才能跟得上它的步伐。还是老方法,在官方的 What's new in Python 3.14 中吸收最原始的滋味,完后再去参考别人家的总结。
Python 官方说 Python 3.14 最大的变化包括 t-string(模板字符串),注解的延迟求值,和子解释器的支持(用以使用自由线程)。再就是标准库的变化 asyncio 的内省功能,支持 Zstandard 压缩,以及 REPL 有了语法高亮了.
总体来说这个版本比 Python 3.13 新特性更有亮点,在 Python 3.13 中自由线程是实验性的,在 Python 3.14 可通过子解释器来使用,和自由线程一样,Python 3.13 中的 JIT 需以源代码通过编译选项获得,在 Python 3.14 中 JIT 仍为实验特性,但官方发布的 Python 3.14 二进制版已包含实验性的 JIT 编译器。
REPL 支持语法着色高亮显示
为什么首先说这个特性,没有为什么。在 IDE 普遍的年代其实 Python 的 REPL 使用场景不多。在 Python 3.13 的 REPL 中首先使用了紫色的提示符,换行自动退格,块编辑,和错误信息的着色显示,代码的关键字等没有高亮显示。在 Python 3.14 中把这一块补齐了,还有更自然的 <Tab> 建议提示,如 import 时多用 <Tab> 键试试; 语法高亮的主题也可以选择。
t-string - 模板字符串
这前的 f-string, f 代表 format, 格式字符串是自 Python 3.6 引入的,其实 f-string 用得都还不熟,f-string 需对应到 str.format() 方法,比如它有宽度,对齐等,快捷调用方法等
f"{pi:10.2f}"
f"{name:<10}"
f"{obj!r}"
现在又引入了一个 t-string 模板字符串,t 自然就是 template。f-string 是一个字符串,而 t-string 不是一个 String, 它是一个 Template 对象,仅此而已。如何处理该 Template 就自已选择了。
参考官方的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
>>> variety = 'Stilton' >>> template = t'Try some {variety} cheese!' >>> template Template(strings=('Try some ', ' cheese!'), interpolations=(Interpolation('Stilton', 'variety', None, ''),)) >>> type(template) <class 'string.templatelib.Template'> >>> list(template) ['Try some ', Interpolation('Stilton', 'variety', None, ''), ' cheese!'] >>> str(template) "Template(strings=('Try some ', ' cheese!'), interpolations=(Interpolation('Stilton', 'variety', None, ''),))" >>> template.strings ('Try some ', ' cheese!') >>> template.interpolations (Interpolation('Stilton', 'variety', None, ''),) >>> >>> "".join([item if isinstance(item, str) else str(item.value) for item in template]) 'Try some Stilton cheese!' >>> >>> variety = "American" >>> "".join([item if isinstance(item, str) else str(item.value) for item in template]) 'Try some Stilton cheese!' |
从上面我们可以理解 t-string 是什么,并不能直接转换为字符串的,它内部存有直接字符串字面量,和点位符及将被替代的值,Template 内部的替代值是在运行时确定的,所以改变 'variety' 并不会改变 template 的内部状态。t-string 把最终的字符串如何渲染留给了实现者,如渲染为 HTML 代码,日志格式输出等。
标准库支持多解释器
还是直接跳到 Python 3.14 最亮丽的特性吧, 标准库支持多解释器了,这样在单进程中使用多个解释器时,就突破了 GIL 的约束。多线程中即使能用到了多核,由于存在 GIL 也是白搭,启动多个解释器就能更好的发挥多核的性能,子解释器是线程与进程间折衷的产物。
其实 CPython 运行时早就支持单进程中多 Python 解释器,自 Python 1.5, 已有超过 20 年的历史,但仅限于使用 C-API,如今在 concurrent.interpreters 模块中开放给了 Python 代码使用多解释器。使用多解释器有两个显著的好处
- 支持更友好的并发编译模型
- 多核并发
一句话就是简单的写出适于多核并发的 Python 程序。
Python 的并发编程模型有 threading, multiprocessing, 而 threading 在 GIL 的裹挟之下,完全丧失了像其他语言之中多线程的并发能力,multiprocessing 多进程让数据共享有些困难。Python 3.14 的 多个解释器运行在同一个进程当中,既能解决并发,同时数据共享也更容易。
与此特性相关的 PEP 是 PEP 734 - Multiple Interproeters in the Stdlib, 它的前身是 PEP 554 -- 这里面描述的内容更易懂。
Python 3.14 在 concurrent 包下新增了 interpreters 模块,以前只有模块 futures。可是在当前的 IntelliJ IDEA 2025.2.3 或 PyCharm 2025.2.3 中即使是选择了 Python 3.14 SDK(如虚拟环境),编辑器都不能识别 concurrent.interpreters
模块,但不影响运行
目前想要 IntelliJ IDEA 或 PyCharm 支持 concurrent.interpreters 的话,可以把 Python 3.14 安装目录中的 concurrent/interpreters 拷贝到虚拟环境的 lib/python3.14/site-packages 中让 IDE 当作一个第三方依赖来对待。如果用 Visual Studio Code, 它可以完美支持 import concurrent.interpreters, 因为它根本用着更完美的支持各版本 Python 的新特性。
在 concurrent.interpreters 的 __init__.py 中的 __all__ 为
1 2 3 4 5 6 7 |
__all__ = [ 'get_current', 'get_main', 'create', 'list_all', 'is_shareable', 'Interpreter', 'InterpreterError', 'InterpreterNotFoundError', 'ExecutionFailed', 'NotShareableError', 'create_queue', 'Queue', 'QueueEmpty', 'QueueFull', ] |
也就是用
from concurrent.interpreters import *
可以直接引入的类和函数,关于主要的 API 还是摘录一些来自于 PEP 554 的表格
函数 | 描述 |
list_all() -> [Interpreter] | 列出进程中所有的解释器 |
get_current() -> [Interpreter] | 获得当前解释器 |
get_main() -> [Interpreter] | 获得主解释器, 主解释器为 Interpreter(0), 其他为 1... |
create() -> [Interpreter] | 创建一个新的解释器 |
is_shareable(obj) -> bool | 判断一个对象是否在解释器间共享 |
Interpreter 实例的属性和方法
函数 | 描述 | ||
id | 解释器的 id, 如 0, 1, 2... | ||
is_running() -> bool | 解释器是否正在运行 | ||
prepare_main(**kwargs) | 给解释器绑定值 | ||
exec(code, /) | 执行代码,代码在当前线程中执行 | ||
call(callable, /) |
执行可调用对象(基本就是函数),返回值被忽略。函数中抛出的异常被传播为 ExecutionFailed 异常。不能直接传递参数,但能用 prepare_main() 来绑定 这种方式对比 exec(code, /) 要方便些,因为不需要把代码以文本方式存在,import 语句等可写在主程序代码中,但执行时和 exec(code, /) 是一样的,callable import 的语句还是会在解释器在重新解释 |
||
call_in_thread(callable, /) -> threading.Thread |
在新线程中调用 Interpreter.call(), 返回值被忽略,并且异常不被传播(消声). 它就是下面的快捷方式
|
||
close() | 销毁解释器 |
接下来将学习如何创建新的解释器来执行代码,以及解释器间数据应如何共享。我们在使用多线程时,代码中看到的全局变量是天然共享的,所以一个线程修改了全局变量会反应到另一个线程中; 而使用多进程(进程池)时,全局变量是隔离的,交换数据涉及到进程间的通信; 解释器间共享数据也不像多线程那么容易,但毕竟多解释器都在同一进程之中,所以共享数据要比进程简单些。
快速回顾一下 Python 多进程的编程方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
from multiprocessing import Process import os count = 0 def task(num): global count count += 1 print(f"process: {os.getpid()}, in task {num}: {count=}, count id: {id(count)}") if __name__ == '__main__': p1 = Process(target=task, args=(1, )) p1.start() p1.join() print(f"process: {os.getpid()}, after p1: {count=}, count id: {id(count)}") p2 = Process(target=task, args=(2, )) p2.start() p2.join() print(f"process: {os.getpid()}, after p1: {count=}, count id: {id(count)}") |
输出结果为
process: 29616, in task 1: count=1, count id: 4369625024
process: 29611, after p1: count=0, count id: 4387106720
process: 29621, in task 2: count=1, count id: 4321030080
process: 29611, after p1: count=0, count id: 4387106720
虽然看到的 count
是一个全局变量,但映射到进程中则会分配为各自的 count=0
变量,进程间互不干涉。如果是多线程来修改 count 的值,则会影响到每一个线程中的 count 值。
创建一个解释器用 interpreters.create(), 通过新的解释器执行代码的方法有
- exec(self, code, /)
- call(self, callable, /, *args, **kwargs)
- call_in_thread(self, callable, /, *args, **kwargs)
简单的执行一行代码
1 2 3 4 5 |
from concurrent import interpreters interp = interpreters.create() print('before') interp.exec('import time\nprint("during, sleep 3 seconds...")\ntime.sleep(3)') print('after') |
用 interp.exec() 执行一行代码时需要用各种转义符处理换行,退格。上面的代码输出为
before
during, sleep 3 seconds...
after
观察输出可判断主解释器是会等待子解释器执行完成后再接着往下走
执行更大的代码片断
用多行字符串的方式
1 2 3 4 5 6 7 |
interp.exec(""" import time print("during, sleep 3 seconds...") time.sleep(3) for i in range(3): print(f"count {i}") """) |
或者用 textwrap
1 2 3 4 5 6 7 |
import textwrap as tw interp.exec(tw.dedent(""" import time print('inside interpreter sleep 3 seconds...') time.sleep(3) """)) |
以上的 tw.dedent(...) 返回的就是一个 str 类型,字符串的值为
"\nimport time\nprint('inside interpreter sleep 5 seconds...')\ntime.sleep(5)\n"
为什么有了 """
, 还要 textwrap, 因为 textwrap 配 """ 有了与 Java 的多行字符串相同的效果,它会自动找到左边纵向的起始线,否则不用 textwrap 的话,interp.exec("""<code>""") 代码必须顶格写成
1 2 3 4 5 |
interp.exec(""" import time print('inside interpreter sleep 3 seconds...') time.sleep(3) """) |
如果没有 textwrap 的情况下写成
1 2 3 4 5 |
interp.exec(""" import time print('inside interpreter sleep 3 seconds...') time.sleep(3) """) |
将会出现错误
IndentationError: unexpected indent
既然 exec() 可接收大段的 Python 代码,当然可以把代码写在一个外部文件中,然后读入执行,只要保证代码格式就行。
往解释器中传递值
1 2 3 4 5 6 7 8 9 10 11 12 13 |
interp = interpreters.create() x=3 interp.prepare_main(a=x, b=2) interp.exec(tw.dedent(""" from concurrent.interpreters import get_current print(f'{get_current()}: {a = }, {b = }') a = a + 10 print(f'{get_current()}: {a = }') """)) print(f"{interpreters.get_current()}: {x = }") |
对于输出应该不会有什么惊讶, 值传递,解释器中显然无法撼动外面的变量值
Interpreter(1): a = 3, b = 2
Interpreter(1): a = 13
Interpreter(0): x = 3
int 是值传递,那如果 prepare_main 传递一个列表类型的值是不是能在解释器内部修改内容呢?
1 2 3 4 5 |
x = 3 y = [2] print(interpreters.is_shareable(x)) print(interpreters.is_shareable(y)) interp.prepare_main(b=y) |
上面的代码输出并有异常
True
False
interp.prepare_main(b=y)
~~~~~~~~~~~~~~^^^^^
concurrent.interpreters.NotShareableError: [2] does not support cross-interpreter data
所以只能传递用 prepare_main 传递 sharable 的变量。
Shareable 的对象用 interp.prepare_main 直接传递,如果借助于 interpreters.Queue 来传递数据的话,只要对象是可被 Pickle 序列化的就能传递。假如对象实现了协议 buffer
, 数据将被包裹为 memoryview
对象在解释器间共享。多数情况下我们都会选择用 interpreters.Queue 进行解释器间的数据交换,即使解释器间的同步也要靠它。
解释器也可能交互 Mutable 的数据,以下将进行验证。
用 interpreters.Queue 解释器间交换数据的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
from concurrent import interpreters import textwrap as tw x = [2] y = bytearray(b"Hello, World!") data1 = interpreters.create_queue() data1.put(x) data2 = memoryview(y) interp = interpreters.create() interp.prepare_main(data1=data1, data2=data2) interp.exec(tw.dedent(""" x = data1.get() x.append(3) print(x) # [2, 3] data1.put(x) data2[7:13] = b"Python" print(data2.tobytes()) # b'Hello, Python!' """)) print(x) # [2] print(data1.get()) # [2, 3] print(data2.tobytes()) # b'Hello, Python' |
这是一个 Interpreter[0] 与 Interpreter[1] 两个解释器间双向交换数据的例子, 分别使用了 interpreters.Queue 和 memoryview 两种方式,从上可看到
- 由 interpreters.Queue 共享的数据是 Immutable, 它们实际经由 Pickle 进行序列化/反序列化交换的
- 而通过 memoryview 内存映射方式交换数据,相当于直接操作相同的内存区域。解释器间对数据的修改会相互影响,所以数据也不需用 Pickle 序列化,但要考虑数据竞争的情况
新的 InterpreterPoolExecutor
和 ThreadPoolExecutor 和 ProcessPoolExecutor 一样,Python 3.14 也为 Interpreter 提供了 InterpreterPoolExecutor
from concurrent.futures import InterpreterPoolExecutor
即能像线程/进程池一样的使用解释器池。
要重点关注 InterpreterPoolExecutor 的以下几个方法
1 2 3 4 5 6 7 8 9 10 11 |
__init__(self, max_workers=None, thread_name_prefix='', initializer=None, initargs=()): prepare_context(cls, initializer, initargs): return WorkerContext.prepare(initializer, initargs) submit(self, fn, /, *args, **kwargs) map(self, fn, *iterables, timeout=None, chunksize=1, buffersize=None) shutdown(self, wait=True, *, cancel_futures=False) |
下面是应用 InterpreterPoolExecutor 的演示
1 2 3 4 5 6 7 8 9 10 11 12 13 |
from concurrent.futures import InterpreterPoolExecutor from concurrent.interpreters import get_current def task(a, b): print(f"hello from interpreter {get_current()}, {a = }, {b = }") b.append('c') print(b) # ['a', 'b', 'c'] x = ['a', 'b'] with InterpreterPoolExecutor() as executor: executor.submit(task, 123, x) print(x) # ['a', 'b'] |
程序输出查看代码中的注释。Python 3.14 只是仿照了 ThreadPoolExecutor 那样实现,实际上解释器间的数据传递仍然是要用到 prepare_main() 或 interpreters.Queue。
另外,和多线程,多进程一样,实际使用多解释器时应多加留意异常是如何传播的,避免执行当中出现了异常,因没有往外传播,或传播的异常类型有别于期待的类型而造成难以定位问题。
学习完多解释器后,发现篇幅太大了,有必要另行一篇学习其余的 Python 3.14 新特性,故为本文冠上第一部分,后续会有第二部分。
本文链接 https://yanbin.blog/python-3-14-new-features-1/, 来自 隔叶黄莺 Yanbin Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。