2025-10-12
Python 3.14 带来的变化很有意思:它不是只加一点语法糖,而是在“字符串处理、类型注解、并发模型”三条线上同时推进。`t-string` 让模板字符串变成可处理的结构化对象,注解延迟让类型系统更舒服,多解释器则把“同进程里的隔离并行”摆到了标准库台面上。
如果用一句话概括:
t-string 适合做安全渲染、SQL 参数化、日志结构化、轻量 DSL。它们有一个共同点:把以前“直接揉成结果”的东西拆成结构。字符串不急着拼,注解不急着算,任务也不急着共享同一份状态。
f-string 会立刻得到 str:
name = "Zoy"
msg = f"hello {name}"
print(type(msg)) # <class 'str'>
t-string 则返回 string.templatelib.Template:
name = "Zoy"
tpl = t"hello {name}"
print(type(tpl))
print(tpl.strings)
print(tpl.values)
print(tpl.interpolations)
可以把它理解成:Python 帮你把“静态文本”和“动态插值”先拆开,至于最后怎么合成,由你自己决定。
Template 主要有三个常用属性:
strings:静态字符串片段。interpolations:插值对象,包含值、表达式文本、转换标记、格式规格。values:所有插值的值。示例:
price = 19.8
tpl = t"price={price:.2f}"
print(tpl.strings)
print(tpl.values)
for item in tpl:
print(repr(item))
它和 f-string 最大的区别在于:f-string 只给你结果,t-string 给你“可处理的中间结构”。这就很适合做安全输出。
下面这个例子把静态文本原样保留,插值部分统一转义:
from html import escape
from string.templatelib import Interpolation, Template
def html_safe(template: Template) -> str:
parts: list[str] = []
for part in template:
if isinstance(part, Interpolation):
parts.append(escape(str(part.value), quote=True))
else:
parts.append(part)
return "".join(parts)
username = '<img src=x onerror=alert(1)>'
page = html_safe(t"<p>Hello, {username}</p>")
print(page)
输出会把用户输入当作动态数据处理,而不是直接塞进 HTML。这里的优势是:调用方仍然能写出自然的字符串模板,渲染方可以集中处理安全策略。
不要把动态值直接拼进 SQL。t-string 可以帮我们把 SQL 文本和参数值拆开:
from string.templatelib import Interpolation, Template
def sql_params(template: Template) -> tuple[str, list[object]]:
sql_parts: list[str] = []
params: list[object] = []
for part in template:
if isinstance(part, Interpolation):
sql_parts.append("?")
params.append(part.value)
else:
sql_parts.append(part)
return "".join(sql_parts), params
user_id = 1001
status = "active"
sql, params = sql_params(
t"select * from users where id={user_id} and status={status}"
)
print(sql)
print(params)
结果类似:
select * from users where id=? and status=?
[1001, 'active']
这个模式很舒服:业务代码保持可读,底层处理函数负责把动态值交给数据库驱动。
不建议把 t-string 当成“更酷的 f-string”。如果只是拼一句日志,f-string 仍然直接。t-string 的价值在于你要拦截、检查、转义、参数化。
以前写类型注解时,经常会遇到前向引用问题。比如一个函数参数类型在后面才定义,老写法可能需要引号:
def handle(user: "User") -> None:
print(user.name)
class User:
def __init__(self, name: str) -> None:
self.name = name
在 Python 3.14 的默认语义下,注解不会在定义处马上求值,而是在真正访问时再处理。这样很多前向引用可以自然书写:
def handle(user: User) -> None:
print(user.name)
class User:
def __init__(self, name: str) -> None:
self.name = name
这对框架和大型项目尤其友好:模型之间互相引用时,不必为了导入顺序写一堆字符串注解。
Python 3.14 提供了 annotationlib,可以用不同格式读取注解。
from annotationlib import Format, get_annotations
def load_plugin(plugin: Plugin) -> Result:
...
print(get_annotations(load_plugin, format=Format.STRING))
print(get_annotations(load_plugin, format=Format.FORWARDREF))
class Plugin:
pass
class Result:
pass
print(get_annotations(load_plugin, format=Format.VALUE))
三个格式的使用倾向:
Format.STRING:适合文档生成器、代码扫描器,只想展示注解文本。Format.FORWARDREF:适合框架扫描,允许暂时未解析的引用存在。Format.VALUE:适合最终运行阶段,需要拿到真实对象。注意:读取注解本身可能触发代码执行,STRING 和 FORWARDREF 也不是沙箱。它们适合减少“必须拿到真实对象”的需求,但不要对不可信模块随意做注解 introspection。
假设我们写一个路由扫描器,只想拿到函数参数类型用于生成接口文档:
from annotationlib import Format, get_annotations
from collections.abc import Callable
def describe_endpoint(func: Callable) -> dict[str, str]:
annotations = get_annotations(func, format=Format.STRING)
return {
name: str(annotation)
for name, annotation in annotations.items()
}
def create_order(payload: CreateOrderRequest) -> OrderResponse:
...
print(describe_endpoint(create_order))
这个扫描器不需要真的导入 CreateOrderRequest 或 OrderResponse,也不需要提前解决所有依赖。对框架来说,这种“先读结构,后做解析”的能力很实用。
from __future__ import annotations 的关系如果模块里仍然写了:
from __future__ import annotations
注解会继续以字符串化语义运行。迁移时不要急着全删,可以按模块逐步调整。比较稳的顺序是:
__annotations__ 调整为 annotationlib 或 typing.get_type_hints。多解释器可以理解成:同一个进程里跑多个相互隔离的 Python 解释器。它们不是普通线程,因为每个解释器有自己的运行状态;它们也不是子进程,因为仍然在同一个进程里。
这带来一个很香的点:多个解释器可以在不同 CPU 核心上并行执行 Python 代码。代价是:对象不能随便共享,通信要显式做。
标准库提供了 concurrent.interpreters:
from concurrent import interpreters
interp = interpreters.create()
interp.exec("print('hello from another interpreter')")
result = interp.call(len, [1, 2, 3])
print(result)
interp.close()
这里的 interp.exec() 会在另一个解释器中执行源码,interp.call() 会把可调用对象和参数传过去再取回结果。
如果你更喜欢 concurrent.futures 的风格,可以用 InterpreterPoolExecutor:
from concurrent.futures import InterpreterPoolExecutor
def count_prime(limit: int) -> int:
total = 0
for n in range(2, limit):
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
break
else:
total += 1
return total
limits = [30_000, 32_000, 34_000, 36_000]
with InterpreterPoolExecutor(max_workers=4) as pool:
results = list(pool.map(count_prime, limits))
print(results)
这个接口看起来像线程池,但每个 worker 在自己的解释器里跑任务。对于纯 Python 的 CPU 计算,它比普通线程更有想象空间。
多解释器的隔离感很强。一个解释器里导入的模块、修改的全局变量、打开的状态,不会自动同步给另一个解释器。大多数可变对象也不能直接共享。
这不是缺点,而是设计选择:它逼你把并发关系写清楚。
from concurrent import interpreters
queue = interpreters.create_queue()
replies = interpreters.create_queue()
worker = interpreters.create()
worker.prepare_main(queue=queue, replies=replies)
queue.put("job-001")
worker.exec("""
item = queue.get()
replies.put(f"done: {item}")
""")
print(replies.get())
worker.close()
实践中可以把它当成“进程内 actor”:每个解释器负责自己的状态,外部只通过消息传递交换数据。
Template 转成字符串后再处理,那会丢掉结构优势。Template,避免调用方传普通字符串。__annotations__,优先使用标准库提供的读取工具。Format.STRING,框架扫描优先考虑 Format.FORWARDREF。Format.VALUE 或 typing.get_type_hints。InterpreterPoolExecutor。想象一个报表系统:
代码大概长这样:
from annotationlib import Format, get_annotations
from concurrent.futures import InterpreterPoolExecutor
from string.templatelib import Interpolation, Template
def to_sql(template: Template) -> tuple[str, list[object]]:
sql_parts: list[str] = []
params: list[object] = []
for part in template:
if isinstance(part, Interpolation):
sql_parts.append("?")
params.append(part.value)
else:
sql_parts.append(part)
return "".join(sql_parts), params
def report_schema(func) -> dict[str, str]:
return {
key: str(value)
for key, value in get_annotations(func, format=Format.STRING).items()
}
def build_sales_report(region: str, minimum: int) -> dict[str, object]:
sql, params = to_sql(
t"select * from sales where region={region} and amount>={minimum}"
)
return {"sql": sql, "params": params}
print(report_schema(build_sales_report))
jobs = [("east", 100), ("west", 200), ("south", 150)]
with InterpreterPoolExecutor(max_workers=3) as pool:
futures = [
pool.submit(build_sales_report, region, minimum)
for region, minimum in jobs
]
print([future.result() for future in futures])
这个例子不追求复杂,但能体现 Python 3.14 的新味道:结构化模板、可控注解读取、隔离并行任务可以组合在一起。