Python 3.14 (π) 正式发布:值得尝试的酷炫新功能
注意,一旦交互式环境拥有足够的上下文来解析你的输入,颜色就会随着你的输入而变化。特别是,诸如下划线 ( _)之类的标记仅在模式匹配的上下文中被识别为软关键字,Python 会用不同的颜色突出显示它们以进行区分。例如,当你在给定代码行上设置breakpoint()时,这种彩色输出也会显示在Python 调试器 (pdb)中。
此外,一些标准库模块现在可以利用 Python 解释器的这种新的语法着色功能:
Python 3.14 标准库模块中的彩色输出
argparse模块显示彩色帮助消息,calendar突出显示当前日期,并支持JSON文档的精美打印和彩色显示json。最后,unittest模块还为失败的断言提供了彩色输出,以便于阅读和诊断。
如果你不喜欢 Python 3.14 中的默认颜色,可以使用实验性主题支持进行自定义。为了使调整持久化,你可能需要指定一个自定义Python 启动脚本,如下所示:
# Set a custom color theme in Python 3.14
try:
from _colorize import ANSIColors, default_theme, set_theme
except ImportError:
pass
else:
custom_theme = default_theme.copy_with(
syntax=default_theme.syntax.copy_with(
prompt=ANSIColors.GREY,
builtin="\x1b[38;2;189;147;249m",
comment="\x1b[38;2;98;114;164m",
definition="\x1b[38;2;139;233;253m",
keyword="\x1b[38;2;255;121;198m",
keyword_constant="\x1b[38;2;255;121;198m",
soft_keyword="\x1b[38;2;255;121;198m",
number="\x1b[38;2;189;147;249m",
op="\x1b[38;2;249;152;204m",
string="\x1b[38;2;241;250;140m",
),
traceback=default_theme.traceback.copy_with(
error_highlight=ANSIColors.BOLD_YELLOW,
error_range=ANSIColors.YELLOW,
filename=ANSIColors.BACKGROUND_RED,
frame=ANSIColors.BACKGROUND_RED,
line_no=ANSIColors.BACKGROUND_RED,
message=ANSIColors.RED,
type=ANSIColors.BOLD_RED,
)
)
set_theme(custom_theme)
# Set a custom shell prompt
import platform
import sys
version = platform.python_version()
sys.ps1 = f"{version} \N{SNAKE} "
sys.ps2 = "." * len(sys.ps1)
每次启动新的交互式 Python REPL 会话时,此脚本都会运行。在本例中,它会检查_colorizePython 3.14 中引入的新模块是否可用。如果可用,它会使用该模块覆盖 Python 语法和回溯的默认颜色,模仿流行的Dracula 主题。此外,它还会自定义默认提示符以显示解释器的版本。
现在运行 Python 解释器时,结果将如下所示:
你已调整了 Python 语法和回溯的颜色,同时保留了前面提到的标准库模块的默认颜色。请注意,此 API 仍处于实验阶段,因此未来的 Python 版本可能会对其进行更改。请将其视为一个便捷但暂时的功能!
注意:如果你想完全禁用颜色,请在 shell 中设置以下环境变量之一:
NO_COLOR=1PYTHON_COLORS=0
任一变量都会关闭颜色输出,同时保留其他 REPL 功能(包括自动补全)。要恢复经典的 Python REPL,请设置该PYTHON_BASIC_REPL=1变量。
以前,REPL 可以自动补全变量和属性,但import语句则留在内存中。Python 3.14 现在扩展了Tab补全功能,可以识别 import and from … import 子句的上下文,并根据当前导入路径建议模块和包名称:
导入语句中模块和包的自动完成
键入import并按Tab两次以显示所有可用的顶级模块和包的列表。如果列表不适合你的屏幕,请继续按Tab以循环显示分页结果。
要缩小选项范围,请输入部分名称,例如dat。列表现在将仅包含诸如dataclasses和 之类的匹配项datetime。如果你输入data,则只有一个可能的匹配项,因此 Python 会立即为你自动完成。否则,请再次按下 来确认选择Tab。
当你输入import后跟一个不明确的前缀,然后点击 时Tab,你会看到一条警告,提示匹配项不唯一。Tab再次点击 可显示完整列表并选择正确的模块。
要探索内部或非公共模块,请键入import _并按Tab以显示以下划线开头的名称。
同样的逻辑也适用于from … import …语句。按下from后,Tab会列出候选模块或子包。按下import后,会列出子模块。不过,出于性能原因,REPL 不会自省模块的内容来建议对象名称,例如函数或类。
此增强功能对于探索性编程或学习新库特别方便,让你无需离开解释器或查阅文档即可发现可用的模块。
更多有用的错误消息
Python 3.14 继续改进错误信息,使其更加实用,尤其对初学者而言。它基于Python 3.9中引入的解析表达式语法以及后续版本中的持续改进.
此 Python 版本优化了代码未按预期运行时出现的许多消息,涵盖语法错误和运行时异常。其结果是错误消息更加清晰,可帮助你更快地查找和修复错误,从而提高调试效率。
Python 现在不再只是简单地标记出了问题,而是会精准指出根本原因,并经常提示解决方法,从而解决新手和老手最常犯的各种错误。通用的“语法无效”消息现在更加具体、更具描述性。
例如,当你在关键字中输入错误时,Python 3.14 通常可以猜测你的意思并建议正确的拼写:
>>> forr i in range(5):
File "<python-input-0>", line 1
forr i in range(5):
^^^^
SyntaxError: invalid syntax. Did you mean 'for'?
相比之下,早期版本的解释器不仅无法识别问题,而且还会误导你,让你注意变量而不是输入错误的关键字:
>>> forr i in range(5):
File "<python-input-0>", line 1
forr i in range(5):
^
SyntaxError: invalid syntax
Python 3.14 更进一步,识别并解释了各种常见错误,例如未终止的字符串文字、不兼容的字符串文字前缀等等:
>>> message = "She said "Hello" to everyone"
File "<python-input-0>", line 1
message = "She said "Hello" to everyone"
^^^^^
SyntaxError: invalid syntax. Is this intended to be part of the string?
>>> text = fb"Binary {text}"
File "<python-input-1>", line 1
text = fb"Binary {text}"
^^
SyntaxError: 'b' and 'f' prefixes are incompatible
>>> 1 if True else pass
File "<python-input-2>", line 1
1 if True else pass
^^^^
SyntaxError: expected expression after 'else', but statement is given
>>> if x > 0:
... print("positive")
... else:
... print("not positive")
... elif x == 0:
... print("zero")
...
File "<python-input-5>", line 5
elif x == 0:
^^^^
SyntaxError: 'elif' block follows an 'else' block
除了上述与语法相关的改进之外,Python 的一些内置异常也得到了改进。它们通过更清晰的消息和更具信息量的上下文,帮助你快速排除故障并解决运行时出现的问题:
>>> import math
>>> math.sqrt(-1)
Traceback (most recent call last):
File "<python-input-1>", line 1, in <module>
math.sqrt(-1)
~~~~~~~~~^^^^
ValueError: expected a nonnegative input, got -1.0
>>> left, right = ["apple", "banana", "orange"]
Traceback (most recent call last):
File "<python-input-2>", line 1, in <module>
left, right = ["apple", "banana", "orange"]
^^^^^^^^^^^
ValueError: too many values to unpack (expected 2, got 3)
>>> points = {[0, 0]: "origin"}
Traceback (most recent call last):
File "<python-input-3>", line 1, in <module>
points = {[0, 0]: "origin"}
^^^^^^^^^^^^^^^^^^
TypeError: cannot use 'list' as a dict key (unhashable type: 'list')
当你升级到 Python 3.14 并在实践中遇到这些类型的异常时,你将花费更少的时间来解读神秘的错误消息,而花费更多的时间来编写正确、可维护的代码。
更安全的实时进程调试
Python 3.14 采用了PEP 768,该规范标准化了一个安全稳定的接口,用于将外部调试器连接到CPython解释器。到目前为止,像pdb这样的工具、IDE以及像gdb这样的系统级调试器通常不得不依赖于私有钩子或CPython 实现细节的内部知识。这种方法虽然有效,但很脆弱。即使是微小的内部更改也可能破坏集成,导致崩溃或调试行为不正确。
安全的外部调试器接口建立在 CPython 公开的低级 C API 之上,但附带一个面向用户的选项,你今天就可以尝试:
$ python3.14 -m pdb -p <PID>
-p选项指示pdb连接到已运行的 Python 解释器,并提供其进程标识符 (PID) 。连接后,你可以检查变量、设置断点并逐步执行代码,就像你从头启动了pdb进程一样。
调试器会挂载到正在运行的进程的主线程中,并允许你在其上下文中运行任意 Python 代码片段。此技巧由一个新函数提供支持sys.remote_exec(),调试器会在后台调用该函数,以安全地执行目标解释器中的代码。
假设你正在运行一个最小的Web 服务器,该服务器使用浏览器的用户代理响应 HTTP 请求:
import os
import sys
from http.server import BaseHTTPRequestHandler, HTTPServer
HOSTNAME = "localhost"
PORT = 8000
classHandler(BaseHTTPRequestHandler):
defdo_GET(self):
user_agent = self.headers.get("User-Agent")
self.send_response(200)
self.end_headers()
self.wfile.write(f"Hello, {user_agent}".encode())
defmain():
print(f"Starting the server on: http://{HOSTNAME}:{PORT}")
print("Run this command as a superuser to attach the debugger:")
print(f"{sys.executable} -m pdb -p {os.getpid()}")
HTTPServer((HOSTNAME, PORT), Handler).serve_forever()
if __name__ == "__main__":
main()
使用以下命令在一个终端窗口中启动服务器:
$ python3.14 web_server.py
Starting the server on: http://localhost:8000
Run this command as a superuser to attach the debugger:
/home/realpython/.pyenv/versions/3.14.0/bin/python -m pdb -p 70262
输出显示服务器 URL 和将调试器附加到 Python 进程的命令,由其唯一的 PID 标识。
现在,从另一个终端以超级用户身份(例如,使用sudo -s以下命令)粘贴命令。或者,你也可以使用以下命令来限定命令,sudo而无需切换用户:
$ sudo /home/realpython/.pyenv/versions/3.14.0/bin/python -m pdb -p 70262
你需要提升权限,因为调试器可以访问另一个进程的内存。
进入后,在读取用户代理后的第 11 行放置一个断点,然后让服务器继续执行:
# /home/realpython/.pyenv/versions/3.14.0/bin/python -m pdb -p 70262
> /home/realpython/.pyenv/versions/3.14.0/lib/python3.14/selectors.py(398)select()
-> fd_event_list = self._selector.poll(timeout)
(Pdb) break web_server:11
Breakpoint 1 at /home/realpython/tutorials/python-314/web_server.py:11
(Pdb) continue
调试器遇到断点时会再次停止。因此,你现在需要打开浏览器并访问该页面以发送请求。或者,你也可以使用cURLhttp://localhost:8000或类似的命令行工具。
接下来,回到调试器。你将回到交互式提示符,在这里你可以打印(p)甚至覆盖服务器的局部变量
> /home/realpython/tutorials/python-314/web_server.py(11)do_GET()
-> self.send_response(200)
(Pdb) p user_agent
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (...)'
(Pdb) user_agent = "It's magic!"
(Pdb) continue
> /home/realpython/tutorials/python-314/web_server.p(11)do_GET()
-> self.send_response(200)
(Pdb)
当你在调试器中发出continue命令时,服务器将恢复,并使用新的用户代理值响应客户端。
要使用此新功能,目标进程和调试器都必须在 Python 3.14 或更高版本上运行。虽然它方便开发,但你可能不希望在生产环境中连接外部调试器。安全的禁用方法是在构建时禁用。你可以使用--without-remote-debug选项编译 CPython,该选项将完全删除 PEP 768 接口,这样任何外部调试器都无法连接。
如果你正在编写 Python 代码,则无需进行任何更改即可利用 PEP 768。这是一项底层改进,旨在使生态系统更加健康。但是,如果你围绕 Python 开发工具,那么你现在就拥有了一个专用的解释器钩子,而不必对其核心进行逆向工程。
新的 Python 语法
如果你喜欢保持代码简洁且富有表现力,此版本将为你带来一些惊喜。模板字符串(T 字符串)新增了一种更安全的字符串插值处理方式,无需使用不必要的括号即可处理多个异常,并且finally代码块现在遵循更严格的规则,从而减少了歧义。这些语法调整看似微不足道,但它们将帮助你编写更简洁、更可预测的代码。
模板字符串(T 字符串)
受JavaScript 的标记模板字面量启发,Python 的新模板字符串(昵称 t-string)可能是 3.14 版本中最引人注目的新增功能。从语法上讲,t-string 与f-string非常相似,共享相同的占位符语法、可选的格式说明符和转换标志。乍一看,唯一可见的区别是前缀,因为模板字符串字面量以t或开头T。
然而,与 f 字符串不同,t 字符串会计算出一个新string.templatelib.Template对象,而不是直接生成一个简单的对象str:
>>> f"This is a formatted string literal (f-string)"
'This is a formatted string literal (f-string)'
>>> t"This is a template string literal (t-string)"
Template(
strings=('This is a template string literal (t-string)',),
interpolations=()
)
这两种字面量形式看起来几乎一模一样,很容易混淆,尤其是当你期望输入的是 f 字符串,却不小心输入t了f,或者反过来的时候。这种细微的差别,如果不小心,可能会导致意想不到的 bug!
另一个令人惊讶的事实是名字的选择有点不太对劲。毕竟,PythonTemplate之前就已经自带了一个类,虽然位于不同的包中,但用途却有些相似:
string.Template | $variable |
string.templatelib.Template |
引入 T 字符串和新Template类的主要动机是希望保留 F 字符串的便捷性和可读性,同时使其更安全。最终,这两种字符串字面量类型都可以在其占位符字段内执行任意代码。例如,下面的代码片段读取用户输入,以填充以格式化字符串字面量表示的数据库查询:
>>> def find_users_query(name: str) -> str:
... """Return a SQL query to find users by name."""
... return f"SELECT * FROM users WHERE name = '{name}'"
...
>>> find_users_query("alice")
"SELECT * FROM users WHERE name = 'alice'"
>>> find_users_query(input("Enter the user name: "))
Enter the user name: ' OR '1'='1
"SELECT * FROM users WHERE name = '' OR '1'='1'"
虽然这是一个强大的功能,但如果滥用,可能会带来严重的安全漏洞。在这种情况下,将原始用户输入直接插入查询中会给SQL 注入带来风险。攻击者可以构造恶意输入来更改查询的预期逻辑,如突出显示的行所示。为了降低此风险,你应该使用预处理语句或对用户输入进行过滤,而不是直接将其格式化为 SQL 字符串。
这时 T 字符串就派上用场了。与 F 字符串不同,T 字符串会在值合并到模板之前进行拦截,从而增加一层额外的安全性。它们允许你验证或清理输入,以便你可以强制类型、转义或拒绝危险字符,或者将值转换为数据库驱动程序可以处理的安全参数占位符。
通过更改find_users_query()函数的定义,使其返回字符串模板而不是纯字符串,你可以延迟替换用户提供的值:
>>> from string.templatelib import Template
>>> def find_users_query(name: str) -> Template:
... """Return a SQL query to find users by name."""
... return t"SELECT * FROM users WHERE name = '{name}'"
...
>>> find_users_query("' OR '1'='1")
Template(
strings=("SELECT * FROM users WHERE name = '", "'"),
interpolations=(
Interpolation("' OR '1'='1", 'name', None, ''),
)
)
请注意,生成的模板对象会将T 字符串字面量中出现的每对花括号 ( {}) 保留为一个string.templatelib.Interpolation实例。该对象既包含任意数据类型的求值(例如"alice"或42),也包含相应的 Python 表达式(例如"name"或"input('Your age: ')" ) ,该表达式从占位符字段反编译为字符串表示形式。
注意: T 字符串字面量会像 F 字符串字面量一样,立即执行其占位符表达式的求值。这意味着在定义模板字符串字面量时,必须预先知道所有占位符。将 T 字符串包装在函数中可以延迟它们的求值。
好的,但是究竟如何将这样的模板对象转换为 SQL 查询字符串呢?从 Python 3.14 开始,你必须编写自己的模板处理器或在第三方库中查找。当前版本仅指定了 T 字符串文字的语法,以及辅助模块string.templatelib和两个类Template和Interpolation。
我们计划在未来的版本中将模板字符串使用者纳入标准库。其中最值得关注的计划记录在PEP 787中,该提案建议扩展subprocess和shlex模块以原生支持 T 字符串。
你可以将 T 字符串处理器实现为一个函数,该函数接受模板对象作为参数,应用一些转换,并生成安全对象str或其他结构化对象。例如,它可以是HTML 元素的抽象表示,也可以是参数化的 SQL 语句。
zip()将模板对象转换为字符串的最直接方法是使用、str.join()和生成器表达式将其固定文本段与插值交织在一起:
>>> defrender(template: Template) -> str:
... return"".join(
... f"{text}{value}"
... for text, value in zip(template.strings, template.values)
... )
...
>>> render(find_users_query("' OR '1'='1"))
"SELECT * FROM users WHERE name = '' OR '1'='1"
这段代码本质上等同于你之前的 f 字符串示例,但它也存在与之前相同的缺点。为了充分利用 t 字符串的真正威力,你可以利用Templateiterable这一特性:
>>> defsafer_render(template: Template) -> str:
... items = []
... for item in template:
... if isinstance(item, str):
... items.append(item)
... else:
... sanitized = str(item.value).replace("'", "''")
... items.append(sanitized)
... return"".join(items)
...
>>> safer_render(find_users_query("' OR '1'='1"))
"SELECT * FROM users WHERE name = ''' OR ''1''=''1'"
在底层,Template是一串交错的字符串和插值,你可以循环遍历它们。此实现使用了一种非常简单的过滤技术,即通过双写(' )来转义每个单引号( '')。因此,你的 SQL 注入尝试会失败,因为注入的文本仍然保留在带引号的字符串字面量中,被视为数据而不是可执行 SQL。
不过,这只是一种基本的保护形式。为了真正的 SQL 安全,你应该依赖数据库驱动程序的参数化查询,而不是手动修改字符串。此外,你不必总是将 T 字符串转换为纯字符串。这是一个更完整、更复杂的例子:
>>> from dataclasses import dataclass
>>> from string.templatelib import Interpolation, Template, convert
>>> from typing import Any
>>> @dataclass(frozen=True)
... classSQLQuery:
... statement: str
... params: list[Any]
...
... def__init__(self, template: Template) -> None:
... items, params = [], []
... for item in template:
... match item:
... case str():
... items.append(item)
... case Interpolation(value, _, conversion, format_spec):
... converted = convert(value, conversion)
... if format_spec:
... converted = format(converted, format_spec)
... params.append(converted)
... items.append("?")
... super().__setattr__("statement", "".join(items))
... super().__setattr__("params", params)
...
你定义一个不可变的数据类,它表示一个参数化的 SQL 查询。它的构造函数接受一个字符串模板作为参数。然后,它使用for循环遍历该模板,并使用模式匹配对固定字符串值和插值进行不同的处理。还要注意如何调用convert()和format()来手动应用格式,这与在后台执行此操作的 f 字符串不同。
下面介绍如何将新数据类合并到查询构建函数中:
>>> deffind_users(name: str) -> SQLQuery:
... """Return a SQL query to find users by name."""
... return SQLQuery(t"SELECT * FROM users WHERE name = {name}")
...
>>> find_users("' OR '1'='1")
SQLQuery(
statement='SELECT * FROM users WHpERE name = ?',
params=["' OR '1'='1"]
)
当你SQLQuery通过Python 数据库 API运行此代码时,相应的驱动程序会尝试将参数绑定到占位符。由于恶意字符串被严格视为文字值,而非可执行的 SQL,因此注入尝试会被安全地阻止。
https://realpython.com/account/join/)
没有括号的异常
Python 3.14 采用了PEP 758,这是对异常处理语法的一项虽小但值得欢迎的调整。在此之前,当捕获多个异常时,必须将它们括在括号中:
try:
int("one")
except (ValueError, TypeError):
print("Something went wrong")
否则,你最终会遇到语法错误:
>>> try:
... int("one")
... except ValueError, TypeError:
... print("Something went wrong")
...
File "<python-input-0>", line 3
except ValueError, TypeError:
^^^^^^^^^^^^^^^^^^^^^
SyntaxError: multiple exception types must be parenthesized
从 Python 3.14 开始,如果不使用as关键字,括号将变为可选。这意味着你现在可以编写以下内容:
>>> try:
... int("one")
... except ValueError, TypeError:
... print("Something went wrong")
...
Something went wrong
新语法也适用于except*用于捕获异常组的子句。实际上,此更改使异常处理与 Python 中其他逗号分隔的结构(例如元组文字)更加一致,因为这些结构中括号并非严格必需。当异常列表变长时,它还可以减少视觉混乱。
如果要将捕获的异常绑定到变量as,则为了清楚起见,括号仍然是必需的:
try:
int("one")
except (ValueError, TypeError) as e:
print("Error:", e)
这避免了对变量具体赋值的歧义。除了语法糖之外,行为没有任何变化。解释器仍然以相同的方式处理异常。
虽然只是细微的调整,但这项调整简化了 Python 程序中一个非常常见的模式,在不破坏向后兼容性的情况下,使代码更具可读性。如果你一直在使用except (ErrorA, ErrorB),可以继续使用,但现在你还可以选择在觉得不需要时删除括号。
try…finally块中的警告
Python 的try…finally代码块旨在保证无论try代码块以何种方式退出(无论是正常结束、引发 异常还是遇到return语句),清理代码都会运行。然而,长期以来,在finally代码块内部使用某些控制流结构一直被认为是有问题的。
如果你在finally代码块中使用return、break或continue,那么你可能会默默地覆盖一个活动的异常,或者跳过重要的清理步骤,从而导致令人困惑和意想不到的行为。考虑一下这个设计的例子:
Python`Python ≤ 3.13
>>> defrisky_operation():
... try:
... raise ValueError("Something went wrong!")
... finally:
... return"Suppressed"
...
>>> risky_operation()
'Suppressed'
在 Python 3.13 及更早版本中,此函数会ValueError完全吞掉 并返回字符串"Suppressed"。调用者永远不会看到异常,这使得调试极其困难。在代码块中使用break或时也会出现类似的效果。所有这些关键字都可以覆盖控制流,从而隐藏触发代码块的原始原因。continue``finally
多年来,这种怪癖一直是 Python 语言中众所周知的“脚枪”。Python 核心团队一度考虑彻底禁止它。但PEP 765最终达成了妥协。从 Python 3.14 开始,为了向后兼容,这种用法仍然是合法的,但解释器现在会发出一个警告,SyntaxWarning以明确指出风险:
>>> defrisky_operation():
... try:
... raise ValueError("Something went wrong!")
... finally:
... return"Suppressed"
...
<python-input-0>:5: SyntaxWarning: 'return'in a 'finally' block
>>> risky_operation()
'Suppressed'
此警告提示你,finally块内的控制流可能存在危险。代码仍然像以前一样运行,但现在你会清楚地知道发生了一些奇怪的事情。
这同样适用于break和continue
>>> whileTrue:
... try:
... raise RuntimeError
... finally:
... break
...
<python-input-2>:5: SyntaxWarning: 'break'in a 'finally' block
你可以将这些警告视为重新审视设计的机会。通常,修复方法很简单:将控制流语句移到finally之外,或者重构逻辑,使finally代码块仅执行清理操作,而不执行流控制。例如,如果你需要返回一个值,请在try代码块中计算该值,然后让finally运行:
>>> defsafer_function():
... try:
... result = "Suppressed"
... raise ValueError("Something went wrong!")
... finally:
... print("Cleaning up...")
... return result
...
>>> safer_function()
Cleaning up...
Traceback (most recent call last):
...
ValueError: Something went wrong!
此时,清理仍然会发生,但异常不再丢失。
由于此更改仅限于SyntaxWarning,现有代码库在 Python 3.14 下运行时不会突然失败。但有了这些警告,你可以立即洞察任何可疑模式。这让你有时间审核和调整代码,以免未来版本将警告升级为错误。
简而言之,Python 3.14 解决了该语言中一个长期存在的极端情况。通过警告代码块中的return、break和continue语句finally,它可以引导你走向更安全、更可预测的模式,而无需在一夜之间切断现有代码。
类型检查革命
本节重点介绍 Python 3.14 中类型系统的最大改进,这些改进建立在多年的渐进式改进之上。为了更好地理解这些变化,我们将简要回顾一下 Python 类型系统的发展历史。如果你已经熟悉这些背景知识,可以直接跳到Python 3.14 中关于注解延迟求值的讨论。
打字历史快速浏览
在 Python 3.14 的所有增强功能中,最具颠覆性的莫过于解释器对注解的全新评估方式。如果你依赖类型提示进行静态类型检查,或者使用在运行时动态处理注解的库,那么这项改进将使你的代码更简洁、更不易出错,甚至可能运行速度更快。这代表了 Python 类型提示生态系统多年来最大的一次飞跃。
Python 类型系统最受期待的更新之一是引入了注解的惰性求值功能,旨在解决类型提示场景中长期存在的问题。然而,事实证明,正确实现这一新功能比预期的要困难得多。
传统上,Python 在定义时会评估类型注释,这与它在函数签名中处理默认参数值的方式非常相似。然而,这种急切的评估有时会导致问题,例如启动性能下降、引用循环以及循环导入错误。
考虑以下示例,你想要定义一个由节点链组成的链表数据结构:
from dataclasses import dataclass
from typing import Any, Optional
@dataclass
classLinkedList:
head: Node
@dataclass
classNode:
value: Any
next: Optional[Node] = None
在 Python 3.13 或更早版本中运行此代码会引发一个错误,NameError因为解释器无法识别Node,你在第 6 行使用它来注释.head属性:
$ python3.13 linked_list.py
Traceback (most recent call last):
...
head: Node
^^^^
NameError: name 'Node' is not defined. Did you mean: 'None'?
此时,该类Node尚未定义。重新排序类定义有时会有所帮助,但在这种情况下,你只会将问题从一个地方转移到另一个地方。请注意,它Node也通过属性引用自身.next,这会导致类似的情况,因为自引用类尚未完全定义。
为了避免这种情况,你需要一种方法来阻止 Python 在遇到注释时立即对其进行评估。
延迟求值注解的概念最早出现在PEP 563中。该特性在Python 3.7中作为可选__future__import引入,它将覆盖每个模块的解释器默认行为:
>>> variable1: print("This code runs immediately.")
This code runs immediately.
>>> from __future__ import annotations
>>> variable2: print("This code becomes a string.")
>>> __annotations__
{
'variable1': None,
'variable2': "print('This code becomes a string.')"
}
默认情况下,以前的 Python 版本会立即运行注释的代码。由于调用print()会以更新标准输出流的形式产生可见的副作用,因此你可以立即看到屏幕上显示的消息。这表明 Python 在后台执行了注释,即使它并不特别关心它们的计算值。
注意:虽然注解通常用于描述数据类型,但它们也可以是任何有效的 Python 表达式,只要它们符合 Python 语法即可。现在你知道为什么了吧!
相比之下,一旦启用了前面提到的 Future 指令,后续代码的处理方式就会有所不同。Python 不再评估注解,而是将这项工作交给了你。相反,它会通过一些低级的魔法,将你的注解保留为纯字符串,就像你手动将每个注解用引号括起来形成字符串文字一样。
检查特殊__annotations__字典可以发现两个模块级变量注释:
variable1带有注释,None由...返回print()。variable2用字符串化的 Python 表达式进行注释。
无论注释是字符串还是其他 Python 对象,对于执行静态代码分析的类型检查器(例如 mypy 或ty)来说都没有区别。但是,在运行时检查注释的库(例如,用于依赖项注入或数据验证)可能需要自行解析这些字符串化的注释以恢复原始值。
将注释存储为字符串可以避免未定义的引用错误,并避免在导入时评估繁重表达式的额外开销。由于这些好处,我们计划在Python 3.10及更高版本的3.11中将字符串化类型提示设为默认设置,但像FastAPI和Pydantic这样的在运行时处理注释的库发现它们很脆弱且难以评估:
面对这些现实障碍,核心团队寻求更稳健的设计,并最终采用了PEP 649。Python 3.14 不再将注释转换为字符串(这可能需要手动求值),而是将它们表示为数据描述符,仅在需要时进行求值,然后进行缓存。这种方法既避免了急切执行,也避免了字符串化的陷阱。
注释的延迟评估
正如 PEP 649 的标题“注解的延迟求值”所暗示的那样,Python 3.14 仍然会为你求值注解,但前提是你明确请求它们。或许可以用一个在求值时导致运行时错误的注解来最好地说明这一点
>>> deffunction() -> 1 / 0:
... print("Python doesn't run annotations just yet.")
...
>>> function()
Python doesn't run annotations just yet.
>>> function.__annotations__
Traceback (most recent call last):
...
def function() -> 1 / 0:
~~^~~
ZeroDivisionError: division by zero
箭头符号 ( ->)后指定的返回类型包含一个通常会触发 的表达式ZeroDivisionError。但是,由于 Python 3.14 在你显式检查类型注释之前不会对其进行求值,因此你仍然可以毫无问题地调用此函数。只有当你访问函数的.__annotations__属性时,才会观察到预期的异常。
话虽如此,Python 仍然会解析你的注解,检查它们的语法是否正确。如果语法不正确,你会收到一个温和的警告或一个完整的语法错误:
>>> def syntax_warning() -> b"\N":
... print("Python parses your annotations.")
...
<python-input-2>:1: SyntaxWarning: "\N" is an invalid escape sequence.
⮑ Such sequences will not work in the future.
⮑ Did you mean "\\N"? A raw string is also an option.
>>> def syntax_error() -> "\N{unknown}":
File "<python-input-0>", line 1
def syntax_error() -> "\N{unknown}":
^^^^^^^^^^^^^
SyntaxError: (unicode error) 'unicodeescape' codec can't decode bytes
⮑ in position 0-10: unknown Unicode character name
字节字面量 b"\N"包含无效的转义序列,Python 当前会为你进行转义。另一方面,下一个函数中的字符串字面量包含未知的Unicode字符名称,导致语法错误。这两个示例都表明 Python 仍然能够解析表示注释的表达式。
值得指出的是,Python 在评估你的注解后,会将计算值放入缓存中,以避免重复执行可能耗时的计算。当你使用斐波那契数列的递归实现时,这种效果尤其明显,因为众所周知,递归实现的效率非常低:
>>> deffib(n):
... print(f"Calculating fib({n})...")
... return n if n < 2else fib(n - 2) + fib(n - 1)
...
>>> deffunction() -> fib(5):
... pass
...
>>> function.__annotations__
Calculating fib(5)...
Calculating fib(3)...
Calculating fib(1)...
Calculating fib(2)...
Calculating fib(0)...
Calculating fib(1)...
Calculating fib(4)...
Calculating fib(2)...
Calculating fib(0)...
Calculating fib(1)...
Calculating fib(3)...
Calculating fib(1)...
Calculating fib(2)...
Calculating fib(0)...
Calculating fib(1)...
{'return': 5}
>>> function.__annotations__
{'return': 5}
>>> function.__annotations__
{'return': 5}
首次访问函数的.__annotations__属性时,Python 会调用fib(5),这会触发一连串的递归调用,直到递归开始展开。从上面的输出中可以看到,有许多使用fib()相同参数的 的冗余调用。幸运的是,这种情况只发生一次,后续读取注释时会返回缓存的结果。
无论如何,你应该避免.__annotations__直接使用属性,因为它是内部接口中一个低级且不太稳定的部分。根据你的目标,Python 3.14 提供了更好的工具来获取各种格式的注释:
annotationlib.get_annotations():多功能注释自省工具inspect.get_annotations():为了向后兼容,前者的功能稍弱一些的别名typing.get_type_hints():专门的运行时类型自省工具
第一个实用函数为读取注释提供了最大的灵活性。它可以返回字符串化的注释、前向引用或求值:
>>> from annotationlib import Format, get_annotations
>>> from dataclasses import dataclass
>>> from typing import Any, Optional
>>> @dataclass
... class LinkedList:
... head: Node
...
>>> @dataclass
... class Node:
... value: Any
... next: Optional[Node] = None
...
>>> get_annotations(LinkedList, format=Format.STRING)
{'head': 'Node'}
>>> get_annotations(LinkedList, format=Format.FORWARDREF)
{'head': ForwardRef('Node', is_class=True, owner=<class '__main__.LinkedList'>)}
>>> get_annotations(LinkedList, format=Format.VALUE)
Traceback (most recent call last):
...
NameError: name 'Node' is not defined. Did you mean: 'None'?
请注意,你之前的链表示例如何在 Python 3.14 中完美运行,无需将声明的类型包装在字符串文字中。
另一方面,当你的注释严格代表类型时,你将从最后一个函数中获得更多,它可以处理令人惊讶的边缘情况:
>>> from typing import Annotated, get_type_hints
>>> def function() -> Annotated["int", "Metadata"]:
... pass
...
>>> get_type_hints(function)
{'return': <class 'int'>}
>>> get_type_hints(function, include_extras=True)
{'return': typing.Annotated[int, 'Metadata']}
>>> get_annotations(function)
{'return': typing.Annotated[ForwardRef('int', is_class=True), 'Metadata']}
本节描述的行为最终在 Python 3.14 中成为默认行为,实现了承诺的优势,并消除了之前导致变更停滞多年的缺陷。简而言之,Python 3.14 中的惰性注解:
推迟评估直到访问注释,从而提高启动性能。 通过前向引用和循环导入消除常见的痛点。 通过标准库中的新实用函数提供一致的自省。 保持向后兼容性,同时为弃用旧的未来指令铺平道路。
这项改进带来了巨大的回报。类型提示更快、更安全、更易于使用,而且不会破坏现有代码。
性能优化
从本质上讲,此版本专注于让 Python 更加精简,响应速度更快。子解释器和自由线程构建等新选项推动了并行执行,同时垃圾收集器和解释器内部也进行了调整,以减少暂停和开销。这些优化不会改变你编写 Python 的方式,但它们可以让你的程序运行更流畅,扩展更高效。
并行子解释器
如果你曾尝试使用线程加速Python 中CPU 密集型代码,那么你可能已经注意到全局解释器锁 (GIL)会造成阻碍。一次只有一个线程可以运行 Python 字节码,因此额外的线程并不能带来真正的并行性。传统的解决方法是multiprocessing。但是,虽然进程并行运行,但它们的启动和数据交换成本很高。
Python 3.14 引入了第三个选项,该选项之前只能通过 C API 使用。子解释器终于可以在纯 Python 中使用了,你不再需要降级到C 语言。
每个子解释器都有自己的 GIL 和隔离状态,这意味着多个解释器可以在不同的 CPU 核心上真正并行地运行 Python 代码。它们比进程更轻量,但仍然比线程更安全,因为解释器默认不共享全局对象。
为了使子解释器易于使用,标准库现在提供了一个InterpreterPoolExecutorin 函数。它的工作方式与ThreadPoolExecutor或ProcessPoolExecutor函数concurrent.futures类似。你提交可调用函数,然后返回结果。在底层,每个工作器都在自己的解释器中运行,因此你的CPU 密集型任务可以跨核心扩展。
下面是一个比较三种执行器类型的简单基准:
from concurrent.futures import (
InterpreterPoolExecutor,
ProcessPoolExecutor,
ThreadPoolExecutor,
)
from time import perf_counter
MAX_VALUE = 35
NUM_VALUES = 4
NUM_WORKERS = 4
deffib(n):
return n if n < 2else fib(n - 2) + fib(n - 1)
defcompute(Pool):
t1 = perf_counter()
with Pool(max_workers=NUM_WORKERS) as pool:
list(pool.map(fib, [MAX_VALUE] * NUM_VALUES))
t2 = perf_counter()
print(f"{Pool.__name__}: {(t2 - t1):.2f}s")
if __name__ == "__main__":
compute(InterpreterPoolExecutor)
compute(ProcessPoolExecutor)
compute(ThreadPoolExecutor)
使用InterpreterPoolExecutor,每个fib()调用都在单独的 Python 解释器中运行,因此它们可以独立运行,而无需争夺一个 GIL。由于解释器不共享活动对象,因此结果和参数必须是可 pickle 的。与进程相比,子解释器启动速度更快,占用内存更少,但你仍然无法在它们之间传递诸如打开文件句柄之类的信息。
这个新工具完美地介于线程和进程之间:
线程:最适合 I/O 密集型工作,但对于常规 CPython 中的 CPU 密集型任务有限制。 进程:最大程度的隔离和兼容性,但开销较高。 解释器:允许真正的并行,其开销比进程低,但隔离性比线程更严格。
简而言之,Python 3.14 中的子解释器为你提供了一种轻量级的方法,可以在不增加进程成本的情况下跨核心扩展 CPU 密集型任务。它们并非灵丹妙药,但它们为 Python 中的并行编程开辟了一个强大的全新中间地带。
注意:并非所有扩展模块都支持多解释器。有些扩展模块ImportError如果假设全局状态,可能会引发问题。
自由线程 Python
全局解释器锁 (GIL)确保同一时刻只有一个线程可以执行 Python字节码。虽然这种设计几十年来简化了CPython 的内存管理,但它也一直是CPU 密集型多线程应用程序的根本瓶颈。克服这一限制一直是 Python 社区的主要目标,而 Python 3.14 在这方面迈出了重要的一步。
在 Python 3.13 中,核心团队发布了一个实验性的自由线程版本,这是一个 CPython 版本,可以通过禁用 GIL 真正并行运行多个线程。这个预览版允许富有冒险精神的开发者在多核机器上测试真正的多线程加速,但也有一些严重的警告。它尚未获得官方支持,性能特性仍在不断发展。
在 Python 3.14 中,自由线程 Python 已达到受支持状态。这意味着该项目已满足PEP 779中列出的标准,并向像你这样的用户保证,该功能将在未来版本中得到维护和改进。它不再是一个实验性的侧分支,而是 CPython 路线图的正式组成部分。
注意:虽然 GIL 是可选的,但在 Python 3.14 中仍然默认启用。要关闭它,你需要先使用该标志编译 Python 源代码。这将生成一个无 GIL 的解释器,允许多个原生线程同时运行 Python 代码。或者,你可以使用pyenv--disable-gil简化自由线程版本的构建。
对于需要临时回退或兼容性的项目,自由线程构建仍然可以在运行时重新启用 GIL。这种灵活性有助于顺利过渡尚未准备好适应无 GIL 环境的代码库和依赖项。
它的主要吸引力在于真正的并行性,因此之前受限于单核执行的 CPU 密集型程序现在可以受益于同时运行的多线程。早期的基准测试表明,它对于数据处理或数值模拟等工作负载具有令人印象深刻的扩展性。然而,也存在一些弊端:
单线程性能下降:不使用 GIL 运行时会引入额外的锁定和内存管理开销。核心团队目前的目标是将单线程代码的性能损失控制在 10% 到 15% 之间,并将内存使用量控制在 20% 左右。 生态系统兼容性:许多第三方C 扩展模块假定 GIL 存在,需要更新或重新编译才能正常工作。新版本使用单独的ABI(应用程序二进制接口)来区分不依赖 GIL 的扩展,因此某些编译后的软件包可能无法直接使用。
自由线程 Python 是 CPython 历史上最重要的转变之一。它最终为纯 Python 代码打开了实现真正多核性能的大门,减少了繁琐的变通方法或将繁重计算转移到外部库的需要。虽然更广泛的 Python 生态系统需要时间才能完全接受它,但 Python 3.14 为未来摆脱 GIL 的束缚奠定了稳定的基础。
实验性 JIT 构建
上一版本中引入的即时编译器 ( JIT ) 在 Python 3.14 中仍处于实验阶段。JIT 可以在运行时将 Python字节码中频繁执行的部分转换为本机机器指令,从而通过减少重复解释的开销来加速执行。虽然这个想法在PyPy等项目中已被证明是有效的,但 CPython 的 JIT 仍处于起步阶段,尚未准备好投入生产使用。
此版本的关键变化在于更广泛的发行版支持。在 Windows 和 macOS 上,官方二进制安装程序现在默认包含启用 JIT 的构建版本,但除非你明确激活它(例如,通过设置PYTHON_JIT=1环境变量),否则 JIT 仍处于关闭状态。在 Linux 上,你仍然需要使用--enable-experimental-jit配置标志自行编译 Python。
如果你使用pyenv,启用 JIT 就像在安装之前设置适当的环境变量一样简单:
$ export PYTHON_CONFIGURE_OPTS='--enable-experimental-jit'
$ pyenv install 3.14.0
安装后,Python 会通过新sys._jit模块为你提供内省 API,让你可以在运行时检查 JIT 的可用性和状态:
Pythonjit_status.py
try:
from sys import _jit
except ImportError:
print("Module sys._jit unavailable")
else:
print("Python compiled with JIT support:", _jit.is_available())
print("JIT enabled for this process:", _jit.is_enabled())
print("JIT currently running:", _jit.is_active())
请注意模块名称中的前导下划线,这暗示它是一个内部接口,在未来的版本中可能会发生变化甚至消失。这个小型API主要用于诊断和实验
$ python3.13 main.py
Module sys._jit unavailable
$ python3.14 main.py
Python compiled with the JIT support: False
JIT enabled for this process: False
JIT currently running: False
$ python3.14-jit main.py
Python compiled with the JIT support: True
JIT enabled for this process: True
JIT currently running: False
$ PYTHON_JIT=0 python3.14-jit main.py
Python compiled with the JIT support: True
JIT enabled for this process: False
JIT currently running: False
目前,JIT 预计不会带来持续的性能提升。事实上,根据你的工作负载,它甚至可能会降低运行速度。Python 核心团队建议将其视为一个实验场所,供那些想要探索 Python 性能未来发展方向的好奇开发者使用。
如果你对低级解释器机制感兴趣,JIT 是一个值得关注的迷人领域,但对于生产代码,你现在应该坚持使用标准解释器。
尾调用解释器
Python 3.14 引入了一种基于C 语言级尾调用构建的替代解释器模式。CPython 不再使用单片调度循环,而是将操作码处理程序编译为小函数,这些小函数会尾调用下一个处理程序。此尾调用解释器默认未启用。你必须使用--with-tail-call-interp配置标志构建 CPython 才能使用它。
如果你的编译器支持保证尾调用(目前是 x86-64 和 AArch64 上的 Clang 19+),则此设计可以减少调度开销,并在许多工作负载下略微提升性能。早期基准测试声称与旧版本相比性能提升了 9-15%,但后续分析表明,考虑到已知的编译器错误,实际提升幅度为 3-5%。
重要的是,这完全是 CPython 内部的。它不会改变Python 的递归语义,也不会提供用户级尾递归消除功能。你现有的递归代码仍然会像以前一样达到递归限制。此外,某些构建组合(例如混合--with-tail-call-interp和--enable-pystats)目前会导致构建失败。
简而言之,Python 3.14 中的尾调用解释器是一个低级性能实验。如果你的环境支持它,你可能会看到略微的加速,但你的 Python 代码和算法无需更改,递归也保持不变。
增量式垃圾收集器
Python 具有自动内存管理功能,可以防止程序内存耗尽。此机制依赖于引用计数,它可以快速回收大多数对象。此外,Python 使用垃圾回收机制来处理诸如引用循环之类的棘手情况。例如,当两个对象相互引用时,即使没有变量指向它们,它们的引用计数仍然保持在零以上,就会形成引用循环。
到目前为止,当循环检测器启动时,它会尝试一次性完成所有清理工作,如果堆积了大量对象,可能会导致程序短暂卡顿。Python 3.14 改变了这一现状。它不再一次性清除所有对象,而是将工作分散到多个步骤或增量中。这意味着即使在高负载下,程序的停顿时间也更短,几乎难以察觉。
在 Python 3.13 中,类似的功能曾短暂发布,但由于导致意外的性能下降,在发布周期后期被回滚。不过,这个想法并没有被放弃。它经过改进、进一步测试,最终在 Python 3.14 中发布。这意味着你现在可以获得最初计划的更流畅的体验,而不会遇到之前的缺点。
这项更改基本上是显而易见的。Python 现在会自动使用增量收集。对于大多数开发者来说,这项改进不会显眼,但最终应该会带来更流畅的运行时体验。在服务器、游戏或交互式应用程序等对延迟敏感的代码中,你会尤其感激这一点。
那么,你应该升级到 Python 3.14 吗?
如果你已经在使用 Python 3.13,那么升级到 3.14 将会非常容易。全新的 REPL 功能、更清晰的错误信息以及更安全的调试工具,让日常开发更加顺畅。此外,子解释器、自由线程构建以及改进的垃圾收集器带来的性能提升,为构建速度更快、可扩展性更强的应用程序奠定了基础。
话虽如此,你不必着急。虽然大多数更改都是向后兼容的,并且你现有的代码无需修改即可运行,但第三方库可能需要时间来跟上新功能。如果你的项目严重依赖于C 扩展、编译轮或其他低级集成,那么你可能需要等到你的依赖项正式支持 Python 3.14 后再进行更改。
对于生产环境,尤其是在企业或遗留系统中,稳定性通常比耀眼的新功能更重要。大型组织通常倾向于等待第一个维护版本(例如 3.14.1)甚至长期支持 (LTS)发行版发布后,再对服务器进行升级。这有助于避免极端情况或生态系统不兼容带来的意外。
另一方面,如果你正在构建新项目、尝试并发,或者在以开发人员生产力为首要任务的环境中工作,那么尽早升级是明智之举。你将立即受益于更友好的 REPL、更智能的错误消息和更安全的调试工具,同时让你的代码库能够充分利用 Python 不断发展的并行性。
简而言之,如果你想要最新的改进,并且对你的依赖堆栈有信心,请立即升级。否则,如果你优先考虑在保守的生产环境中保持长期稳定性,那么请等到生态系统完全接受这些变化。
结论
每次 Python 的新版本发布,都是一次回顾语言发展历程的机会,同时也是对无数贡献者不断推动其进步的感谢。Python 3.14 是一个在完善与进步之间取得平衡的版本,它不仅在完善的基础上不断进步,也为未来埋下了伏笔。