geekdoc-python-zh/docs/realpython/python311-tomllib.md

778 lines
44 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Python 3.11 预览版:TOML 和 tomllib
> 原文:<https://realpython.com/python311-tomllib/>
Python 3.11 离最终发布越来越近,最终发布将在 2022 年 10 月。新版本目前正在进行 beta 测试,你可以自己安装它来预览和测试一些新功能,包括支持使用新的`tomllib`模块读取 TOML。
TOML 是一种配置文件格式,在 Python 生态系统中越来越流行。这是由采用`pyproject.toml`作为 Python 打包中的中央配置文件所驱动的。其他重要的工具,像[黑色](https://pypi.org/project/black/)、 [mypy](https://mypy.readthedocs.io/en/stable/) 和 [pytest](https://realpython.com/pytest-python-testing/) 也使用 TOML 进行配置。
**在本教程中,您将:**
* 在你的电脑上安装 Python 3.11 测试版,就在你当前安装的 Python 旁边
* 熟悉 **TOML 格式**的基础知识
* **使用新的`tomllib`模块读取 TOML** 文件
* **用第三方库编写 TOML** ,了解为什么这个功能**没有包含在`tomllib`的**中
* 探索 Python 3.11 新的**类型特性**,包括`Self`和`LiteralString`类型以及可变泛型
Python 3.11 中还有许多其他新特性和改进。查看变更日志中的[新增内容](https://docs.python.org/3.11/whatsnew/3.11.html)以获得最新列表,并在 Real Python 上阅读其他 [Python 3.11 预览版](https://realpython.com/search?kind=article&q=python+3.11)以了解其他特性。
**免费奖励:** [掌握 Python 的 5 个想法](https://realpython.com/bonus/python-mastery-course/),这是一个面向 Python 开发者的免费课程,向您展示将 Python 技能提升到下一个水平所需的路线图和心态。
## Python 3.11 测试版
Python 的新版本在每年 10 月发布。代码是在发布日期前[经过 17 个月的时间](https://www.python.org/dev/peps/pep-0602/)开发和测试的。新功能在 [alpha 阶段](https://en.wikipedia.org/wiki/Software_release_life_cycle#Alpha)实现。对于 Python 3.11,在 2021 年 10 月至 2022 年 4 月期间,共发布了七个 **alpha 版本**
Python 3.11 的第一个 **beta 版本**发生在 2022 年 5 月 8 日[凌晨](https://twitter.com/pyblogsal/status/1523636192587423744)。每个这样的预发布都由一个发布经理协调——目前是[Pablo ga lindo Salgado](https://twitter.com/pyblogsal)——并将来自 Python 核心开发者和其他志愿者的数百个提交集合在一起。
这个版本也标志着新版本的**功能冻结**。换句话说Python 3.11.0b1 中没有的新功能不会添加到 Python 3.11 中。相反,功能冻结和发布日期(2022 年 10 月 3 日)之间的时间用于测试和巩固代码。
大约每月[一次](https://www.python.org/dev/peps/pep-0664/)在[测试阶段](https://en.wikipedia.org/wiki/Software_release_life_cycle#Beta)Python 的核心开发者发布一个新的**测试版本**,继续展示新特性,测试它们,并获得早期反馈。目前 Python 3.11 的最新测试版是 **3.11.0b3** ,发布于[2022 年](https://www.python.org/downloads/release/python-3110b3/)6 月 1 日。
**注:**本教程使用的是 Python 3.11 的第三个 beta 版本。如果您使用更高版本,可能会遇到一些小的差异。然而,`tomllib`建立在一个成熟的库之上,你可以预期你在本教程中学到的东西将在 Python 3.11 的测试阶段和最终版本中保持不变。
如果你在维护你自己的 Python 包,那么测试阶段是一个重要的时期,你应该开始用新版本测试你的包。核心开发人员与社区一起,希望在最终发布之前找到并修复尽可能多的 bug。
[*Remove ads*](/account/join/)
### 很酷的新功能
Python 3.11 的一些亮点包括:
* **增强的错误消息**,帮助您更有效地调试代码
* **任务和异常组**,它们简化了异步代码的使用,并允许程序同时引发和处理多个异常
* **TOML 支持**,它允许你使用标准库解析 TOML 文档
* 静态类型改进,让你更精确地注释你的代码
* **优化**,承诺让 Python 3.11 比以前的版本快很多
Python 3.11 有很多值得期待的地方!你已经可以在早期的 [Python 3.11 预览版](https://realpython.com/search?kind=article&q=python+3.11)文章中读到关于[增强的错误消息](https://realpython.com/python311-error-messages/)和[任务和异常组](https://realpython.com/python311-exception-groups/)。要获得全面的概述,请查看 [Python 3.11:供您尝试的酷新功能](https://realpython.com/python311-new-features/)。
在本教程中,您将关注如何使用新的`tomllib`库来读取和解析 TOML 文件。您还将看到 Python 3.11 中的一些打字改进。
### 安装
要使用本教程中的代码示例,您需要在系统上安装 Python 3.11 版本。在这一小节中,你将学习几种不同的方法:使用 **Docker** ,使用 **pyenv** ,或者从**源**安装。选择最适合您和您的系统的一个。
**注意:**测试版是即将推出的功能的预览。虽然大多数特性都可以很好地工作,但是你不应该依赖任何 Python 3.11 beta 版本的产品,或者任何潜在错误会带来严重后果的地方。
如果您可以在您的系统上访问 [Docker](https://docs.docker.com/get-docker/) ,那么您可以通过拉取并运行`python:3.11-rc-slim` [Docker 镜像](https://hub.docker.com/_/python)来下载最新版本的 Python 3.11:
```py
$ docker pull python:3.11-rc-slim
3.11-rc-slim: Pulling from library/python
[...]
docker.io/library/python:3.11-rc-slim
$ docker run -it --rm python:3.11-rc-slim
```
这会将您带入 Python 3.11 REPL。查看 Docker 中的[运行 Python 版本,了解更多关于通过 Docker 使用 Python 的信息,包括如何运行脚本。](https://realpython.com/python-versions-docker/#running-python-in-a-docker-container)
[pyenv](https://realpython.com/intro-to-pyenv/) 工具非常适合管理系统上不同版本的 Python如果你愿意你可以用它来安装 Python 3.11 beta。它有两个不同的版本一个用于 Windows一个用于 Linux 和 macOS。使用下面的切换器选择您的平台:
* [*视窗*](#windows-1)
** [**Linux + macOS**](#linux-macos-1)
**在 Windows 上,你可以使用 [pyenv-win](https://pyenv-win.github.io/pyenv-win/) 。首先更新您的`pyenv`安装:
```py
PS> pyenv update
:: [Info] :: Mirror: https://www.python.org/ftp/python
[...]
```
进行更新可以确保您可以安装最新版本的 Python。你也可以[手动更新`pyenv`](https://pyenv-win.github.io/pyenv-win/#how-to-update-pyenv)。
在 Linux 和 macOS 上,可以使用 [pyenv](https://github.com/pyenv/pyenv) 。首先使用 [`pyenv-update`](https://github.com/pyenv/pyenv-update) 插件更新您的`pyenv`安装:
```py
$ pyenv update
Updating /home/realpython/.pyenv...
[...]
```
进行更新可以确保您可以安装最新版本的 Python。如果你不想使用更新插件那么你可以[手动更新`pyenv`](https://github.com/pyenv/pyenv#upgrading)。
使用`pyenv install --list`查看 Python 3.11 有哪些版本。然后,安装最新版本:
```py
$ pyenv install 3.11.0b3
Downloading Python-3.11.0b3.tar.xz...
[...]
```
安装可能需要几分钟时间。一旦安装了新的测试版,你就可以创建一个虚拟环境,在这里你可以玩它:
* [*视窗*](#windows-2)
** [**Linux + macOS**](#linux-macos-2)*
```py
PS> pyenv local 3.11.0b3
PS> python --version
Python 3.11.0b3
PS> python -m venv venv
PS> venv\Scripts\activate
```
您使用`pyenv local`激活您的 Python 3.11 版本,然后使用`python -m venv`设置虚拟环境。
```py
$ pyenv virtualenv 3.11.0b3 311_preview
$ pyenv activate 311_preview
(311_preview) $ python --version
Python 3.11.0b3
```
在 Linux 和 macOS 上,你使用 [`pyenv-virtualenv`](https://github.com/pyenv/pyenv-virtualenv) 插件来设置虚拟环境并激活它。
你也可以从[python.org](https://www.python.org/)的预发布版本中安装 Python。选择[最新预发布](https://www.python.org/download/pre-releases/),向下滚动到页面底部的*文件*部分。下载并安装与您的系统对应的文件。更多信息参见 [Python 3 安装&设置指南](https://realpython.com/installing-python/)。
本教程中的大多数示例都依赖于新特性,因此您应该使用 Python 3.11 可执行文件来运行它们。具体如何运行可执行文件取决于您的安装方式。如果你需要帮助,那么看看关于 [Docker](https://realpython.com/python-versions-docker/#running-python-in-a-docker-container) 、 [pyenv](https://realpython.com/intro-to-pyenv/) 、[虚拟环境](https://realpython.com/python-virtual-environments-a-primer/)或者[从源码](https://realpython.com/installing-python/)安装的相关教程。
[*Remove ads*](/account/join/)
## `tomllib`Python 3.11 中的 TOML 解析器
Python 是一门成熟的语言。Python 的第一个公共版本发布于 30 多年前的 1991 年。Python 的许多独特特性,包括显式异常处理、对空白的依赖以及丰富的数据结构,如列表和字典,甚至在早期的[就已经存在。](https://en.wikipedia.org/wiki/History_of_Python)
然而Python 的第一个版本缺少的一个特性是共享社区包和模块的便捷方式。这并不奇怪。事实上Python 和[万维网](https://en.wikipedia.org/wiki/World_Wide_Web)几乎是同时发明的。1991 年底,全世界只有[12 台网络服务器](https://w3.org/2012/08/history-of-the-web/origins.htm#c6p4),而且没有一台是专门用于发布 Python 代码的。
随着时间的推移Python 和 T2 互联网变得越来越流行。几个[倡议](https://realpython.com/pypi-publish-python-package/#get-to-know-python-packaging)旨在允许共享 Python 代码。这些特性有机地发展,导致 Python 与打包的关系有些混乱。
在过去的几十年里,这个问题已经通过几个[打包 pep](https://peps.python.org/topic/packaging/)(Python 增强提案)得到了解决,对于[库维护者](https://realpython.com/pypi-publish-python-package/)和[最终用户](https://pypi.org/)来说,情况已经有了很大的改善。
一个挑战是构建包依赖于执行一个`setup.py`文件,但是没有机制知道该文件依赖于哪些依赖项。这就产生了一种[先有鸡还是先有蛋](https://en.wikipedia.org/wiki/Chicken_or_the_egg)的问题,你需要运行`setup.py`来发现如何运行`setup.py`。
实际上,[`pip`](https://realpython.com/what-is-pip/)——Python 的包管理器——假设它应该使用 [Setuptools](https://setuptools.pypa.io/) 来构建包,并且 Setuptools 在你的计算机上是可用的。这使得使用像 [Flit](https://flit.pypa.io) 和[poems](https://realpython.com/dependency-management-python-poetry/)这样的替代构建系统变得更加困难。
为了解决这种情况, [PEP 518](https://peps.python.org/pep-0518/) 引入了 **`pyproject.toml`** 配置文件,它指定了 Python 项目构建依赖关系。2016 年接受了 PEP 518。当时TOML 仍然是一种相当新的格式,而且 Python 或其标准库中没有对解析 TOML 的内置支持。
随着 TOML 格式的成熟和`pyproject.toml`文件的使用Python 3.11 增加了对解析 TOML 文件的支持。在这一节中,您将了解更多关于什么是 TOML 格式,如何使用新的`tomllib`来解析 TOML 文档,以及为什么`tomllib`不支持编写 TOML 文件。
### 学习基本的 TOML
[Tom Preston-Werner](https://tom.preston-werner.com/) 先是[宣布](https://github.com/toml-lang/toml/releases/tag/v0.1.0) **Tom 的显而易见、极简的语言**——俗称**TOML**——并于 2013 年发布了其规范的[版本 0.1.0](https://toml.io/en/v0.1.0) 。从一开始TOML 的目标就是提供一种“最小化的配置文件格式,由于语义明显,易于阅读”( [Source](https://toml.io/en/v0.1.0#objectives) )。TOML 规范的稳定[版本 1.0.0](https://toml.io/en/v1.0.0) 于 2021 年 1 月发布。
TOML 文件是一个 [UTF-8](https://realpython.com/python-encodings-guide/#enter-unicode) 编码的区分大小写的文本文件。TOML 中的主要构件是**键-值对**,其中键与值用等号(`=`)隔开:
```py
version = 3.11
```
在这个最小的 TOML 文档中,`version`是一个具有相应值`3.11`的键。TOML 中的值有类型。`3.11`解释为**浮点数**。您可以利用的其他基本类型有[字符串](https://realpython.com/python-strings/)、[布尔值](https://realpython.com/python-boolean/)、[整数](https://realpython.com/python-numbers/#integers)和日期:
```py
version = 3.11 release_manager = "Pablo Galindo Salgado" is_beta = true beta_release = 3 release_date = 2022-06-01
```
这个例子展示了其中的大部分类型。语法类似于 Python 的语法除了有小写布尔和一个特殊的日期文字。在其基本形式中TOML 键值对类似于 Python 变量赋值,因此它们应该看起来很熟悉。关于这些和其他相似之处的更多细节,请查看 [TOML 文档](https://toml.io/)。
从本质上讲TOML 文档是键值对的集合。您可以通过将它们包装在数组和表中,为这些对添加一些结构。一个**数组**是一个值列表,类似于一个 Python `list`。一个**表**是一个键值对的嵌套集合,类似于 Python `dict`
使用方括号将数组的元素括起来。表格从命名表格的`[key]`行开始:
```py
[python] version = 3.11 release_manager = "Pablo Galindo Salgado" is_beta = true beta_release = 3 release_date = 2022-06-01 peps = [657, 654, 678, 680, 673, 675, 646, 659] [toml] version = 1.0 release_date = 2021-01-12
```
这个 TOML 文档可以用 Python 表示如下:
```py
{
"python": {
"version": 3.11,
"release_manager": "Pablo Galindo Salgado",
"is_beta": True,
"beta_release": 3,
"release_date": datetime.date(2022, 6, 1),
"peps": [657, 654, 678, 680, 673, 675, 646, 659],
},
"toml": {
"version": 1.0,
"release_date": datetime.date(2021, 1, 12),
},
}
```
TOML 中的`[python]`键在 Python 中由字典中的`"python"`键表示,指向包含 TOML 部分中所有键值对的嵌套字典。TOML 表可以任意嵌套,一个 TOML 文档可以包含几个 TOML 表。
这就结束了对 TOML 语法的简短介绍。虽然 TOML 的设计有一个相当简单的语法,但是这里还有一些细节没有涉及到。要深入了解,请查看 [Python 和 TOML:新的最好的朋友](https://realpython.com/python-toml)或 [TOML 规范](https://toml.io/en/latest)。
除了语法之外,您还应该考虑如何解释 TOML 文件中的值。TOML 文档通常用于配置。最终,其他一些应用程序会使用 TOML 文档中的信息。因此,该应用程序对 TOML 文件的内容有一些期望。这意味着一个 TOML 文档可能有两种不同的错误:
1. **语法错误:**TOML 文档不是有效的 TOML。TOML 解析器通常会捕捉到这一点。
2. **模式错误:**TOML 文档是有效的 TOML但是它的结构不是应用程序所期望的。应用程序本身必须处理这个问题。
TOML 规范目前还不包括一种可以用来验证 TOML 文档结构的模式语言,尽管有几个提案存在。这种模式将检查给定的 TOML 文档是否包含给定用例的正确的表、键和值类型。
作为一个非正式模式的例子, [PEP 517](https://peps.python.org/pep-0517/#source-trees) 和 [PEP 518](https://peps.python.org/pep-0518/#build-system-table) 说一个`pyproject.toml`文件应该定义`build-system`表,该表必须包括关键字`requires`和`build-backend`。此外,`requires`的值必须是字符串数组,而`build-backend`的值必须是字符串。下面是一个满足这个模式的 TOML 文档的[示例](https://realpython.com/pypi-publish-python-package/#configure-your-package):
```py
# pyproject.toml [build-system] requires = ["setuptools>=61.0.0", "wheel"] build-backend = "setuptools.build_meta"
```
本例遵循 PEP 517 和 PEP 518 的要求。然而,验证通常由构建者前端完成。
**注意:**如果你想了解更多关于用 Python 构建自己的包的知识,请查看[如何向 PyPI 发布开源 Python 包](https://realpython.com/pypi-publish-python-package/)。
您可以自己检查这个验证。创建以下错误的`pyproject.toml`文件:
```py
# pyproject.toml [build-system] requires = "setuptools>=61.0.0" backend = "setuptools.build_meta"
```
这是有效的 TOML因此该文件可以被任何 TOML 解析器读取。但是,根据 PEPs 中的要求,它不是有效的`build-system`表。为了确认这一点,安装 [`build`](https://pypa-build.readthedocs.io/) ,这是一个符合 PEP 517 的构建前端,并基于您的`pyproject.toml`文件执行构建:
```py
(venv) $ python -m pip install build
(venv) $ python -m build
ERROR Failed to validate `build-system` in pyproject.toml:
`requires` must be an array of strings
```
错误消息指出`requires`必须是一个字符串数组,如 PEP 518 中所指定的。尝试其他版本的`pyproject.toml`文件,注意`build`为你做的其他验证。您可能需要在自己的应用程序中实现类似的验证。
到目前为止,您已经看到了一些 TOML 文档的例子,但是您还没有探索如何在您自己的项目中使用它们。在下一小节中,您将了解如何使用标准库中新的`tomllib`包来读取和解析 Python 3.11 中的 TOML 文件。
[*Remove ads*](/account/join/)
### 用`tomllib`读 TOML】
Python 3.11 在标准库中新增了一个模块,名为 [`tomllib`](https://docs.python.org/3.11/library/tomllib.html) 。您可以使用`tomllib`来读取和解析任何符合 TOML v1.0 的文档。在这一小节中,您将学习如何直接从文件和包含 TOML 文档的字符串中加载 TOML。
[PEP 680](https://peps.python.org/pep-0680/) 描述了`tomllib`和一些导致 TOML 支持被添加到标准库中的过程。在 Python 3.11 中包含`tomllib`的两个决定性因素是`pyproject.toml`在 Python 打包生态系统中扮演的核心角色,以及 TOML 规范将在 2021 年初达到 1.0 版本。
`tomllib`的实现或多或少是直接从[的](https://github.com/hukkin) [`tomli`](https://pypi.org/project/tomli/) 中剽窃来的,他也是 PEP 680 的合著者之一。
`tomllib`模块非常简单,因为它只包含两个函数:
1. **`load()`** 从文件中读取 TOML 文件。
2. **`loads()`** 从字符串中读取 TOML 文件。
您将首先看到如何使用`tomllib`来读取下面的`pyproject.toml`文件,它是 [`tomli`](https://github.com/hukkin/tomli) 项目中相同文件的简化版本:
```py
# pyproject.toml [build-system] requires = ["flit_core>=3.2.0,<4"] build-backend = "flit_core.buildapi" [project] name = "tomli" version = "2.0.1" # DO NOT EDIT THIS LINE MANUALLY. LET bump2version DO IT description = "A lil' TOML parser" requires-python = ">=3.7" readme = "README.md" keywords = ["toml"] [project.urls] "Homepage" = "https://github.com/hukkin/tomli" "PyPI" = "https://pypi.org/project/tomli"
```
复制该文档,并将其保存在本地文件系统上名为`pyproject.toml`的文件中。现在,您可以开始 REPL 会话,探索 Python 3.11 的 TOML 支持:
>>>
```py
>>> import tomllib
>>> with open("pyproject.toml", mode="rb") as fp: ... tomllib.load(fp) ...
{'build-system': {'requires': ['flit_core>=3.2.0,<4'],
'build-backend': 'flit_core.buildapi'},
'project': {'name': 'tomli',
'version': '2.0.1',
'description': "A lil' TOML parser",
'requires-python': '>=3.7',
'readme': 'README.md',
'keywords': ['toml'],
'urls': {'Homepage': 'https://github.com/hukkin/tomli',
'PyPI': 'https://pypi.org/project/tomli'}}}
```
通过向函数传递一个文件指针,使用`load()`来读取和解析 TOML 文件。注意,文件指针必须指向二进制流。确保这一点的一种方法是使用`open()`和`mode="rb"`,其中`b`表示二进制模式。
**注意:**根据 [PEP 680](https://peps.python.org/pep-0680/#types-accepted-as-the-first-argument-of-tomllib-load) 的规定,文件必须以二进制模式打开,这样`tomllib`才能确保 UTF-8 编码在所有系统上都得到正确处理。
将原始的 TOML 文档与生成的 Python 数据结构进行比较。文档由 Python 字典表示其中所有的键都是字符串TOML 中的不同表表示为嵌套字典。注意,原始文件中关于`version`的注释被忽略,并且不是结果的一部分。
您可以使用`loads()`来加载已经用字符串表示的 TOML 文档。以下示例解析来自[前面的子节](#learn-basic-toml)的示例:
>>>
```py
>>> import tomllib
>>> document = """
... [python]
... version = 3.11
... release_manager = "Pablo Galindo Salgado"
... is_beta = true
... beta_release = 3
... release_date = 2022-06-01
... peps = [657, 654, 678, 680, 673, 675, 646, 659]
... ... [toml]
... version = 1.0
... release_date = 2021-01-12
... """
>>> tomllib.loads(document)
{'python': {'version': 3.11,
'release_manager': 'Pablo Galindo Salgado',
'is_beta': True,
'beta_release': 3,
'release_date': datetime.date(2022, 6, 1),
'peps': [657, 654, 678, 680, 673, 675, 646, 659]},
'toml': {'version': 1.0,
'release_date': datetime.date(2021, 1, 12)}}
```
与`load()`类似,`loads()`返回一个字典。一般来说,表示基于基本的 Python 类型:`str`、`float`、`int`、`bool`,以及[字典](https://realpython.com/python-dicts/)、[列表](https://realpython.com/python-lists-tuples/#python-lists)和[、`datetime`对象](https://realpython.com/python-datetime/)。`tomllib`文档包括一个[转换表](https://docs.python.org/3.11/library/tomllib.html#conversion-table),它展示了如何用 Python 表示 TOML 类型。
如果您愿意,那么您可以使用`loads()`结合`pathlib`从文件中读取 TOML:
>>>
```py
>>> import pathlib
>>> import tomllib
>>> path = pathlib.Path("pyproject.toml")
>>> with path.open(mode="rb") as fp:
... from_load = tomllib.load(fp) ...
>>> from_loads = tomllib.loads(path.read_text())
>>> from_load == from_loads
True
```
在这个例子中,你使用`load()`和`loads()`来加载`pyproject.toml`。然后确认无论如何加载文件Python 表示都是相同的。
`load()`和`loads()`都接受一个可选参数: [`parse_float`](https://docs.python.org/3.11/library/tomllib.html#tomllib.load) 。这允许您控制如何用 Python 解析和表示浮点数。默认情况下,它们被解析并存储为`float`对象,在大多数 Python 实现中,这些对象是 64 位的,精度为[精度](https://en.wikipedia.org/wiki/Double-precision_floating-point_format)的大约 16 位十进制数字。
另一种方法是,如果你需要用更精确的数字,用 [`decimal.Decimal`](https://realpython.com/python-rounding/#the-decimal-class) 代替:
>>>
```py
>>> import tomllib
>>> from decimal import Decimal
>>> document = """
... small = 0.12345678901234567890
... large = 9999.12345678901234567890
... """
>>> tomllib.loads(document)
{'small': 0.12345678901234568,
'large': 9999.123456789011}
>>> tomllib.loads(document, parse_float=Decimal)
{'small': Decimal('0.12345678901234567890'),
'large': Decimal('9999.12345678901234567890')}
```
这里加载一个带有两个键值对的 TOML 文档。默认情况下,当使用`load()`或`loads()`时,您会损失一些精度。通过使用`Decimal`类,您可以保持输入的精确性。
如上所述,`tomllib`模块改编自流行的`tomli`模块。如果你想在需要支持旧版本 Python 的代码库上使用 TOML 和`tomllib`,那么你可以依靠`tomli`。为此,请在您的需求文件中添加以下行:
```py
tomli >= 1.1.0 ; python_version < "3.11"
```
这将在 3.11 之前的 Python 版本上使用时安装`tomli`。在您的源代码中,您可以适当地使用`tomllib`或`tomli`和下面的[导入](https://realpython.com/python-import/#handle-packages-across-python-versions):
```py
try:
import tomllib
except ModuleNotFoundError:
import tomli as tomllib
```
这段代码将在 Python 3.11 和更高版本中导入`tomllib`。如果`tomllib`不可用,那么`tomli`被导入并别名为`tomllib`名。
您已经看到了如何使用`tomllib`来读取 TOML 文档。您可能想知道如何编写 TOML 文件。原来不能用`tomllib`写 TOML。请继续阅读了解原因并查看一些替代方案。
[*Remove ads*](/account/join/)
### 写入 tomlt0
类似的现有库如`json`和`pickle`包括`load()`和`dump()`函数,后者用于写数据。`dump()`功能,以及相应的`dumps()`,被故意排除在`tomllib`之外。
根据 [PEP 680](https://peps.python.org/pep-0680/#including-an-api-for-writing-toml) 和围绕它的讨论,这样做有几个原因:
* 将`tomllib`包含在标准库中的主要动机是为了能够*读取生态系统中使用的* TOML 文件。
* TOML 格式被设计成一种对人友好的配置格式,所以许多 TOML 文件都是手工编写的。
* TOML 格式不是像 JSON 或 pickle 那样的数据序列化格式,所以没有必要完全与`json`和`pickle`API 保持一致。
* TOML 文档可能包含在写入文件时应该保留的注释和格式。这与将 TOML 表示为基本 Python 类型不兼容。
* 关于如何布局和格式化 TOML 文件有不同的观点。
* 没有一个核心开发人员表示有兴趣为`tomllib`维护一个写 API。
一旦某些东西被添加到标准库中,就很难更改或删除,因为有人依赖它。这是一件好事,因为这意味着 Python 在很大程度上保持了向后兼容:在 Python 3.10 上运行的 Python 程序很少会在 Python 3.11 上停止工作。
另一个后果是,核心团队对添加新功能持保守态度。如果有明确的需求,可以在以后添加对编写 TOML 文档的支持。
不过,这不会让你空手而归。有几个第三方 TOML 编写器可用。`tomllib`文档提到了两个包:
* [**`tomli-w`**](https://pypi.org/project/tomli-w/) 顾名思义就是可以写 TOML 文档的`tomli`的兄弟姐妹。这是一个简单的模块,没有很多选项来控制输出。
* [**`tomlkit`**](https://pypi.org/project/tomlkit/) 是一个强大的处理 TOML 文档的软件包它支持读写。它保留注释、缩进和其他空白。TOML 工具包是为[诗](https://realpython.com/dependency-management-python-poetry/)开发和使用的。
根据您的用例,其中一个包可能会满足您的 TOML 编写需求。
如果你不想仅仅为了写一个 TOML 文件而添加一个外部依赖,那么你也可以试着滚动你自己的 writer。以下示例显示了一个不完整的 TOML 编写器的示例。它不支持 TOML v1.0 的所有特性,但是它支持编写您之前看到的`pyproject.toml`示例:
```py
# tomllib_w.py
from datetime import date
def dumps(toml_dict, table=""):
document = []
for key, value in toml_dict.items():
match value:
case dict():
table_key = f"{table}.{key}" if table else key
document.append(
f"\n[{table_key}]\n{dumps(value, table=table_key)}"
)
case _:
document.append(f"{key} = {_dumps_value(value)}")
return "\n".join(document)
def _dumps_value(value):
match value:
case bool():
return "true" if value else "false"
case float() | int():
return str(value)
case str():
return f'"{value}"'
case date():
return value.isoformat()
case list():
return f"[{', '.join(_dumps_value(v) for v in value)}]"
case _:
raise TypeError(
f"{type(value).__name__} {value!r} is not supported"
)
```
`dumps()`函数接受一个代表 TOML 文档的字典。它通过遍历字典中的键值对,将字典转换为字符串。你很快就会更仔细地了解细节。首先,您应该检查代码是否有效。打开 REPL 并导入`dumps()`:
>>>
```py
>>> from tomllib_w import dumps
>>> print(dumps({"version": 3.11, "module": "tomllib_w", "stdlib": False}))
version = 3.11
module = "tomllib_w"
stdlib = false
```
你用不同类型的值编写一个简单的字典。它们被正确地写成 TOML 类型:数字是普通的,字符串用双引号括起来,布尔值是小写的。
回头看看代码。大多数对 TOML 类型的序列化发生在助手函数`_dumps_value()`中。它使用[结构模式匹配](https://realpython.com/python310-new-features/#structural-pattern-matching)基于`value`的类型构造不同种类的 TOML 字符串。
主`dumps()`函数与字典一起工作。它遍历每个键值对。如果值是另一个字典,那么它通过添加一个表头来构造一个 TOML 表,然后递归地调用自己来处理表中的键值对。如果值不是一个字典,那么`_dumps_value()`用于正确地将键-值对转换成 TOML。
如上所述,这个编写器不支持完整的 TOML 规范。例如,它不支持 TOML 中可用的所有日期和时间类型,也不支持内嵌或数组表等嵌套结构。在字符串处理中也有一些不被支持的边缘情况。但是,对于许多应用程序来说,这已经足够了。
例如,您可以尝试加载并转储您之前使用的`pyproject.toml`文件:
>>>
```py
>>> import tomllib
>>> from tomllib_w import dumps
>>> with open("pyproject.toml", mode="rb") as fp:
... pyproject = tomllib.load(fp)
...
>>> print(dumps(pyproject))
[build-system]
requires = ["flit_core>=3.2.0,<4"]
build-backend = "flit_core.buildapi"
[project]
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
requires-python = ">=3.7"
readme = "README.md"
keywords = ["toml"]
[project.urls]
Homepage = "https://github.com/hukkin/tomli"
PyPI = "https://pypi.org/project/tomli"
```
这里你先用`tomllib`读`pyproject.toml`。然后使用自己的`tomllib_w`模块将 TOML 文档写回控制台。
如果你需要更好的支持来编写 TOML 文档,你可以扩展一下`tomllib_w`。然而,在大多数情况下,你应该依赖一个现有的包,比如`tomli_w`或者`tomlkit`。
虽然 Python 3.11 不支持编写 TOML 文件,但是包含的 TOML 解析器对许多项目都很有用。接下来,您可以将 TOML 用于您的配置文件,因为您知道在 Python 中读取它们将获得一流的支持。
[*Remove ads*](/account/join/)
## 其他新功能
TOML 支持当然值得庆祝,但是 Python 3.11 中也有一些小的改进。在很长一段时间里Python 的[类型检查](https://realpython.com/python-type-checking/)领域已经出现了这样的增量变化。
[PEP 484](https://peps.python.org/pep-0484/) 引入了类型提示。它们从 Pyhon 3.5 开始就可用了,每一个新的 Python 版本[都为静态类型系统增加了功能](https://dafoster.net/articles/2021/01/26/python's-type-checking-renaissance/)。[在](https://twitter.com/llanga) [PyCon US 2022](https://realpython.com/python-news-april-2022/#pycon-us-2022) 大会的[主题演讲中,茹卡兹·兰加](https://www.youtube.com/watch?v=wbohVjhqg7c)谈到了类型检查。
Python 3.11 接受了几个新的与类型相关的 pep。您将很快了解到更多关于`Self`类型、`LiteralString`类型和可变泛型的知识。
**注意:**类型检查增强有点特殊,因为它们依赖于您的 Python 版本和类型检查工具的版本。最新的测试版支持一些新的 Python 3.11 类型系统特性,但是还没有在所有的类型检查器中实现。
例如,你可以在他们的 [GitHub 页面](https://github.com/python/mypy/issues/12840)上监控 [mypy 的](https://mypy.readthedocs.io/)对新功能的支持状态。
甚至还有一些新的与打字相关的特性,下面就不介绍了。 [PEP 681](https://peps.python.org/pep-0681/) 增加了`@dataclass_transform` [装饰器](https://realpython.com/primer-on-python-decorators/),可以标记语义类似于[数据类](https://realpython.com/python-data-classes/)的类。此外, [PEP 655](https://peps.python.org/pep-0655/) 允许您在[类型化词典](https://realpython.com/python38-new-features/#more-precise-types)中标记必填和可选字段。
### 自身类型
[PEP 673](https://peps.python.org/pep-0673/) 引入了一个新的`Self`类型,它动态地引用当前的类。当您用返回类实例的方法实现类时,这很有用。考虑由[极坐标](https://en.wikipedia.org/wiki/Polar_coordinate_system)表示的二维点的以下部分实现:
```py
# polar_point.py
import math
from dataclasses import dataclass
@dataclass
class PolarPoint:
r: float
φ: float
@classmethod
def from_xy(cls, x, y):
return cls(r=math.hypot(x, y), φ=math.atan2(y, x))
```
您添加了`.from_xy()`构造函数,这样您就可以方便地从相应的[笛卡尔坐标](https://en.wikipedia.org/wiki/Cartesian_coordinate_system)创建`PolarPoint`实例。
**注意:**属性名`.r`和`.φ`是特意选择来模仿[公式](https://en.wikipedia.org/wiki/Polar_coordinate_system#Converting_between_polar_and_Cartesian_coordinates)中使用的数学符号。
一般来说,[建议](https://peps.python.org/pep-0008/#naming-conventions)为属性使用更长更具描述性的名称。然而,有时候遵循你的问题领域的惯例也是有用的。如果你愿意的话,可以随意用`.radius`替换`.r`,用`.phi`或`.angle`替换`.φ`。
Python 源代码由[默认](https://docs.python.org/3/reference/lexical_analysis.html)编码在 [UTF-8](https://realpython.com/python-encodings-guide/#enter-unicode) 中。然而,[标识符](https://peps.python.org/pep-3131/)像变量和属性[不能使用](https://github.com/python/cpython/pull/1686)完整的 Unicode 字母表。例如,在你的变量和属性名中,你必须[远离](https://www.youtube.com/watch?v=Wtm7Iy-wEUI&t=52m43s)和[表情符号](https://pypi.org/project/pythonji)。
您可以按如下方式使用新类:
>>>
```py
>>> from polar_point import PolarPoint
>>> point = PolarPoint.from_xy(3, 4)
>>> point
PolarPoint(r=5.0, φ=0.9272952180016122)
>>> from math import cos
>>> point.r * cos(point.φ)
3.0000000000000004
```
这里,首先创建一个表示笛卡尔点(34)的点。在极坐标中,这个点用半径`r` = 5.0,角度`φ` ≈ 0.927 来表示。您可以使用公式`x = r * cos(φ)`转换回笛卡尔`x`坐标。
现在,您想给`.from_xy()`添加类型提示。它返回一个`PolarPoint`对象。然而,在这一点上你不能直接使用`PolarPoint`作为注释,因为那个类还没有被完全定义。相反,您可以使用带引号的`"PolarPoint"`或者添加一个 [PEP 563](https://peps.python.org/pep-0563/) future import使[推迟注释的求值](https://realpython.com/python37-new-features/#typing-enhancements)。
这两种变通方法都有其缺点,目前推荐的是用一个`TypeVar`代替。这种方法即使在子类中也能工作,但是它很麻烦并且容易出错。
使用新的`Self`类型,您可以向您的类添加类型提示,如下所示:
```py
import math
from dataclasses import dataclass
from typing import Self
@dataclass
class PolarPoint:
r: float
φ: float
@classmethod
def from_xy(cls, x: float, y: float) -> Self: return cls(r=math.hypot(x, y), φ=math.atan2(y, x))
```
注释`-> Self`表明`.from_xy()`将返回当前类的一个实例。如果你创建了一个`PolarPoint`的子类,这也可以正常工作。
工具箱中有了`Self`类型,就可以更方便地使用类和面向对象的特性(如继承)向项目添加静态类型。
[*Remove ads*](/account/join/)
### 任意文字字符串类型
Python 3.11 中的另一个新类型是`LiteralString`。虽然这个名字可能会让你想起 Python 3.8 中添加的`Literal`,但是`LiteralString`的主要用例有点不同。要理解将它添加到类型系统的动机,首先退一步考虑字符串。
一般来说Python 不关心如何构造字符串:
>>>
```py
>>> s1 = "Python"
>>> s2 = "".join(["P", "y", "t", "h", "o", "n"])
>>> s3 = input()
Python
>>> s1 == s2 == s3
True
```
在这个例子中,您以三种不同的方式创建字符串`"Python"`。首先,将它指定为一个文字字符串。接下来,将六个单字符字符串连接起来,形成字符串`"Python"`。最后,您使用 [`input()`](https://docs.python.org/3/library/functions.html#input) 从用户输入中读取字符串。
最后的测试显示每个字符串的值是相同的。在大多数应用程序中,您不需要关心特定的字符串是如何构造的。但是,有些时候您需要小心,尤其是在处理用户输入时。
不幸的是,针对数据库的攻击非常普遍。 [Java Log4j 漏洞](https://nvd.nist.gov/vuln/detail/CVE-2021-44228)同样利用日志系统执行任意代码。
回到上面的例子。虽然`s1`和`s3`的值恰好相同,但是您对这两个字符串的信任应该是完全不同的。假设您需要构建一个 SQL 语句,从数据库中读取关于用户的信息:
>>>
```py
>>> def get_user_sql(user_id):
... return f"SELECT * FROM users WHERE user_id = '{user_id}'"
...
>>> user_id = "Bobby"
>>> get_user_sql(user_id)
"SELECT * FROM users WHERE user_id = 'Bobby'"
>>> user_id = input()
Robert'; DROP TABLE users; --
>>> get_user_sql(user_id)
"SELECT * FROM users WHERE user_id = 'Robert'; DROP TABLE users; --'"
```
这是对一个经典的 SQL 注入例子的改编。恶意用户可以利用编写任意 SQL 代码的能力进行破坏。如果最后一条 SQL 语句被执行,那么它将删除`users`表。
有许多机制可以抵御这类攻击。 [PEP 675](https://peps.python.org/pep-0675/) 名单上又多了一个。一种新的类型被添加到了`typing`模块中:`LiteralString`是一种特殊的字符串类型,它是在您的代码中定义的。
您可以使用`LiteralString`来标记易受用户控制字符串攻击的函数。例如,执行 SQL 查询的函数可以注释如下:
```py
from typing import LiteralString
def execute_sql(query: LiteralString):
# ...
```
类型检查器会特别注意在这个函数中作为`query`传递的值的类型。以下字符串将全部被允许作为`execute_sql`的参数:
>>>
```py
>>> execute_sql("SELECT * FROM users")
>>> table = "users"
>>> execute_sql("SELECT * FROM " + table)
>>> execute_sql(f"SELECT * FROM {table}")
```
最后两个例子没问题,因为`query`是从文字字符串构建的。如果字符串的所有部分都是按字面定义的,则该字符串仅被识别为`LiteralString`。例如,以下示例将无法通过类型检查:
>>>
```py
>>> user_input = input()
users
>>> execute_sql("SELECT * FROM " + user_input)
```
即使`user_input`的值恰好与前面的`table`的值相同,类型检查器也会在这里产生一个错误。用户控制着`user_input`的值,并有可能将其更改为对您的应用程序不安全的值。如果您使用`LiteralString`标记这些易受攻击的函数,类型检查器将帮助您跟踪需要格外小心的情况。
### 可变泛型类型
一个**通用类型**指定了一个用其他类型参数化的类型例如一个字符串列表或一个由一个整数、一个字符串和另一个整数组成的元组。Python 使用方括号来参数化泛型。你把这两个例子分别写成`list[str]`和`tuple[int, str, int]`。
一个**变量**是一个接受可变数量参数的实体。例如,`print()`在 Python 中是一个[变量函数](https://en.wikipedia.org/wiki/Variadic_function):
>>>
```py
>>> print("abc", 123, "def")
abc 123 def
```
通过使用 [`*args`和`**kwargs`](https://realpython.com/python-kwargs-and-args/) 来捕获多个位置和关键字参数,您可以定义自己的变量函数。
如果你想指定你自己的类是泛型的,你可以使用`typing.Generic`。下面是一个向量的例子,也称为一维数组:
```py
# vector.py
from typing import Generic, TypeVar
T = TypeVar("T")
class Vector(Generic[T]):
...
```
[类型变量](https://realpython.com/python-type-checking/#type-variables) `T`用作任何类型的替身。可以在类型注释中使用`Vector`,如下所示:
>>>
```py
>>> from vector import Vector
>>> position: Vector[float]
```
在这个特定的例子中,`T`将是`float`。为了让你的代码更加清晰和类型安全,你也可以使用[类型别名](https://realpython.com/python-type-checking/#type-aliases)甚至[专用的派生类型](https://mypy.readthedocs.io/en/stable/more_types.html#newtypes):
>>>
```py
>>> from typing import NewType
>>> from vector import Vector
>>> Coordinate = NewType("Coordinate", float)
>>> Coordinate(3.11)
3.11
>>> type(Coordinate(3.11))
<class 'float'>
>>> position: Vector[Coordinate]
```
这里,`Coordinate`在运行时的行为类似于`float`,但是静态类型检查将区分`Coordinate`和`float`。
现在,假设您创建了一个更通用的数组类,它可以处理可变数量的维度。直到现在,还没有好的方法来指定这样的**可变泛型**。
[PEP 646](https://peps.python.org/pep-0646/) 引入 [`typing.TypeVarTuple`](https://docs.python.org/3.11/library/typing.html#typing.TypeVarTuple) 来处理这个用例。这些**类型变量元组**本质上是包装在元组中的任意数量的类型变量。您可以使用它们来定义任意维数的数组:
```py
# ndarray.py
from typing import Generic, TypeVarTuple
Ts = TypeVarTuple("Ts")
class Array(Generic[*Ts]):
...
```
注意解包操作符(`*`)的使用。这是语法的必要部分,表明`Ts`代表可变数量的类型。
**注意:**在 3.11 之前的 Python 版本上可以从`typing_extensions`导入`TypeVarTuple`。然而,`*Ts`语法在这些版本上不起作用。作为等价的替代,你可以用 [`typing_extensions.Unpack`](https://docs.python.org/3.11/library/typing.html#typing.Unpack) 并写成`Unpack[Ts]`。
您可以使用`NewType`来标记数组中的尺寸,或者使用 [`Literal`](https://realpython.com/python38-new-features/#more-precise-types) 来指定精确的形状:
>>>
```py
>>> from typing import Literal, NewType
>>> from ndarray import Array
>>> Height = NewType("Height", int)
>>> Width = NewType("Width", int)
>>> Channels = NewType("Channels", int)
>>> image: Array[Height, Width, Channels]
>>> video_frame: Array[Literal[1920], Literal[1080], Literal[3]]
```
您将`image`标注为一个三维数组,其维度标记为`Height`、`Width`和`Channels`。您不需要指定这些维度的大小。第二个例子,`video_frame`,用文字值注释。实际上,这意味着`video_frame`必须是一个特定形状为 1920 × 1080 × 3 的数组。
可变泛型的主要动机是对数组进行类型化,就像你在上面的例子中看到的那样。然而,也有[其他用例](https://realpython.com/python-news-february-2022/#pep-646-variadic-generics)。一旦工具到位, [NumPy](https://realpython.com/numpy-tutorial/) 和[其他](https://peps.python.org/pep-0646/#endorsements)数组库[计划实现](https://github.com/numpy/numpy/issues/16544#issuecomment-1058675773)可变泛型。
[*Remove ads*](/account/join/)
## 结论
在本教程中,您了解了 Python 3.11 中的一些新特性。虽然最终版本将于 2022 年 10 月发布,但你已经可以下载测试版并尝试新功能。在这里,您已经探索了新的`tomllib`模块,并逐渐熟悉了 TOML 格式。
**您已经完成了以下操作:**
* **在您的计算机上安装了** Python 3.11 测试版,就在您当前安装的 Python 旁边
* **使用新的`tomllib`模块读取 TOML** 文件
* 用第三方库编写了 TOML 并创建了自己的函数来编写 TOML 的子集
* 探索 Python 3.11 新的**类型特性**,包括`Self`和`LiteralString`类型以及可变泛型
你已经在你的项目中使用 TOML 了吗?尝试新的 TOML 解析器,并在下面的评论中分享你的经验。
**免费奖励:** [掌握 Python 的 5 个想法](https://realpython.com/bonus/python-mastery-course/),这是一个面向 Python 开发者的免费课程,向您展示将 Python 技能提升到下一个水平所需的路线图和心态。*************