MoreRSS

site iconPython猫修改

运营「Python 潮流周刊」。Python编程、生活随笔、个人作品。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Python猫的 RSS 预览

Python 3.14 新特性盘点,更新了些什么?

2025-05-08 08:00:00

Python 3.14.0 稳定版将于 2025 年 10 月正式发布,目前已进入 beta 测试阶段。这意味着在往后的几个月里,3.14 的新功能已冻结,不再合入新功能(除了修复问题和完善文档)。
3.14 正好是圈周率 π 的前几位,所以我们将要迎来的其实是一个“圆满”的版本😂
本文想梳理这个版本中一些值得关注的新特性,让我们先睹为快吧~

重点更新

1. 类型提示惰性求值 (PEP 649)

Python 3.14 对类型提示机制做了根本性改进。从这个版本开始,函数、类和模块上的类型提示不再立即计算,而是先存储起来,等真正需要时才进行求值。
这个特性解决了大型项目中一个老大难问题:大量类型提示导致的启动缓慢。对企业级应用和大型项目来说,这简直就是一个福音!
这个改进带来了几个实实在在的好处:
  • 项目启动速度大幅提升,尤其是大型代码仓
  • 引用未定义类型时不再需要加引号,代码更加简洁
  • 新增的 annotationlib 模块提供了灵活的类型提示检查方式
下面的示例直观展示了这个特性带来的好处:
# Python 3.14 - 不再需要引号,即使类型未定义
>>> from annotationlib import get_annotations, Format
>>> def new_way(arg: Undefined):  # 直接使用未定义类型
...     pass

# Python 3.13 及之前版本需要这样写
>>> def old_way(arg: "Undefined"):  # 必须加引号,否则报错
...     pass

# 可以用不同格式查看类型提示
>>> get_annotations(new_way, format=Format.STRING)  # 字符串形式
{'arg': 'Undefined'}
>>> get_annotations(new_way, format=Format.FORWARDREF)  # 前向引用对象形式
{'arg': ForwardRef('Undefined', owner=<function new_way at 0x...>)}
这项改进让 Python 的类型系统变得更加好用,尤其是在使用类型检查工具时确实很有帮助。

2. 基于尾调用的新解释器

Python 3.14 的 CPython 引入了新的解释器实现。不同于之前用大块 switch-case 的传统做法,新解释器用小型 C 函数之间的尾调用来处理字节码。
这种新的架构设计体现了 Python 团队对性能持续优化的决心,虽然效果可能没一开始宣传的那么震撼。
关于性能数据,这里有个有趣的小插曲:
  • 这个特性最早被官宣时,声称有 9-15% 的性能提升
  • 但后来发现数据受到了 LLVM/Clang 19 编译器一个回归问题的影响
  • 经过更严格的测试,实际数据显示:在标准 pyperformance 测试中,平均提升仅有 3-5%
  • 不过在特定的字节码密集型计算中,性能提升仍然可以达到 30%
这项改进的真正价值可能不在当下。从长远来看,这很可能是 Python 解释器架构的一次重要转型,为未来更大幅度的性能提升打下基础。
目前这个解释器只支持 Clang 19 及以上版本编译器,而且只能在 x86-64 和 AArch64 架构上跑。不过官方表示未来 GCC 也会支持。要使用这个功能,需要在编译 Python 时手动指定 --with-tail-call-interp 参数。1

3. 无侵入调试接口 (PEP 768)

Python 3.14 新增的安全外部调试接口是个很给力的新功能,大大改善了 Python 程序的调试体验。以后不用预先在代码中插入任何调试语句,也能连上已经运行中的 Python 进程。
使用起来很简单:
python -m pdb -p 1234  # 直接连接到PID为1234的Python进程
实现这个功能的技术原理很巧妙:
  1. CPython 把关键内部结构的偏移量保存在 _Py_DebugOffsets 中,调试工具可以直接访问
  2. PyThreadState 中新加了外部调试支持结构,可以注入调试脚本
  3. 通过 sys.remote_exec() 功能可以检查和控制运行中的进程
这个特性不仅改善了开发者的调试体验,还为专业调试工具(如 pyspy、py-spy 等)提供了标准接口。这些工具以前必须为每个 Python 版本单独维护内存结构偏移量,现在不用这么麻烦了。生产环境下排查问题的难度也大大降低!

附:Python 3.14 调试特性的深度解读

4. 模板字符串 (PEP 750)

Python 3.14 新增了一种特别的字符串写法——模板字符串(t-string)。就像常用的 f-string 前面加 f 一样,这种新字符串前面加 t
它主要解决了 Web 开发中的安全需求。在用 Django、Flask 这类框架开发网站时,如何安全地处理 HTML 内容和 SQL 语句一直是个老大难问题。这个新的 t-string 语法就是为了解决这个问题,它兼顾了代码的简洁性和内容的安全性。
简单的例子:
from string.templatelib import Template

name = "World"
template = t"Hello {name}"  # 返回一个 Template 对象,而不是字符串
t-string 与 f-string 最大的区别在于:f-string 会直接计算并返回字符串,而 t-string 生成的是 Template 类型的对象。有了这个对象,我们就能对模板内容做各种安全处理,比如 HTML 转义和 SQL 注入防护:
from string.templatelib import Template, html

# 有恶意代码
evil = "<script>alert('evil')</script>"

# 用模板处理,防止 XSS 攻击
template = t"<p>{evil}</p>"

# 使用 html() 函数转义危险字符
html_safe = html(template) 
assert html_safe == "<p>&lt;script&gt;alert('evil')&lt;/script&gt;</p>"
这个新功能会让 Web 开发、日志记录、数据库操作等场景更加安全和方便。

附:Python 3.14 t-string 的全面解析与使用教程

其它重要更新

1. 支持 Zstandard 压缩格式 (PEP 784)

Python 3.14 标准库新增了 compression.zstd 模块,正式把 Zstandard 压缩格式纳入标准支持。这种由 Meta 公司开发的格式既压得小又压得快,甚至超过很多老牌压缩算法,非常适合处理大数据和网络通信场景。
随之一起更新的还有 tarfilezipfileshutil 模块,现在都能直接处理 Zstandard 压缩包了。而原有的 lzmabz2 等压缩模块也被整合到新的 compression 包下,统一了访问方式。

2. 用于 C 扩展的 Python 配置 API (PEP 741)

Python 3.14 新加了 C 语言的配置 API,写 C 扩展时操作 Python 配置变得简单多了。用这个 API 提供的函数,我们可以很方便地查询或改变 Python 的运行时配置。
比如,以前写 C 扩展必须深入了解 CPython 内部构造,现在只需用几行简单代码就能操作内存分配器、日志设置等:
// 看看调试模式是否开启
int debug_enabled = _PyConfig_GetBool(config, "debug");

// 指定模块搜索路径
_PyConfig_SetString(config, "pythonpath", "/custom/path");

3. 实验性 JIT 编译器 (PEP 744)

Python 3.14 正式在官方 macOS 和 Windows 二进制版本中加入了实验性的即时编译器(JIT)。不推荐用于生产环境,仅供测试和尝鲜。
启用方式简单,只需设置环境变量:
# 设置环境变量开启 JIT
PYTHON_JIT=1 python3.14 your_script.py
可以通过 sys._jit 模块检查 JIT 状态:
import sys

if hasattr(sys, "_jit"):
    print(f"JIT状态: {"启用" if sys._jit.is_enabled() else "未启用"}")
else:
    print("JIT不支持")

4. REPL 语法高亮

Python 3.14 的交互式环境(REPL)正式支持语法高亮,大大提升了开发体验。这个功能默认开启,除非你设置了 PYTHON_BASIC_REPL 环境变量。
默认主题使用标准的 ANSI 颜色代码,确保兼容性的同时提供了清晰的视觉区分度,另外也支持自定义颜色主题。
这一改进让原生 Python 接近了 IPython 和 Jupyter 等现代工具的用户体验,也表明官方在重视开发者日常使用的小细节。

5. 放弃 PGP 签名 (PEP 761)

Python 3.14 版本将告别 PGP 签名,改用更现代的签名和验证方式来发布官方软件包。这一改变其实是 Python 跟上了开源软件分发领域的潮流,和当下大多数主流项目的做法一致。

6. finally 块中不能再用跳转语句 (PEP 765)

Python 3.14 增加了一项新限制:不能在 finally 块里用 returnbreakcontinue 跳出去。这种写法以后会被视为语法错误。
这个限制有很实在的目的,因为 finally 中的跳转语句通常会导致代码难懂、难维护,容易把异常情况给吞掉,造成莫名其妙的问题。
# Python 3.13 允许这种危险写法,但异常会被吞
try:
    raise ValueError("重要错误")
except Exception as e:
    print(f"捕获到错误: {e}")
    raise
finally:
    # 这里写 return 会打断异常传递,调用方看不到原始错误
    return "似乎一切正常"

# Python 3.14 中,上述代码将在语法层面被禁止

7. 捕获多个异常时不用加括号了 (PEP 758)

Python 3.14 简化了异常处理的写法。以前想同时捕获多种异常类型必须加括号,而在 3.14 版本中可以直接用逗号分隔:
try:
    operation()
except ValueError, TypeError:  # 可以用逗号分隔多个异常类型
    print("处理值错误或类型错误")

# 对异常组也适用同样的语法
try:
    operation()
except* ValueError, TypeError:  # 异常组也可以用逗号分隔
    print("处理值错误或类型错误")
这种写法不仅更简洁,也将更符合直觉。

不兼容变更

Python 3.14 带来了一些重要的不兼容变更,升级时需要特别注意:
  1. 除了 macOS 和 Windows 外,其它平台上 multiprocessingProcessPoolExecutor 的默认启动方式从 fork 改成了 forkserver。这能避免一些多线程的问题,但可能要修改依赖于旧行为的代码。

  2. CPython 解释器优化了引用计数机制,去掉了一些不必要的计数更新,所以 sys.getrefcount()Py_REFCNT() 返回的数字可能和以前不一样了。

  3. PEP 738 修改了 async/await 关键字的解析规则,一些极端情况下可能造成不兼容。

  4. distutils 模块在 3.12 中已经被强烈警告,3.14 版本会完全移除。如果还在使用,应该迁移到 setuptoolssysconfig

  5. imp 模块所有标记为弃用的函数已经删除,应改用 importlib

  6. asyncio 模块中的一些废弃函数如 @coroutine 装饰器已被移除,应该用 async/await 语法替代。

  7. XML 模块默认开启了更严格的安全限制,可能会导致原有较宽松的解析器设置失效。

  8. SSL/TLS 模块默认禁用了某些过时的加密算法和协议版本,对于需要与旧系统通信的应用可能要额外配置。

总结

这个带着圆周率数字的 Python 3.14 版本,给我们带来了全方位的升级。
在性能上,类型提示的惰性求值和全新的尾调用解释器大幅减轻了启动负担,而实验性的 JIT 编译器则在特定场景下展现了可观的速度提升。
在开发体验上,无侵入调试接口的加入和 REPL 的语法高亮功能使日常编程变得更加舒心逸意。
模板字符串和 Zstandard 压缩的加入则扩展了 Python 在 Web 开发和数据处理领域的应用空间。
除了功能增强,3.14 也带来了一系列语法优化,如异常捕获时不再需要括号以及 finally 块中禁用跳转语句等。这些简化和限制看似微小,却能减少代码中的隐藏 bug。同时,这个版本还作出了一些前瞻性的决定,如放弃 PGP 签名和改用 forkserver 作为默认启动方式。
总的来说,这个圆周率版本在性能、开发体验和语言功能上都有实质性的改进。不管是类型提示、解释器改进还是模板字符串,都展示了 Python 在各个领域的持续发力和进步。

参考资料

uv:统一的 Python 包管理

2025-05-05 08:00:00

花下猫语:uv 项目自发布起就大受欢迎,目前 Github star 52.6 K,远超过它的同类竞品们。前不久,它的创始人在 X 上披露了一组惊人的数据:uv 曾占据了 PyPI 超过 20% 的流量,用户每天通过它发起约 4-5 亿次下载请求!
我在去年翻译过 uv 首发时的新闻文章,根据博客后台不完整的统计,从 Google 搜索进入的访问量已经超过 3000,妥妥成为了我博客的搜索访问 TOP 1!这侧面也反映出 uv 的火爆程度!
uv 刚过一周岁不久,如此年轻,成绩斐然啊。趁着这波热点(实际是假期有时间),我抽空把 uv 重要版本更新的文章也翻译出来了,enjoy reading~

作者:@charliermarsh
译者:豌豆花下猫@Python猫
英文:uv: Unified Python packaging (https://astral.sh/blog/uv-unified-python-packaging)
声明:本翻译是出于交流学习的目的,为便于阅读,部分内容略有改动。转载请保留作者信息。
简述: uv 是一个用 Rust 开发的超高性能 Python 包管理器。
我们最初在 2 月份发布了 uv,作为常见 pip 工作流的即插即用替代方案。(译注:uv 在 2024.02 发布,本文写于 2024.08,翻译于 2025.05)
今天,我们发布一系列新功能,这些功能将 uv 从一个 pip 替代品扩展成为一个端到端的解决方案,可用于管理 Python 项目、命令行工具、单文件脚本,甚至 Python 本身。
它就像是 Python 世界的Cargo:提供了一个快速、可靠且易用的统一接口。

在 Astral,我们致力于为 Python 生态系统打造高性能的开发工具。我们最知名的产品是 Ruff,一个超高速的 Python 代码检查器格式化工具
在 2 月份,我们发布了 uv,这是一个极快的 Python 包安装器和依赖解析器,最初设计为常见 pip 工作流的即插即用替代方案。
今天,我们宣布 uv 自首次发布以来最大规模的功能扩展:
  • 端到端项目管理uv runuv lockuv sync。uv 现在能基于标准元数据创建跨平台的锁文件,并利用该文件来安装依赖。它是 Poetry、PDM 和 Rye 等工具的高性能替代品。
  • 工具管理uv tool installuv tool run (别名为 uvx)。uv 能在隔离的虚拟环境中安装命令行工具,还能无需先安装就直接执行命令(如 uvx ruff check)。它是 pipx 等工具的高性能替代品。
  • Python 安装uv python install。uv 现在可以直接帮你安装 Python,替代 pyenv 等工具。
  • 脚本执行:uv 现在可以管理基于 PEP 723 标准的单文件 Python 脚本,这些脚本内部包含依赖元数据。只需要一个简单的 uv run 命令就能执行这些独立的 Python 脚本。
…所有功能背后都是一个跨平台的超高速依赖解析器在支撑。
上图展示了启用所有可选依赖项时解析 Transformers 项目的速度对比:上方无缓存状态,下方启用了缓存。
并且所有功能都包含在新的、全面的文档中。
这些新功能每一项都大大扩展了 uv 的能力范围。但更独特的是,它们组合起来形成了一个完整的工具链,大大简化了 Python 开发过程。
结合 uv pip(我们将它作为一等功能,继续维护并改进),uv 适用于任何 Python 工作流,从一次性脚本到大型的多包工作区开发。
你可以通过我们的独立安装程序或从 PyPI 安装 uv:
curl
curl -LsSf https://astral.sh/uv/install.sh | sh
win
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
pip
pip install uv
pipx
pipx install uv

项目

uv 现在可以全面管理 Python 项目了。如果你用过 PoetryPDMRye,那么 uv 的项目 API 对你来说一定不陌生。
项目 API 基于 Python 标准构建,使用 pyproject.toml 来定义项目元数据。
例如,你可以运行 uv init && uv add "fastapi>=0.112" 生成以下内容:
[project]
name = "hello-world"
version = "0.1.0"
readme = "README.md"
dependencies = ["fastapi>=0.112"]
从它开始,uv 将基于项目的依赖项创建一个锁文件。以下是一个示例片段:
[[package]]
name = "fastapi"
version = "0.112.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "pydantic" },
    { name = "starlette" },
    { name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2c/09/71a961740a1121d7cc90c99036cc3fbb507bf0c69860d08d4388f842196b/fastapi-0.112.1.tar.gz", hash = "sha256:b2537146f8c23389a7faa8b03d0bd38d4986e6983874557d95eed2acc46448ef", size = 291025 }
wheels = [
    { url = "https://files.pythonhosted.org/packages/39/b0/0981f9eb5884245ed6678af234f2cbcd40f44570718caddc0360bdb4015d/fastapi-0.112.1-py3-none-any.whl", hash = "sha256:bcbd45817fc2a1cd5da09af66815b84ec0d3d634eb173d1ab468ae3103e183e4", size = 93163 },
]

[[package]]
name = "fastapi-cli"
version = "0.0.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "typer" },
    { name = "uvicorn", extra = ["standard"] },
]
sdist = { url = "https://files.pythonhosted.org/packages/c5/f8/1ad5ce32d029aeb9117e9a5a9b3e314a8477525d60c12a9b7730a3c186ec/fastapi_cli-0.0.5.tar.gz", hash = "sha256:d30e1239c6f46fcb95e606f02cdda59a1e2fa778a54b64686b3ff27f6211ff9f", size = 15571 }
wheels = [
    { url = "https://files.pythonhosted.org/packages/24/ea/4b5011012ac925fe2f83b19d0e09cee9d324141ec7bf5e78bb2817f96513/fastapi_cli-0.0.5-py3-none-any.whl", hash = "sha256:e94d847524648c748a5350673546bbf9bcaeb086b33c24f2e82e021436866a46", size = 9489 },
]
uv 的锁文件就像项目依赖关系的完整快照,它能确保项目在不同机器上的运行环境保持一致。
这个锁文件最大的优势是跨平台。不管你在哪个系统上生成锁文件,它都能在其它平台上正常工作。uv 为每个平台定义了一个独特的解决方案,生成一个可读且可审计的锁文件,精确地定义了将安装哪些软件包。
例如:如果你在 macOS 上运行 uv lock,uv 仍然会为 Linux 和 Windows 生成解析方案,即使每个平台上所需的依赖集稍有不同。
而且它做得非常快。在没有任何缓存的情况下,uv 可以在大约半秒钟内解析 Jupyter 项目的依赖项(在缓存预热的情况下约为 20 毫秒)。
解析 Jupyter 项目时无缓存(上)和有缓存(下)的对比。
项目 API 的核心是 uv run 命令,它能在项目专属环境中运行命令,而且不需要手动激活虚拟环境。
uv run 非常快速。每次执行时,它都会自动重新锁定和同步项目,确保你的环境始终是最新状态。完全不需要手动干预。
换句话说,uv run 能确保你的命令每次都在一个一致的、受锁文件管理的环境中运行。
使用 uv run 命令启动 FastAPI 应用的示例。
有了 uv run,你再也不需要操心虚拟环境的激活、依赖包的管理或者项目的维护更新。一切都变得无比简单。
想了解更多详情,请查阅项目文档

本文翻译并首发于 Python猫:uv:统一的 Python 包管理

依赖源:可编辑依赖、相对路径等

uv 基于 Python 标准构建,如 PEP 621。同时,它还添加了一些在标准中没有的功能,这些功能对本地开发至关重要,如相对路径和可编辑依赖。
在标准的 project.dependencies 部分,你可以定义项目的依赖和可发布的元数据。而在开发过程中,uv 还允许你通过 tool.uv.sources 为这些依赖项指定替代来源。
例如,要使用本地的、可编辑版本的 anyio,你可以运行 uv add --editable ../anyio 来生成以下 pyproject.toml
[project]
name = "hello-world"
version = "0.1.0"
readme = "README.md"
dependencies = ["anyio"]

[tool.uv.sources]
anyio = { path = "../anyio", editable = true }
当发布到 PyPI 时,上述包会声明对anyio 的依赖。但在本地开发时,使用 uv run 可以将 ../anyio 路径下的可编辑的包加到开发环境中。这样,项目的依赖定义保持不变,只是依赖的来源发生了变化。
详细内容请查阅依赖源文档

工作区

Cargo 相同概念的启发,uv 支持工作区:一组管理在一起的包的集合。
想象这样一个场景:在同一个 Git 仓库中,有一个使用 FastAPI 构建的 Web 应用,还有多个作为独立 Python 包开发和维护的库。
大多数大型 Python 项目最终都会用自己的方式来实现这个概念,它们通常借助手写脚本和自定义工具来管理各个包之间的关系。
uv 的工作区 API 为大规模开发中的各种挑战提供了一致、高效且具有创新性的解决方案。
在工作区中,每个包都有自己的 pyproject.toml,但工作区共享一个锁文件,确保工作区使用一致的依赖集运行。
[project]
name = "fastapi"
version = "0.1.0"
readme = "README.md"
dependencies = ["uvicorn"]

[tool.uv.sources]
uvicorn = { workspace = true }

[tool.uv.workspace]
members = ["libraries/*"]
在工作区的根目录下,你可以运行命令来操作任何工作区包。例如,执行 uv run --package fastapiuv run --package uvicorn
更多细节,请查阅工作区文档

本文翻译并首发于 Python猫:(https://pythoncat.top/posts/2025-05-05-uv)

工具

uv 现在有两种新能力:
  • 通过 uv tool install 在专用的隔离虚拟环境中安装命令行工具(如 Ruff)
  • 通过 uvx 直接运行一次性命令,无需预先安装
如果你使用过 pipxnpx,你会发现 uv 的工具 API 很熟悉。
比如,输入 uvx posting 就能直接运行 Darren Burns 开发的 posting 终端界面程序(TUI)。
使用 uvx 命令在终端中运行 posting 的示例。
工具 API 让你能集中管理系统上的 Python 工具。你可以:
  • uv tool list 查看所有已安装的工具及其可执行文件
  • uv tool upgrade --all 将所有工具升级到最新版本
uv 的工具 API 非常快,通过 uvx 执行命令几乎没有任何额外开销。
更多工具功能,请查阅工具文档

Python 引导安装

uv 现在能够安装和管理 Python 本身,使其完全自引导:
$ curl -LsSf https://astral.sh/uv/install.sh | sh
$ uv python install 3.12
如果你的机器上没有安装 Python,或者缺少特定的、所需的 Python 版本,除了显式调用 uv python 外,uv 还可以根据需要,自动下载所需的 Python 版本(如 uv runuv venv 等命令)。
例如,以下是在新的 Ubuntu Docker 镜像上运行 posting 所需的全部命令,没有任何隐藏步骤:
$ apt-get update && apt-get install -y curl
$ curl -LsSf https://astral.sh/uv/install.sh | sh
$ source $HOME/.cargo/env
$ uvx posting
当你运行 uvx 命令时,uv 会一步到位地完成所有工作:
  • 安装 Python 3.12.4(文章写作时的最新稳定版本)
  • 创建虚拟环境
  • 安装必要的依赖包
  • 启动 posting 程序
整个过程无缝衔接,高效快捷。

单文件脚本

最后:uv 现在提供了对单文件 Python 脚本的一等支持,这些脚本可以内嵌依赖元数据,完全符合 PEP-723 标准。
例如,以下文件 main.py,它从 Python 增强提案索引中获取前 10 个 PEP:
import requests
from rich.pretty import pprint

resp = requests.get("https://peps.python.org/api/peps.json")
data = resp.json()
pprint([(k, v["title"]) for k, v in data.items()][:10])
这个脚本依赖requestsrich,但不包含任何显式的依赖元数据。早些时候,你需要单独管理这些依赖项,例如使用专门的 requirements.txt 文件。
现在,你可以使用 uv add 自动将依赖声明嵌入到脚本中:
$ uv add --script main.py "requests<3" "rich"
$ cat main.py
# /// script
# requires-python = ">=3.12"
# dependencies = [
#     "requests<3",
#     "rich",
# ]
# ///
import requests
from rich.pretty import pprint

resp = requests.get("https://peps.python.org/api/peps.json")
data = resp.json()
pprint([(k, v["title"]) for k, v in data.items()][:10])
有了它,uv run main.py 将在一个隔离的、临时的虚拟环境中执行脚本,并安装所有依赖项。
使用 uv run 执行带有内嵌依赖项的单文件脚本的示例。
你也可以使用--with 标记在运行时指定依赖项,例如:uv run --with "requests<3" --with rich main.py
有了 uv 的 Python 引导安装功能,你只需要 uv 就可以运行封闭式的、可重分发的单文件 Python 脚本,再也不用担心虚拟环境、包管理或 Python 版本的问题。
更多细节,请查阅脚本文档

为规模化而构建

在 Astral,我们的指导原则之一是:“为所有人设计,为规模化构建(Design for everyone, build for scale)。”
我们希望打造的工具对初学者友好,但同时又能满足最大型 Python 项目的需求。
我们相信 uv 已经很好地实现了这两个目标。
如果你在组织里使用 uv,并想一起解决 Python 生态的包管理挑战,欢迎联系我们。让我们携手共创更好的工具。

附录

Python 潮流周刊#100:有了 f-string,为什么还要 t-string?

2025-05-03 08:00:00

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
分享了 12 篇文章,12 个开源项目,1 则音视频
重要提示: 本周刊迎来第 100 期,限时订或新订阅,可享受八折优惠,欢迎领取优惠券 —> https://www.xiaobot.net/coupon/d2c69b05-91b8-4e2b-b346-e7bc4dbc141a
以下是本期摘要:
① Python 3.14 t-string 要来了,它与 f-string 有何不同?
② 调试 Python f-string 的错误
③ Python 3.14: Python 世界的一大步
④ PEP-790 – Python 3.15 的发布计划
⑤ DjangoCon EU 2025 总结
⑥ 如何用 Meilisearch 为 Django 网站添加闪电般的搜索?
⑦ PyXL:解锁 Python 硬件潜能,GPIO 速度飞跃
⑧ 使用 Gemini 和少量 Python 自动删除代码
⑨ 让 PyPI 的测试套件速度提升 81%
⑩ Python 类型提示:混合类
⑪ 记住 Python 的海象操作符(:=)
⑫ 软件工程师是如何使用 AI 的?
① pyrefly:新一代 Python 类型检查器和 IDE
② Paper2Code:基于机器学习论文自动生成代码
③ aiwaf:AI 驱动的 Django Web 应用防火墙
④ picologging:Python 高性能日志库
⑤ Agentfy:针对社交媒体的多 agent 协调系统
⑥ glyphx:新一代的 Python 绘图库
⑦ sdk-python: Temporal 分布式工作流编排引擎
⑧ mininterface:为 Python 应用提供 GUI、TUI、CLI、Web
⑨ asv:带 Web 报告的 Python 基准测试工具
⑩ nbdime:比较和合并 Jupyter 笔记本
⑪ Qwen3:阿里巴巴的大模型系列
⑫ Kimi-Audio:音频理解、生成和对话大模型
① Python 装饰器的陷阱
周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,欢迎订阅这个你绝对不会后悔的专栏:https://xiaobot.net/p/python_weekly
订阅后,可免费查看 第 100 期周刊的全文:https://www.xiaobot.net/post/b71ec2b4-08ed-4ccd-be66-0cf8cf515171
Python 潮流周刊第 2 季完结了,分享几项总结:https://pythoncat.top/posts/2024-07-14-iweekly
周刊前30期的免费合集,含精美电子书(EPUB/PDF):https://pythoncat.top/posts/2023-12-11-weekly
万字浓缩版,Python 潮流周刊第 1 季的 800 个链接!:https://xiaobot.net/post/78c3d645-86fa-4bd8-8eac-46fb192a339e
微信关注 Python猫https://img.pythoncat.top/python_cat.jpg

Python 类不要再写 __init__ 方法了

2025-05-02 08:00:00

花下猫语:我们周刊第 98 期分享过一篇文章,它指出了 __init__ 方法存在的问题和新的最佳实践,第 99 期也分享了一篇文章佐证了第一篇文章的观点。我认为它们提出的是一个值得注意和思考的问题,因此将第一篇文章翻译成了中文。

原作:Glyph

译者:豌豆花下猫@Python猫

原题:Stop Writing __init__ Methods

原文:https://blog.glyph.im/2025/04/stop-writing-init-methods.html

历史背景

在 Python 3.7 版本(2018 年 6 月发布)引入数据类 (dataclasses) 之前,__init__ 特殊方法有着重要的用途。如果你有一个表示数据结构的类——例如带有 xy 属性的 2DCoordinate——你如果想通过 2DCoordinate(x=1, y=2) 这样的方式构造它,就需要添加一个带有 xy 参数的 __init__ 方法。
那时候可用的其它实现方法都存在相当严重的问题:
  1. 你可以将 2DCoordinate 从公共 API 中移除,转而暴露一个 make_2d_coordinate 函数并使其不可导入,但这样你该如何在文档体现返回值或参数类型呢?
  2. 你可以记录 xy 属性并让用户自己分别赋值,但这样 2DCoordinate() 就会返回一个无效的对象。
  3. 你可以使用类属性将坐标默认值设为 0,这虽然解决了选项 2 的问题,但这会要求所有 2DCoordinate 对象不仅是可变的,而且在每个调用点都必须被修改。
  4. 你可以通过添加一个新的抽象类来解决选项 1 的问题,这个抽象类可以在公共 API 中暴露,但这会使每个新的公共类的复杂性激增,无论它有多简单。更糟糕的是,typing.Protocol 直到 Python 3.8 才出现,所以在 3.7 之前的版本中,这会迫使你使用具体的继承并声明多个类,即使对于最基本的数据结构也是如此。
此外,一个只负责分配几个属性的 __init__ 方法并没有什么明显的问题,所以在这种情况下它是一个不错的选择。考虑到我刚才描述的所有替代方案的问题,它在大多数情况下成为了明显的默认选择,这是有道理的。
然而,因为接受了”定义一个自定义的 __init__“作为用户创建对象的默认方式,我们养成了一个习惯:在每个类的开头都放上一堆可以随意编写的代码,这些代码在每次实例化时都会被执行。
哪里有随意编写的代码,哪里就会有不可控的问题。

问题所在

让我们设想一个复杂点的数据结构,创建一个与外部 I/O 交互的结构:FileReader
当然 Python 有自己的文件对象抽象,但为了演示,我们暂时忽略它。
假设我们有以下函数,位于一个 fileio 模块中:
  • open(path: str) -> int
  • read(fileno: int, length: int)
  • close(fileno: int)
我们假设 fileio.open 返回一个表示文件描述符的整数【注1】,fileio.read 从打开的文件描述符中读取 length 个字节,而 fileio.close 则关闭该文件描述符,使其失效。
根据我们写了无数个 __init__ 方法所形成的思维习惯,我们可能会这样定义 FileReader 类:
class FileReader:
    def __init__(self, path: str) -> None:
        self._fd = fileio.open(path)
    def read(self, length: int) -> bytes:
        return fileio.read(self._fd, length)
    def close(self) -> None:
        fileio.close(self._fd)
对于我们的初始用例,这没问题。客户端代码通过执行类似 FileReader("./config.json") 的操作,来创建一个 FileReader,它会将文件描述符 int 作为私有状态维护起来。这正是我们期望的;我们不希望用户代码看到或篡改 _fd,因为这可能会违反 FileReader 的不变性。构造有效 FileReader 所需的所有必要工作——即调用 open——都由 FileReader.__init__ 处理好了。
然而,随着需求增加,FileReader.__init__ 变得越来越尴尬。
最初我们只关心 fileio.open,但后来,我们可能需要适配一个库,它因为某种原因需要自己管理对 fileio.open 的调用,并想要返回一个 int 作为我们的 _fd,现在我们不得不采用像这样的奇怪变通方法:
def reader_from_fd(fd: int) -> FileReader:
    fr = object.__new__(FileReader)
    fr._fd = fd
    return fr
这样一来,我们之前通过规范对象创建过程所获得的所有优势都丢失了。reader_from_fd的类型签名接收的只是一个普通的int,它甚至无法向调用者建议该如何传入的正确的int 类型。
测试也变得麻烦多了,因为当我们想要在测试中获取 FileReader 的实例而不做实际的文件 I/O 时,都必须打桩替换自己的 fileio.open 副本,即使我们可以(例如)为测试目的在多个 FileReader 之间共享一个文件描述符。
上述例子都假定 fileio.open 是同步操作。但有许多网络资源实际上只能通过异步(因此:可能缓慢,可能容易出错)API 获得,虽然这可能是一个假设性问题。如果你曾经想要写出 async def __init__(self): ...,那么你已经在实践中碰到了这种限制。
要全面描述这种方法的所有问题,恐怕得写一本关于面向对象设计哲学的专著。所以我简单总结一下:所有这些问题的根源其实是相同的——我们把“创建数据结构”这个行为与“这个数据结构常见的副作用”紧密地绑定在了一起。既然说是“常见的”,那就意味着它们并非“总是”相关联的。而在那些并不相关的情况下,代码就会变得笨重且容易出问题
总而言之,定义 __init__ 是一种反模式,我们需要一个替代方案。

解决方案

我认为采用以下三种设计,可解决上述问题:
  • 使用 dataclass 定义属性,
  • 替换之前在 __init__ 中执行的行为,改为用一个新的类方法来实现相同的功能,
  • 使用精确的类型来描述一个有效的实例。

使用 dataclass 属性来创建 __init__

首先,让我们将 FileReader 重构为一个 dataclass。它会为我们生成一个 __init__ 方法,但这不是我们可以随意定义的,它会受到约束,即只能用于赋值属性。
@dataclass
class FileReader:
    _fd: int
    def read(self, length: int) -> bytes:
        return fileio.read(self._fd, length)
    def close(self) -> None:
        fileio.close(self._fd)
但是… 糟糕。在修复自定义 __init__ 调用 fileio.open 的问题时,我们又引入了它所解决的几个问题:
  1. 我们丢失了 FileReader("path") 的简洁便利。现在用户不得不导入底层的 fileio.open,这让最常见的创建对象方式变得既啰嗦又不直观。如果我们想让用户知道如何在实际场景中创建 FileReader,就不得不在文档中添加对其它模块的使用指导。
  2. _fd 作为文件描述符的有效性没有强制检查;它只是一个整数,用户很容易传入不正确的数字,但没有出现报错。
单独来看,只使用 dataclass ,无法解决所有问题,所以我们要加入第二项技术。

使用 classmethod 工厂来创建对象

我们不希望产生额外的导入,或要求用户去查看其它模块——即除了 FileReader 本身之外的任何东西——来弄清楚该如何创建想要的 FileReader
幸运的是,我们有一个工具可以轻松解决这些问题:@classmethod。让我们定义一个 FileReader.open 类方法:
from typing import Self
@dataclass
class FileReader:
    _fd: int
    @classmethod
    def open(cls, path: str) -> Self:
        return cls(fileio.open(path))
现在,你的调用者可以将 FileReader("path") 替换为 FileReader.open("path"),获得与__init__ 相同的好处。
另外,如果我们需要使用await fileio.open(...),就需要一个签名为@classmethod async def open的方法,这可以不受限于__init__作为特殊方法的约束。@classmethod 完全可以是async的,它还可对返回值作修改,比如返回一组相关值的tuple,而不仅仅是返回构造好的对象。

使用 NewType 解决对象有效性问题

接下来,让我们解决稍微棘手的对象有效性问题。
我们的类型签名将这个东西称为 int,底层的 fileio.open 返回的就是普通整数,这点我们无法改变。但是为了有效校验,我们可以使用 NewType 来精确要求:
from typing import NewType
FileDescriptor = NewType("FileDescriptor", int)
有几种方法可以处理底层库的问题,但为简洁起见,也为了展示这种方法不会带来任何运行时开销,我们干脆直接告诉 Mypy:这里使用的 fileio.openfileio.readfileio.write 已经接收 FileDescriptor 类型的整数,而不是普通整数。
from typing import Callable
_open: Callable[[str], FileDescriptor] = fileio.open  # type:ignore[assignment]
_read: Callable[[FileDescriptor, int], bytes] = fileio.read
_close: Callable[[FileDescriptor], None] = fileio.close
当然,我们也必须稍微调整 FileReader,但改动很小。综合这些修改,代码变成了:
from typing import Self
@dataclass
class FileReader:
    _fd: FileDescriptor
    @classmethod
    def open(cls, path: str) -> Self:
        return cls(_open(path))
    def read(self, length: int) -> bytes:
        return _read(self._fd, length)
    def close(self) -> None:
        _close(self._fd)
请注意,这里的关键不是使用NewType,而是让“属性齐全”的对象自然成为“有效实例”。NewType只是一个方便的工具,帮助我们在使用intstrbytes等基本类型时施加必要的约束。

总结 - 新的最佳实践

从现在开始,当你定义新的 Python 类时:
  • 将它写成数据类(或者一个 attrs 类,如果你喜欢的话)
  • 使用默认的 __init__ 方法。【注2】
  • 添加 @classmethod ,为调用者提供方便且公开的对象构造方法。
  • 要求所有依赖项都通过属性来满足,这样总是先创建出一个有效的对象。
  • 使用typing.NewType来对基本数据类型(比如intstr)添加限制条件,尤其是当这些类型需要具备一些特殊属性时,比如必须来自某个特定库、必须是随机生成的等等。
如果以这种方式来定义类,你将获得自定义 __init__ 方法的所有好处:
  • 所有调用你数据结构的人都能拿到有效对象,因为只要属性设置正确,对象自然就是有效的。
  • 你的库用户能够使用便捷的对象创建方法,这些方法会处理好各种复杂工作,让使用变得简单。而且用户只要看一眼类的方法列表,就能发现这些创建方式。
还有一些其它的好处:
  • 你的代码会更经得起未来的考验,能轻松应对用户创建对象的各种新需求。
  • 如果需要有多种实例化你的类的方式,那么可以给每种方式一个有意义的名称;不需要使用像 def __init__(self, maybe_a_filename: int | str | None = None): 这样的怪物。
  • 写测试时,你只需要提供所有需要的依赖项就能构造对象;不需要再用猴子补丁了,因为你可以直接调用类型构造器而不会产生任何 I/O 操作或副作用。
在没有数据类之前,Python 语言中有个怪现象:仅仅是给数据结构填充数据这么基础的事情,竟然要重写一个带着 4 个下划线的方法。__init__方法就像个异类。而其他的魔术方法,像__add____repr__,本质上是在处理类的一些高级特性。
如今,这个历史遗留的语言瑕疵已经得到解决。有了@dataclass@classmethodNewType ,你可以构建出易用、符合 Python 风格、灵活、易测试和健壮的类。
文中注释:
  1. 如果你还不熟悉,“文件描述符”其实是一个只在程序内部有意义的整数。当你让操作系统打开一个文件时,它会回应“我已经为你打开了文件 7”,之后每当你引用“7”这个数字,它就代表那个文件,直到你执行close(7)关闭它。
  2. 当然,除非你有非常充分的理由。比如为了向后兼容,或者与其它库兼容,这些都可能是合理的理由。还有一些数据一致性校验,是无法通过类型系统表达的。最常见的例子是需要检查两个不同字段之间关系的类,比如“range”对象,其中start必须始终小于end。这类规则总有例外。不过,在__init__里执行任何 I/O 操作基本上都不是好主意,而那些在某些特殊情况下可能有用的其它操作,几乎都可以通过__post_init__来实现,而不必直接写__init__

Python 3.14 t-string 要来了,它与 f-string 有何不同?

2025-04-27 08:00:00

Python 最近出了个大新闻:PEP-750 t-string 语法被正式采纳了!
这意味着 Python 将在今年 10 月发布的 3.14 版本中引入一种新的字符串前缀 t,称为模板字符串(Template Strings),即 t-string。
这是继 f-string 之后,字符串处理能力的重大升级,旨在提供更安全、更灵活的字符串插值处理机制。
t-string 的基本语法与 f-string 非常相似,但得到的结果却大为不同:
name = "World"

# f-string 语法
formatted = f"Hello {name}!"
print(type(formatted))  # 输出:<class 'str'>
print(formatted)  # 输出:Hello World!

# t-string 语法
templated = t"Hello {name}!"
print(type(templated))  # 输出:<class 'string.templatelib.Template'>
print(templated.strings)  # 输出:('Hello ', '')
print(templated.interpolations[0].value)  # 输出:World
print("".join(
        item if isinstance(item, str) else str(item.value)
        for item in templated
    ))  # 输出:Hello World!
如上所述,t-string 与 f-string 不同的是,它不会立即得到普通字符串,而是返回一个新的类型 Template(来自 Python 3.14 新增的标准库模块 string.templatelib)。
这个类型不能直接作为普通字符串输出,但它提供了对字符串和其内部插值表达式的结构化访问,使得开发者能够在字符串组合插入值前添加拦截和转换。
一句话总结 t-string 的核心特点就是延迟渲染

为什么要设计 t-string?

f-string 因其简洁易用而广受欢迎,但它也存在一些无法忽视的局限性:
  1. 安全隐患:当直接将用户输入嵌入到 SQL 查询、HTML 内容或系统命令中时,f-string 可能导致注入攻击
  2. 缺乏转换能力:f-string 没有提供在字符串组合前拦截和转换插入值的机制
  3. 灵活性不足:对于复杂的字符串处理任务,f-string 的能力有限

提升字符串处理的安全性

不谨慎使用 f-string,可能导致安全漏洞:
# 使用 f-string 的不安全示例(SQL 注入风险)
sql_query = f"SELECT * FROM users WHERE name = '{user_input}'"

# 使用 f-string 的不安全示例(XSS 风险)
html_content = f"<div>{user_input}</div>"
而 t-string 允许开发者在字符串组合前对插值进行适当处理:
# 使用 t-string 的安全示例
evil = "<script>alert('evil')</script>"
template = t"<p>{evil}</p>"
# 可以定义处理函数来转义内容
assert html(template) == "<p>&lt;script&gt;alert('evil')&lt;/script&gt;</p>"

增强字符串处理的灵活性

t-string 最大的优势在于它提供了统一的字符串处理机制,让开发者可以根据实际需求实现各种自定义渲染逻辑。这种设计避免了为每种场景创建专门语法的复杂性,同时保持了 Python 简洁统一的风格。
以下示例展示了如何基于同一个 t-string 模板,利用不同的渲染器输出不同的格式:
from string.templatelib import Template, Interpolation

data = {"name": "Python猫", "age": 18}
template = t"用户 {data['name']} 的年龄是 {data['age']}"

def standard_renderer(template: Template) -> str:
    """标准文本渲染"""
    return "".join(
        item if isinstance(item, str) else str(item.value)
        for item in template
    )

def json_renderer(template: Template) -> str:
    """JSON格式渲染"""
    import json, re
    values = {}
    for item in template:
        if not isinstance(item, str):
            # 使用正则表达式从表达式中提取键名
            # 匹配 data['name'] 或 data["name"] 模式中的name
            match = re.search(r"\['([^']+)'\]|\[\"([^\"]+)\"\]", item.expression)
            if match:
                # 获取匹配的第一个分组
                key = match.group(1) if match.group(1) else match.group(2)
                values[key] = item.value
    return json.dumps(values, ensure_ascii=False)
    
def xml_renderer(template: Template) -> str:
    """XML格式渲染"""
    parts = ["<data>"]
    for item in template:
        if not isinstance(item, str) and hasattr(item, "expression"):
            name = item.expression.split("'")[1] if "'" in item.expression else item.expression
            parts.append(f"  <{name}>{item.value}</{name}>")
    parts.append("</data>")
    return "\n".join(parts)

# 同一个模板,不同的输出格式
print(standard_renderer(template))  # 输出: 用户 Python猫 的年龄是 18
print(json_renderer(template))      # 输出: {"name": "Python猫", "age": 18}
print(xml_renderer(template))       # 输出: <data>\n  <name>Python猫</name>\n  <age>18</age>\n</data>
这种灵活性是 f-string 所不具备的,对于构建各种 DSL(领域特定语言)、模板引擎或格式化系统非常有价值。

Template 类的结构

t-string 求值后的 Template 类具有以下主要属性和方法:
class Template:
    strings: tuple[str, ...]
    """
    模板中字符串部分的非空元组。
    包含 N+1 个元素,其中 N 是模板中插值表达式的数量。
    """

    interpolations: tuple[Interpolation, ...]
    """
    模板中插值部分的元组。
    如果没有插值表达式,这将是一个空元组。
    """

    def __new__(cls, *args: str | Interpolation):
        """
        创建一个新的 Template 实例。
        参数可以按任意顺序提供。
        """
        ...

    @property
    def values(self) -> tuple[object, ...]:
        """
        返回模板中每个 Interpolation 的 `value` 属性组成的元组。
        如果没有插值表达式,这将是一个空元组。
        """
        ...

    def __iter__(self) -> Iterator[str | Interpolation]:
        """
        迭代模板中的字符串部分和插值表达式。
        这些可能以任意顺序出现。不包含空字符串。
        """
        ...
这种结构使开发者能够:
  • 访问原始字符串片段(strings

  • 访问插值表达式及其计算结果(interpolations

  • 直接获取所有插值的值(values

  • 按顺序迭代模板的所有组成部分

注:__iter__ 函数注释说出现顺序不固定,但 PEP 文档中它的具体实现却是按序的,我认为是注释有误。

t-string 与 f-string 的异同点

相似之处

  1. 基本语法:二者都使用花括号 {} 作为插值表达式的分隔符
  2. 表达式求值:都支持在花括号中放置任意 Python 表达式
  3. 格式说明符:都支持格式说明符(如 .2f)和转换说明符(如 !r
  4. 引号支持:都支持所有有效的引号标记('"'''""")
  5. 大小写不敏感前缀tT 都是有效的,就像 fF

不同之处

  1. 返回类型:f-string 直接返回 str 类型,而 t-string 返回 Template 类型
  2. 求值时机:f-string 在定义时立即求值,t-string 提供延迟求值能力
  3. 结构访问:t-string 允许访问原始模板的结构(字符串部分和插值部分)
  4. 处理模型:f-string 是”即时完成”模型,t-string 是”预处理+转换”模型

t-string 的应用场景

1. 安全的 HTML 模板

使用 t-string 可以创建出自动转义用户输入的 HTML 模板:
def html(template: Template) -> str:
    parts = []
    for item in template:
        if isinstance(item, str):
            parts.append(item)
        else:  # Interpolation
            parts.append(html_escape(item.value))
    return "".join(parts)

user_input = "<script>alert('XSS')</script>"
safe_html = html(t"<div>{user_input}</div>")
# 输出: <div>&lt;script&gt;alert('XSS')&lt;/script&gt;</div>

2. 安全的 SQL 查询构建

t-string 可以构建防注入的 SQL 查询:
def safe_sql(template: Template) -> str:
    parts = []
    params = []
    
    for item in template:
        if isinstance(item, str):
            parts.append(item)
        else:
            parts.append("?")
            params.append(item.value)
    
    return "".join(parts), params

user_id = "user' OR 1=1--"
query, params = safe_sql(t"SELECT * FROM users WHERE id = {user_id}")
# query: "SELECT * FROM users WHERE id = ?"
# params: ["user' OR 1=1--"]

3. 结构化日志

使用 t-string 可以实现优雅的结构化日志记录:
import json
import logging
from string.templatelib import Template, Interpolation

class TemplateMessage:
    def __init__(self, template: Template) -> None:
        self.template = template
    
    @property
    def message(self) -> str:
        # 格式化为可读消息
        return f(self.template)  # 使用自定义 f() 函数
    
    @property
    def values(self) -> dict:
        # 提取结构化数据
        return {
            item.expression: item.value
            for item in self.template.interpolations
        }
    
    def __str__(self) -> str:
        return f"{self.message} >>> {json.dumps(self.values)}"

action, amount, item = "traded", 42, "shrubs"
logging.info(TemplateMessage(t"User {action}: {amount:.2f} {item}"))
# 输出: User traded: 42.00 shrubs >>> {"action": "traded", "amount": 42, "item": "shrubs"}

4. 安全的子进程调用

PEP-787 专门针对 t-string 在子进程调用中的场景作了扩展,使 subprocess 模块能原生支持 t-string:
# 不安全的 f-string 用法
subprocess.run(f"echo {message_from_user}", shell=True)  # 命令注入风险

# 安全的 t-string 用法
subprocess.run(t"echo {message_from_user}")  # 自动进行适当的命令转义
这种方式既保留了字符串命令的可读性,又避免了安全风险。

t-string 的进阶用法

1. 自定义多功能模板渲染器

t-string 的真正威力在于可以自定义渲染器模板:
from string.templatelib import Template, Interpolation
import html

def smart_renderer(template: Template, context="text") -> str:
    """上下文感知的渲染器
    根据context参数自动决定如何处理每个插值:
    - "text": 普通文本模式,直接转为字符串
    - "html": HTML模式,自动转义HTML特殊字符,防止XSS
    - "sql": SQL模式,自动转义SQL特殊字符,防止注入
    """
    parts = []
    
    for item in template:
        if isinstance(item, str):
            parts.append(item)
        else:  # Interpolation
            value = item.value
            expression = item.expression
            
            # 基于值类型和上下文进行智能处理
            if context == "html":
                # HTML模式:自动转义HTML特殊字符
                parts.append(html.escape(str(value)))
            elif context == "sql":
                # SQL模式:防止SQL注入
                if isinstance(value, str):
                    # 将1个单引号转义成2个
                    escaped_value = value.replace("'", "''")
                    parts.append(f"'{escaped_value}'")
                elif value is None:
                    parts.append("NULL")
                else:
                    parts.append(str(value))
            else:
                parts.append(str(value))
    
    return "".join(parts)

# 同一个模板在不同上下文中的自动适配渲染
user_input = "<script>alert('evil')</script>"
template = t"用户输入: {user_input}"
print(smart_renderer(template, context="html")) # 输出: 用户输入: &lt;script&gt;alert(&#x27;evil&#x27;)&lt;/script&gt;

# SQL注入防护示例
user_id = "1'; DROP TABLE users; --"
sql_template = t"SELECT * FROM users WHERE id = {user_id}"
print(smart_renderer(sql_template, context="sql")) # 输出: SELECT * FROM users WHERE id = '1''; DROP TABLE users; --'

# f-string 对于SQL注入,必须先处理值,再放入f-string
escaped_id = user_id.replace("'", "''")
sql_safe_id = f"'{escaped_id}'"
print(f"SQL查询(f-string): SELECT * FROM users WHERE id = {sql_safe_id}")

2. 结构化嵌套模板处理

t-string 和 f-string 在嵌套使用时有本质区别:
# f-string的嵌套:内部表达式立即求值,信息丢失
value = "world"
inner_f = f"inner {value}"
outer_f = f"outer {inner_f}"
print(outer_f)  # 输出: outer inner world
print(type(outer_f))  # <class 'str'> - 只是普通字符串

# t-string的嵌套:保留完整结构信息
inner_t = t"inner {value}"
outer_t = t"outer {inner_t}"
print(type(outer_t))  # <class 'string.templatelib.Template'>
print(type(outer_t.interpolations[0].value))  # 也是Template对象!

# 可以访问和处理任意深度的嵌套结构
user = {"name": "Alice", "age": 30}
message = t"用户{user['name']}信息: {t'年龄:{user['age']}'}"
inner_template = message.interpolations[1].value
print(inner_template.strings)  # 输出: ('年龄:', '')
print(inner_template.interpolations[0].value)  # 输出: 30
这种结构化处理能力使 t-string 特别适合构建复杂的模板系统,可以按需延迟或自定义渲染过程的所有部分。

3. 延迟求值与异步处理

t-string 的结构特性使得它支持延迟求值和异步处理。以下是异步模板渲染示例:
import asyncio

# 模拟异步数据获取
async def fetch_data(key: str) -> str:
    await asyncio.sleep(0.1)  # 模拟网络延迟
    return f"获取的{key}数据"

async def render_async(template):
    tasks = {}
    # 并行启动所有异步查询
    for item in template.interpolations:
        tasks[item.expression] = asyncio.create_task(
            fetch_data(item.expression)
        )
    
    # 等待所有查询完成
    for expr, task in tasks.items():
        tasks[expr] = await task
    
    # 组装结果
    result = []
    for item in template:
        if isinstance(item, str):
            result.append(item)
        else:
            result.append(tasks[item.expression])
    
    return "".join(result)

async def main():
    template = t"用户: {user}, 年龄: {age}"
    result = await render_async(template)
    print(result) 

# asyncio.run(main())
这种模式的关键优势:
  • 结构保留: 可以获取完整表达式信息
  • 并行获取: 同时处理多个异步任务
  • 延迟组合: 等所有数据就绪再拼接

总结

Python 的 t-string 语法是对字符串处理能力的重要扩展,它在保持与 f-string 语法相似性的同时,提供了更灵活、更安全的字符串插值处理机制。通过将字符串模板结构化为 Template 对象,开发者可以在字符串组合前对插值进行拦截和转换,从而避免常见的安全问题,并支持更多高级用例。
它就像是数据与视图的分离模式,f-string 是直接渲染的视图,而 t-string则保留了数据模型,允许你在最终呈现前执行各种转换规则和验证。
t-string 的设计理念体现了功能性与安全性的平衡,虽然它比 f-string 更复杂,但这种复杂性带来了更高级的可组合性和更强的安全保障。
它遵循了 Python 的”显式优于隐式”原则,通过明确分离模板结构和渲染过程,让字符串处理的每个步骤都清晰可见。
t-string 并不是一种替换 f-string 的语法,f-string 的简单易用性依然有其重要价值。
那么,在 Python 3.14 版本后,两个字符串插值方法该如何选择呢?
一句话总结:当只需简单地格式化字符串时,使用 f-string 更直接高效;而当处理不受信任的输入、需要自定义渲染逻辑、构建复杂模板系统或进行异步处理时,应选择功能更强大的 t-string。

参考资料

  1. PEP 750 – Template Strings
  2. PEP 787 – Safer subprocess usage using t-strings
  3. Template strings accepted for Python 3.14

Python 潮流周刊#99:如何在生产环境中运行 Python?

2025-04-26 08:00:00

本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章、教程、开源项目、软件工具、播客和视频、热门话题等内容。愿景:帮助所有读者精进 Python 技术,并增长职业和副业的收入。
分享了 12 篇文章,12 个开源项目,2 则音视频,2 则热门话题
重要提示: 本周刊即将迎来第 100 期,即日起续订或新订阅,可享受八折优惠,欢迎领取优惠券 —> https://www.xiaobot.net/coupon/d2c69b05-91b8-4e2b-b346-e7bc4dbc141a
以下是本期摘要:
① 如何在生产环境中运行 Python?
② Browser Use 原理解析—为何一个小项目能融1700万美元?
③ 2025 年 Django Admin 主题汇总
④ 15,000 行验证过的密码学代码已合入 Python
⑤ 不符合常规的__init__ 用法
⑥ 先睹为快:Python 全新的 ASN.1 API
⑦ 巧用 Python 正则表达式
⑧ Python 的下一个重大事件
⑨ 使用 copier 快速初始化 Python 项目
⑩ ProcessThreadPoolExecutor:当 I/O 成为 CPU 密集型时
⑪ 小白教程:LLM agent 实际就是图
⑫ 万字长文:深度对话50位谷歌高管,揭秘谷歌的“AI追赶之路”
① dia:生成超逼真对话的 TTS 模型
② ai-agents-for-beginners:入门开发 AI agent 的 10 节课
③ juv:创建/管理/运行带依赖的 Jupyter notebook
④ fastapi-forge:带 UI 的 FastAPI 项目生成工具
⑤ py-xiaozhi:Python 版本的小智 AI
⑥ 全方位强化 Python 服务可观测性:以 FastAPI 和 Grafana Stack 为例
⑦ django-ledger:复式记账会计系统和财务分析引擎
⑧ cooragent:AI agent 协作社区
⑨ Tutorial-Codebase-Knowledge:将代码库转换为简易教程
⑩ pyspur:以 10 倍速加快 agent 开发
⑪ WatermarkRemover-AI:基于 Florence-2 和 LaMA 模型的 AI 去水印工具
⑫ index:浏览器 agent,自主执行复杂任务
① 核心开发者聊垃圾回收
② 与 MCP 创造者的播客对谈
① Python 小组作业,做个什么东西好呢?
② 现在用什么技术工具栈开发小程序?
周刊实行付费订阅制,年费 128 元,平均每天不到 4 毛钱,但绝对是一笔有眼光的投资。花钱学习知识,花钱提升自己,欢迎订阅这个你绝对不会后悔的专栏:https://xiaobot.net/p/python_weekly
订阅后,可免费查看 第 99 期周刊的全文:https://www.xiaobot.net/post/3aedf1d1-fc7e-470e-931e-df1a8f554530
Python 潮流周刊第3季总结,附电子书下载:https://pythoncat.top/posts/2025-04-20-sweekly
Python 潮流周刊第二季完结(31~60):https://pythoncat.top/posts/2025-04-20-iweekly
Python 潮流周刊第 2 季完结了,分享几项总结:https://pythoncat.top/posts/2024-07-14-iweekly
Python 潮流周刊第2季(31~60)-纯链接版:https://pythoncat.top/posts/2025-04-19-sweekly
Python 潮流周刊第一季精华合集(1~30):https://pythoncat.top/posts/2023-12-11-weekly
万字浓缩版,Python 潮流周刊第 1 季的 800 个链接!:https://xiaobot.net/post/78c3d645-86fa-4bd8-8eac-46fb192a339e
微信关注 Python猫https://img.pythoncat.top/python_cat.jpg