Python 技术手册第三版(八)
原文:
annas-archive.org/md5/9e375b08cb0be52e8b7c2a9eba6f5313译者:飞龙
第二十三章:结构化文本:XML
XML,即 可扩展标记语言,是广泛使用的数据交换格式。除了 XML 本身外,XML 社区(主要在万维网联盟(W3C)内)还标准化了许多其他技术,如模式语言、命名空间、XPath、XLink、XPointer 和 XSLT。
行业联盟还定义了基于 XML 的行业特定标记语言,用于其各自领域应用程序之间的数据交换。XML、基于 XML 的标记语言和其他与 XML 相关的技术经常用于特定领域中的应用程序间、跨语言、跨平台的数据交换。
出于历史原因,Python 标准库在 xml 包下支持多个支持 XML 的模块,具有重叠的功能;本书并未覆盖所有内容,但感兴趣的读者可以在 在线文档 中找到详细信息。
本书(特别是本章)仅涵盖了处理 XML 的最 Pythonic 方法:ElementTree,由我们深感怀念的 Fredrik Lundh,即“effbot”创建。它的优雅、速度、通用性、多种实现和 Pythonic 架构使其成为 Python XML 应用程序的首选包。有关 xml.etree.ElementTree 模块的教程和完整详情,超出了本章提供的内容,请参阅在线文档。本书默认读者具有一些关于 XML 本身的基础知识;如果你需要了解更多关于 XML 的知识,我们推荐 XML in a Nutshell 由 Elliotte Rusty Harold 和 W. Scott Means(O’Reilly)编写。
从不受信任的源解析 XML 会使你的应用程序面临许多可能的攻击风险。我们并未专门讨论这个问题,但 在线文档 建议使用第三方模块来帮助保护你的应用程序,如果你确实需要从无法完全信任的源解析 XML。特别是,如果你需要具有针对解析不受信任源的安全防护的 ElementTree 实现,请考虑使用 defusedxml.ElementTree。
ElementTree
Python 和第三方插件提供了几种 ElementTree 功能的替代实现;你始终可以依赖的是标准库中的模块 xml.etree.ElementTree。只需导入 xml.etree.ElementTree,即可获得你的 Python 安装中标准库中最快的实现。本章介绍的第三方包 defusedxml,提供了略慢但更安全的实现,如果你需要从不受信任的源解析 XML;另一个第三方包 lxml 则提供了更快的性能和一些额外的功能,通过 lxml.etree。
传统上,您可以使用类似以下的 from...import...as 语句获取您喜欢使用的 ElementTree 实现:
`from` xml.etree `import` ElementTree `as` et
或者尝试导入 lxml,并在无法导入时退回到标准库中提供的版本:
`try`:
`from` lxml `import` etree `as` et
`except` ImportError:
`from` xml.etree `import` ElementTree `as` et
成功导入实现后,将其作为 et 使用(有些人喜欢大写变体 ET)在您的代码的其余部分中使用它。
ElementTree 提供了一个表示 XML 文档中节点的基本类:Element 类。ElementTree 还提供了其他重要的类,主要是代表整个树的类,具有输入和输出方法以及许多方便的类,等效于其 Element root 上的类——即 ElementTree 类。此外,ElementTree 模块提供了几个实用函数和次要重要的辅助类。
Element 类
Element 类代表了 XML 文档中的一个节点,是整个 ElementTree 生态系统的核心。每个元素有点像映射,具有将字符串键映射到字符串值的属性,也有点像序列,具有其他元素(有时称为元素的“子元素”)的子节点。此外,每个元素还提供了一些额外的属性和方法。每个 Element 实例 e 有四个数据属性或属性,详见 Table 23-1。
Table 23-1. Element 实例 e 的属性
| attrib | 包含 XML 节点所有属性的字典,以字符串为键(通常相应的值也是字符串)。例如,解析 XML 片段 bc,得到一个 e 其中 e.attrib 是 {'x': 'y'}。
避免访问 Element 实例上的 attrib
在可能的情况下,最好避免访问 e.attrib,因为实现可能需要在访问时动态构建它。e 本身提供了一些典型的映射方法(列在 Table 23-2 中),您可能希望在 e.attrib 上调用这些方法;通过 e 自己的方法让实现可以为您优化性能,而不是通过实际的字典 e.attrib 获得的性能。
|
| tag | 节点的 XML 标签:一个字符串,有时也称为元素的类型。例如,解析 XML 片段 bc,得到一个 e 其中 e.tag 设置为 'a'。 |
|---|---|
| tail | 紧随元素之后的任意数据(字符串)。例如,解析 XML 片段 bc,得到一个 e 其中 e.tail 设置为 'c'。 |
| text | 直接“在”元素内的任意数据(字符串)。例如,解析 XML 片段 bc,得到一个 e 其中 e.text 设置为 'b'。 |
e 有一些类似映射的方法,避免了需要显式请求 e.attrib 字典。这些方法在 Table 23-2 中列出。
表 23-2. Element 实例 e 的类似映射方法
| clear | e.clear() “清空” e,除了其标签外,移除所有属性和子元素,并将文本和尾部设置为 None。 |
|---|---|
| get | e.get(key, default=None) 类似于 e.attrib.get(key, default),但可能更快。不能使用 e[key],因为在 e 上进行索引用于访问子元素,而不是属性。 |
| items | e.items() 返回所有属性的 (name, value) 元组列表,顺序任意。 |
| keys | e.keys() 返回所有属性名的列表,顺序任意。 |
| set | e.set(key, value) 将名为 key 的属性设置为 value。 |
e 的其他方法(包括使用 e[i] 语法进行索引和获取长度的方法,如 len(e))处理 e 的所有子元素作为一个序列,或者在某些情况下——如本节其余部分所示——处理 e 的所有后代(以 e 为根的子树中的元素,也称为 e 的子元素)。
不要依赖于 Element 的隐式布尔转换
在所有 Python 3.11 及之前的版本中,如果 Element 实例 e 没有子元素,e 的布尔值为假,这遵循了 Python 容器隐式布尔转换的常规规则。然而,文档记录表明,这种行为可能会在未来的某个版本中发生变化。为了未来的兼容性,如果你想检查 e 是否没有子元素,请显式地检查 if len(e) == 0: 而不是使用通常的 Python 习惯用法 if not e:。
e 的命名方法处理子元素或后代的详细信息列在 表 23-3 中(本书不涵盖 XPath:有关该主题的信息,请参阅 在线文档)。许多以下方法接受一个可选参数 namespaces,默认为 None。当存在时,namespaces 是一个映射,XML 命名空间前缀作为键,相应的 XML 命名空间全名作为值。
表 23-3. Element 实例 e 处理子元素或后代的方法
| append | e.append(se) 在 e 的子元素末尾添加子元素 se(se 必须是一个 Element)。 |
|---|---|
| extend | e.extend(ses) 将可迭代对象 ses 中的每个元素(每个元素必须是一个 Element)添加到 e 的子元素末尾。 |
| find | e.find(match, namespaces=None) 返回第一个匹配 match 的后代元素,match 可以是标签名或 XPath 表达式(在当前 ElementTree 实现支持的子集内)。如果没有后代元素匹配 match,则返回 None。 |
| findall | e.findall(match, namespaces=None) 返回匹配 match 的所有后代元素列表,match 可以是标签名或 XPath 表达式(在当前 ElementTree 实现支持的子集内)。如果没有后代元素匹配 match,则返回 []。 |
| findtext | e.findtext(match, default=None, namespaces=None) 返回匹配 match 的第一个后代的文本,match 可以是标签名或当前 ElementTree 实现支持的 XPath 表达式的子集。如果匹配的第一个后代没有文本,则结果可能是空字符串 ''。如果没有后代匹配 match,则返回 default。 |
| insert | e.insert(index, se) 在 e 的子元素序列中的索引 index 处添加子元素 se(se 必须是 Element 类型)。 |
| iter | e.iter(tag='') 返回一个迭代器,按深度优先顺序遍历所有 e 的后代。当 tag 不为 '' 时,仅产生标签等于 tag 的子元素。在循环 e.iter 时,请不要修改以 e 为根的子树。 |
| iterfind | e.iterfind(match, namespaces=None) 返回一个迭代器,按深度优先顺序遍历所有匹配 match 的后代,match 可以是标签名或当前 ElementTree 实现支持的 XPath 表达式的子集。当没有后代匹配 match 时,结果迭代器为空。 |
| itertext | e.itertext(match, namespaces=None) 返回一个迭代器,按深度优先顺序遍历所有匹配 match 的后代的文本(不包括尾部),match 可以是标签名或当前 ElementTree 实现支持的 XPath 表达式的子集。当没有后代匹配 match 时,结果迭代器为空。 |
| remove | e.remove(se) 删除元素 se(如在 表 3-4 中所述)。 |
ElementTree 类
ElementTree 类表示映射 XML 文档的树。ElementTree 实例 et 的核心附加值是具有用于整体解析(输入)和写入(输出)整个树的方法。这些方法在 表 23-4 中描述。
表 23-4. ElementTree 实例解析和写入方法
| parse | et.parse(source, parser=None) source 可以是打开以供读取的文件,或要打开并读取的文件名(要解析字符串,请将其包装在 io.StringIO 中,如 “内存文件:io.StringIO 和 io.BytesIO” 中所述),其中包含 XML 文本。et.parse 解析该文本,构建其元素树作为 et 的新内容(丢弃 et 的先前内容(如果有)),并返回树的根元素。parser 是一个可选的解析器实例;默认情况下,et.parse 使用由 ElementTree 模块提供的 XMLParser 类的实例(本书不涵盖 XMLParser;请参阅 在线文档)。 |
|---|---|
| write | et.write(file, encoding='us-ascii', xml_declaration=None, default_namespace=None, method='xml', short_empty_elements=True) file 可以是已打开并用于写入的文件,或要打开并写入的文件名称(要写入字符串,请将 file 作为 io.StringIO 的实例传递,详见 “内存文件:io.StringIO 和 io.BytesIO”)。et.write 将文本写入该文件,表示树的 XML 文档内容,该树是 et 的内容。 |
| write (续) | encoding 应该按照 标准 拼写,而不是使用常见的“昵称” — 例如,'iso-8859-1',而不是 'latin-1',尽管 Python 本身接受这两种编码拼写方式,并且类似地,'utf-8' 带有破折号,而不是 'utf8' 没有破折号。通常最好选择将 encoding 传递为 'unicode'。当 file.write 接受这样的字符串时,这会输出文本(Unicode)字符串;否则,file.write 必须接受字节串,而 et.write 输出的字符串类型将是这种类型,对于不在编码中的字符,将使用 XML 字符引用输出 — 例如,默认的 US-ASCII 编码,“带重音符的 e”,é,将输出为 é。您可以将 xml_declaration 传递为 False 以避免在生成的文本中包含声明,或者传递为 True 以包含声明;默认情况下,仅在编码不是 'us-ascii'、'utf-8' 或 'unicode' 之一时才包含声明。
您可以选择性地传递 default_namespace 来设置 xmlns 结构的默认命名空间。
您可以将 method 传递为 'text' 以仅输出每个节点的文本和尾部(无标记)。您可以将 method 传递为 'html' 以 HTML 格式输出文档(例如,在 HTML 中不需要的结束标记,如
将被省略)。默认为 'xml',以 XML 格式输出。
您可以通过名称(而不是位置)选择性地将 short_empty_elements 传递为 False,以始终使用显式的开始和结束标记,即使对于没有文本或子元素的元素也是如此;默认情况下,对于这种空元素使用 XML 简短形式。例如,默认情况下,具有标签 a 的空元素将输出为 ,如果将 short_empty_elements 传递为 False,则将输出为 。 |
此外,ElementTree 的一个实例 et 提供了方法 getroot(返回树的根)和便利方法 find、findall、findtext、iter 和 iterfind,每个方法与在树的根上调用相同的方法完全等效,也就是说,在 et.getroot 的结果上调用。
ElementTree 模块中的函数
ElementTree 模块还提供了几个函数,详见 表 23-5。
表 23-5. ElementTree 函数
| Comment | Comment(text=None) 返回一个元素,在插入 ElementTree 作为节点后,将作为 XML 注释输出,注释文本字符串被封闭在''之间。XMLParser 跳过任何文档中的 XML 注释,因此这个函数是插入注释节点的唯一方法。 |
|---|---|
| dump | dump(e) 将 e(可以是 Element 或 ElementTree)以 XML 形式写入 sys.stdout。此函数仅用于调试目的。 |
| fromstring | fromstring(text, parser=None) 从 text 字符串解析 XML 并返回一个 Element,就像刚刚介绍的 XML 函数一样。 |
| fromstringlist | fromstringlist(sequence, parser=None) 就像 fromstring(''.join(sequence)),但通过避免连接,可能会更快一些。 |
| iselement | iselement(e) 如果 e 是一个 Element,则返回 True;否则返回 False。 |
| iterparse | iterparse(source, events=['end'], parser=None) 解析 XML 文档并逐步构建相应的 ElementTree。source 可以是打开进行读取的文件,或要打开并读取的文件名,包含 XML 文档作为文本。iterparse 返回一个迭代器,产生两项元组 (event, element),其中 event 是参数 events 中列出的字符串之一(每个字符串必须是 'start'、'end'、'start-ns' 或 'end-ns'),随着解析的进行而变化。element 是 'start' 和 'end' 事件的 Element,'end-ns' 事件的 None,以及 'start-ns' 事件的两个字符串元组(namespace_prefix, namespace_uri)。parser 是一个可选的解析器实例;默认情况下,iterparse 使用 ElementTree 模块提供的 XMLParser 类的实例(有关 XMLParser 类的详细信息,请参阅在线文档)。
iterparse 的目的是在可行的情况下,允许你逐步解析一个大型 XML 文档,而不必一次性将所有生成的 ElementTree 存储在内存中。我们在“逐步解析 XML”中详细讨论了 iterparse。
| parse | parse(source, parser=None) 就像 ElementTree 的 parse 方法,在表 23-4 中介绍的一样,但它返回它创建的 ElementTree 实例。 |
|---|---|
| ProcessingInstruction | ProcessingInstruction(target, text=None) 返回一个元素,在插入 ElementTree 作为节点后,将作为 XML 处理指令输出,目标和文本字符串被封闭在''之间。XMLParser 跳过任何文档中的 XML 处理指令,因此这个函数是插入处理指令节点的唯一方法。 |
| register_namespace | register_namespace(prefix, uri) 将字符串 prefix 注册为字符串 uri 的命名空间前缀;命名空间中的元素将使用此前缀进行序列化。 |
| SubElement | SubElement(parent, tag, attrib={}, **extra) 创建一个带有给定tag和来自字典 attrib 的属性以及作为额外命名参数传递的其他内容的 Element,并将其作为 Element parent的最右边子节点添加;返回它创建的 Element。 |
| tostring | tostring(e, encoding='us-ascii', method='xml', short_empty_elements=True) 返回一个字符串,其中包含以 Element e为根的子树的 XML 表示。参数的含义与 ElementTree 的 write 方法相同,见表 23-4。 |
| tostringlist | tostringlist(e, encoding='us-ascii', method='xml', short_empty_elements=True) 返回一个字符串列表,其中包含以 Element e为根的子树的 XML 表示。参数的含义与 ElementTree 的 write 方法相同,见表 23-4。 |
| XML | XML(text, parser=None) 从文本字符串text解析 XML 并返回一个 Element。parser 是可选的解析器实例;默认情况下,XML 使用由 ElementTree 模块提供的 XMLParser 类的实例(本书不涵盖 XMLParser 类;详见在线文档)。 |
| XMLID | XMLID(text, parser=None) 从文本字符串text解析 XML 并返回一个包含两个条目的元组:一个 Element 和一个将 id 属性映射到每个唯一 Element 的字典(XML 禁止重复 id)。parser 是可选的解析器实例;默认情况下,XMLID 使用由 ElementTree 模块提供的 XMLParser 类的实例(本书不涵盖 XMLParser 类;详见在线文档)。 |
ElementTree 模块还提供了 QName、TreeBuilder 和 XMLParser 类,这些我们在本书中不涵盖,以及 XMLPullParser 类,见“迭代解析 XML”。
使用 ElementTree.parse 解析 XML
在日常使用中,创建 ElementTree 实例最常见的方法是从文件或类似文件的对象中解析它,通常使用模块函数 parse 或 ElementTree 类实例的方法 parse。
在本章剩余的示例中,我们使用在http://www.w3schools.com/xml/simple.xml找到的简单 XML 文件;它的根标记是'breakfast_menu',根的子节点是标记为'food'的元素。每个'food'元素都有一个标记为'name'的子元素,其文本是食物的名称,以及一个标记为'calories'的子元素,其文本是该食物一份中的卡路里数的整数表示。换句话说,对于示例感兴趣的 XML 文件内容的简化表示如下:
`<breakfast_menu``>`
`<food``>`
`<name``>`Belgian Waffles`</name>`
`<calories``>`650`</calories>`
`</food>`
`<food``>`
`<name``>`Strawberry Belgian Waffles`</name>`
`<calories``>`900`</calories>`
`</food>`
`<food``>`
`<name``>`Berry-Berry Belgian Waffles`</name>`
`<calories``>`900`</calories>`
`</food>`
`<food``>`
`<name``>`French Toast`</name>`
`<calories``>`600`</calories>`
`</food>`
`<food``>`
`<name``>`Homestyle Breakfast`</name>`
`<calories``>`950`</calories>`
`</food>`
`</breakfast_menu>`
因为 XML 文档位于 WWW URL 上,所以首先获取一个具有该内容的类似文件的对象,并将其传递给 parse;最简单的方法使用 urllib.request 模块:
`from` `urllib` `import` request
`from` `xml``.``etree` `import` ElementTree `as` et
content = request.urlopen('http://www.w3schools.com/xml/simple.xml')
tree = et.parse(content)
从 ElementTree 中选择元素
假设我们想要在标准输出上打印出各种食物的卡路里和名称,按升序卡路里排序,按字母顺序打破平局。以下是此任务的代码:
`def` bycal_and_name(e):
`return` int(e.find('calories').text), e.find('name').text
`for` `e` `in` sorted(tree.findall('food'), key=bycal_and_name):
print(f"{e.find('calories').text} {e.find('name').text}")
当运行时,这将打印:
600 French Toast
650 Belgian Waffles
900 Berry-Berry Belgian Waffles
900 Strawberry Belgian Waffles
950 Homestyle Breakfast
编辑 ElementTree
构建好一个 ElementTree(无论是通过解析还是其他方式),你可以通过 ElementTree 和 Element 类的各种方法以及模块函数来“编辑”它——插入、删除和/或修改节点(元素)。例如,假设我们的程序可靠地通知我们菜单上添加了一种新食物——涂了黄油的烤面包,两片白面包烤过涂了黄油,含有“berry”字样的食物已被删除(不区分大小写)。针对这些规格的“编辑树”部分可以编码如下:
*`# add Buttered Toast to the menu`*
menu = tree.getroot()
toast = et.SubElement(menu, 'food')
tcals = et.SubElement(toast, 'calories')
tcals.text = '180'
tname = et.SubElement(toast, 'name')
tname.text = 'Buttered Toast'
*`# remove anything related to 'berry' from the menu`*
`for` `e` `in` menu.findall('food'):
name = e.find('name').text
`if` 'berry' `in` name.lower():
menu.remove(e)
一旦我们在解析树的代码和从中选择性打印的代码之间插入这些“编辑”步骤,后者将打印:
180 Buttered Toast
600 French Toast
650 Belgian Waffles
950 Homestyle Breakfast
有时,编辑 ElementTree 的便捷性可能是一个关键考虑因素,值得你将其全部保留在内存中。
从头开始构建 ElementTree
有时,你的任务并不是从现有 XML 文档开始:相反,你需要根据代码从不同来源(如 CSV 文件或某种类型的数据库)获得的数据制作一个 XML 文档。
对于这类任务的代码类似于我们展示的用于编辑现有 ElementTree 的代码——只需添加一个小片段来构建一个最初为空的树。
例如,假设你有一个 CSV 文件,menu.csv,其中两列逗号分隔的是各种食物的卡路里和名称,每行一种食物。你的任务是构建一个 XML 文件,menu.xml,与我们在之前示例中解析过的类似。下面是你可以这样做的一种方式:
import csv
`from` `xml``.``etree` `import` ElementTree `as` et
menu = et.Element('menu')
tree = et.ElementTree(menu)
`with` open('menu.csv') `as` f:
r = csv.reader(f)
`for` calories, namestr `in` r:
food = et.SubElement(menu, 'food')
cals = et.SubElement(food, 'calories')
cals.text = calories
name = et.SubElement(food, 'name')
name.text = namestr
tree.write('menu.xml')
逐步解析 XML
针对从现有 XML 文档中选择元素的任务,有时你不需要将整个 ElementTree 构建在内存中——这一点特别重要,如果 XML 文档非常大时(对于我们处理的微小示例文档不适用,但可以想象类似的以菜单为中心的文档,列出了数百万种不同的食物)。
假设我们有这样一个大型文档,并且我们想要在标准输出上打印出卡路里最低的 10 种食物的卡路里和名称,按升序卡路里排序,按字母顺序打破平局。我们的 menu.xml 文件现在是一个本地文件,假设它列出了数百万种食物,因此我们宁愿不将其全部保存在内存中(显然,我们不需要一次性完全访问所有内容)。
以下代码代表了一种无需在内存中构建整个结构的简单尝试来解析:
import heapq
`from` `xml``.``etree` `import` ElementTree `as` et
def cals_and_name():
*`# generator for (calories, name) pairs`*
`for` _, elem `in` et.iterparse('menu.xml'):
`if` elem.tag != 'food':
`continue`
*`# just finished parsing a food, get calories and name`*
cals = int(elem.find('calories').text)
name = elem.find('name').text
`yield` (cals, name)
lowest10 = heapq.nsmallest(10, cals_and_name())
`for` cals, name `in` lowest10:
print(cals, name)
这种方法确实有效,但不幸的是,它消耗的内存几乎与基于完整 et.parse 的方法相同!这是因为 iterparse 在内存中逐步构建了整个 ElementTree,尽管它仅仅回传事件,如(默认情况下仅)'end',意味着“我刚刚完成了对这个元素的解析”。
要真正节省内存,我们至少可以在处理完元素后立即丢弃每个元素的所有内容——也就是说,在 yield 后,我们可以添加 elem.clear() 使刚处理过的元素为空。
这种方法确实可以节省一些内存,但并非全部,因为树的根仍然会有一个巨大的空子节点列表。要真正节省内存,我们需要获取'开始'事件,以便获取正在构建的 ElementTree 的根,并在使用每个元素后从中移除每个元素,而不仅仅是清除元素。也就是说,我们希望将生成器改为:
def cals_and_name():
*`# memory-thrifty generator for (calories, name) pairs`*
root = `None`
`for` event, elem `in` et.iterparse('menu.xml', ['start', 'end']):
`if` event == 'start':
`if` root `is` `None`:
root = elem
`continue`
`if` elem.tag != 'food':
`continue`
*`# just finished parsing a food, get calories and name`*
cals = int(elem.find('calories').text)
name = elem.find('name').text
`yield` (cals, name)
root.remove(elem)
这种方法尽可能地节省内存,同时完成任务!
在异步循环中解析 XML
虽然 iterparse 在正确使用时可以节省内存,但仍不足以在异步循环中使用。这是因为 iterparse 对传递给它的文件对象进行阻塞读取调用:在异步处理中这种阻塞调用是不可取的。
ElementTree 提供了 XMLPullParser 类来解决这个问题;请参阅在线文档了解该类的使用模式。
¹ Alex 太谦虚了,不过从 1995 年到 2005 年,他和 Fredrik 以及 Tim Peters 一起,都是 Python 的权威。他们以其对语言的百科全书式和详细的了解而闻名,effbot、martellibot 和 timbot 创建的软件和文档对数百万人至关重要。
第二十四章:打包程序和扩展
本章内容有所删减以适应印刷出版,我们在这本书的 GitHub 存储库中的在线版本提供了更多材料。在线版本中我们还描述了诗歌(poetry),这是一个符合现代标准的 Python 构建系统,并将其与传统的 setuptools 方法进行了比较,此外还涵盖了其他主题(完整列表请参见“在线材料”)。
假设您有一些 Python 代码需要交付给其他人和团体。它在您的机器上运行正常,但现在您需要使其在其他人那里也能正常运行。这涉及将您的代码打包成适合的格式并提供给预期的受众。
自上一版以来,Python 打包生态系统的质量和多样性有了很大改善,其文档组织更为完善、内容更加完整。这些改进基于对 Python 源树格式的仔细规定,独立于任何特定构建系统的规定,详见PEP 517,“独立于构建系统的 Python 源树格式”,以及PEP 518,“为 Python 项目指定最低构建系统要求”,后者的“原理”部分简要描述了为何需要进行这些改变,其中最重要的是不再需要运行 setup.py 文件来发现(据推测是通过观察回溯信息)构建的要求。
PEP 517 的主要目的是指定一个名为 pyproject.toml 的文件中的构建定义格式。该文件被组织成称为“表”的部分,每个表的标题包含在方括号中的表名,类似于配置文件。每个表包含各种参数的值,包括名称、等号和值。Python 3.11+ 包括了用于提取这些定义的tomllib模块,具有类似于 json 模块中的 load 和 loads 方法。¹
虽然 Python 生态系统中越来越多的工具使用这些现代标准,但您仍应该预期会继续遇到更传统的基于 setuptools 的构建系统(它本身正在过渡到 PEP 517 推荐的pyproject.toml基础)。关于可用的打包工具的优秀概述,请参阅由 Python Packaging Authority (PyPA) 维护的列表。
为了解释打包,我们首先描述其发展,然后讨论 poetry 和 setuptools。其他符合 PEP 517 标准的构建工具包括 flit 和 hatch,随着互操作性的不断改善,你应该期待它们的数量将继续增长。对于分发相对简单的纯 Python 包,我们还介绍了标准库模块 zipapp,并在本章中通过一个简短的部分解释如何访问作为包一部分捆绑的数据。
本章未涵盖的内容
除了 PyPA 认可的方法外,还有许多其他可能的 Python 代码分发方式,远远超出了单章节的覆盖范围。我们不涵盖以下打包和分发主题,这些主题可能会对希望分发 Python 代码的人感兴趣:
-
使用 conda
-
使用 Docker
-
从 Python 代码创建二进制可执行文件的各种方法,例如以下工具(这些工具对于复杂项目的设置可能有些棘手,但通过扩大应用程序的潜在受众来回报努力):
-
PyInstaller,它接受一个 Python 应用程序并将所有必需的依赖(包括 Python 解释器和必要的扩展库)打包成一个单独的可执行程序,可以作为独立应用程序分发。每个体系结构都有适用于 Windows、macOS 和 Linux 的版本,但每个体系结构只能生成自己的可执行文件。
-
PyOxidizer,这是同名实用工具集中的主要工具,不仅允许创建独立的可执行文件,还可以创建 Windows 和 macOS 安装程序及其他工件。
-
cx_Freeze,它创建一个包含 Python 解释器、扩展库和 Python 代码 ZIP 文件的文件夹。你可以将其转换为 Windows 安装程序或 macOS 磁盘映像。
-
Python 打包的简要历史
在虚拟环境出现之前,维护多个 Python 项目并避免它们不同依赖需求的冲突是一项复杂的任务,需要仔细管理 sys.path 和 PYTHONPATH 环境变量。如果不同项目需要同一个依赖的不同版本,没有一个单独的 Python 环境可以同时支持它们。如今,每个虚拟环境(参见“Python 环境”以深入了解此主题)都有自己的 site_packages 目录,可以通过多种便捷的方式安装第三方和本地的包和模块,大大减少了对机制的需求,使得这些问题基本上不再需要考虑。²
当 Python 包索引于 2003 年构思时,没有这样的功能可用,也没有统一的方法来打包和分发 Python 代码。开发人员必须仔细地为他们所工作的每个不同项目调整他们的环境。随着 distutils 标准库包的开发,情况发生了变化,很快被第三方的 setuptools 包及其 easy_install 实用工具所利用。现在已过时的跨平台 egg 打包格式是 Python 包分发的第一个单文件格式的定义,允许从网络源轻松下载和安装 eggs。安装一个包使用了 setup.py 组件,其执行将使用 setuptools 的特性将包的代码集成到现有的 Python 环境中。要求使用第三方(即不是标准发行版的一部分)模块,如 setuptools,显然不是一个完全令人满意的解决方案。
与这些发展同时进行的是 virtualenv 包的创建,它通过为不同项目使用的 Python 环境提供清晰的分离大大简化了普通 Python 程序员的项目管理。在此之后不久,基于 setuptools 背后的思想,pip 实用程序被引入。使用源树而不是 eggs 作为其分发格式,pip 不仅可以安装软件包,还可以卸载它们。它还可以列出虚拟环境的内容,并接受项目依赖项的带版本的列表,按照约定存储在名为 requirements.txt 的文件中。
setuptools 的开发有些古怪,对社区需求反应不够灵活,因此创建了一个名为 distribute 的分支作为一个可直接替换的解决方案(它安装在 setuptools 名下),以便允许更具合作性的开发工作进行。这最终被合并回了 setuptools 代码库中,现在由 PyPA 控制:能够做到这一点肯定了 Python 的开源许可政策的价值。
-3.11 distutils 包最初设计为标准库组件,帮助安装扩展模块(特别是那些用编译语言编写的模块,在 第二十五章 中有介绍)。尽管它目前仍然存在于标准库中,但它已被弃用,并计划在版本 3.12 中删除,届时可能会并入 setuptools。出现了许多其他工具,符合 PEP 517 和 518。在本章中,我们将介绍不同的方法来将额外的功能安装到 Python 环境中。
随着PEP 425,“内置分发的兼容性标签”,以及PEP 427,“Wheel 二进制包格式”,Python 终于有了一个二进制分发格式的规范(wheel,其定义已经更新),允许在不同架构下分发编译的扩展包,当没有合适的二进制 wheel 可用时则回退到源码安装。
PEP 453,“Python 安装中 pip 的显式引导”,决定 pip 实用程序应成为 Python 中首选的安装包的方式,并建立了一个独立于 Python 的更新过程,以便可以不必等待新的语言发布而提供新的部署功能。
这些发展以及许多其他使 Python 生态系统合理化的努力都归功于 PyPA,Python 的领导“Steering Council”已将与打包和分发相关的大多数事项委托给他们。要深入了解本章节的更高级内容,请参阅“Python 打包用户指南”,该指南为希望广泛发布其 Python 软件的任何人提供了明智的建议和有用的指导。
在线资料
正如本章节开头所提到的,本章节的在线版本包含额外的材料。讨论的主题包括:
-
构建过程
-
入口点
-
发布格式
-
poetry
-
setuptools
-
分发您的包
-
zipapp
-
访问与您的代码一起提供的数据
¹ 旧版本的用户可以使用pip install toml从 PyPI 安装该库。
² 请注意,某些软件包对虚拟环境不太友好。幸运的是,这种情况很少见。
第二十五章:经典 Python 的扩展和嵌入
本章内容已经为本书的印刷版本进行了缩编。完整内容可在在线版中找到,详见“在线资料”。
CPython 运行在一个可移植的、用 C 编写的虚拟机上。Python 的内置对象,比如数字、序列、字典、集合和文件,都是用 C 编写的,Python 标准库中也有几个模块是如此。现代平台支持动态加载库,文件扩展名为 .dll(Windows)、.so(Linux)和 .dylib(Mac),构建 Python 时会生成这些二进制文件。你可以用 C(或任何可以生成 C 可调用库的语言)编写自己的 Python 扩展模块,使用本章节介绍的 Python C API。有了这个 API,你可以生成和部署动态库,供 Python 脚本和交互会话后续使用,使用 import 语句导入,详见“导入语句”。
扩展 Python 意味着构建模块,供 Python 代码import以访问模块提供的功能。嵌入 Python 意味着在另一种语言编写的应用程序中执行 Python 代码。为了使这种执行有用,Python 代码反过来必须能够访问一些你的应用程序的功能。因此,实际上,嵌入暗示了一些扩展,以及一些特定于嵌入的操作。希望扩展 Python 的三个主要原因可以总结如下:
-
在较低级语言中重新实现一些功能(最初用 Python 编写),希望能获得更好的性能。
-
让 Python 代码访问由低级语言编写(或至少可从中调用)的库提供的一些现有功能
-
让 Python 代码访问一个正在将 Python 作为应用程序脚本语言嵌入到应用程序中的应用程序的一些现有功能
Python 的在线文档涵盖了嵌入和扩展;在那里,你可以找到深入的教程和广泛的参考手册。许多细节最好通过 Python 的广泛文档化的 C 源代码学习。下载 Python 的源代码分发包,并学习 Python 核心的源代码、C 编写的扩展模块以及为此目的提供的示例扩展。
在线资料
本章假设读者具备一些 C 的知识
虽然我们包括一些非 C 扩展选项,但要使用 C API 扩展或嵌入 Python,你必须了解 C 和/或 C++ 编程语言。我们在本书中不涵盖 C 和 C++,但有许多印刷和在线资源可供学习。本章的在线内容大多假设你至少有一些 C 的知识。
在本章的在线版本中,你会找到以下章节:
“用 Python 的 C API 扩展 Python”
包括参考表格和示例,用于创建 C 代码 Python 扩展模块,可以导入到你的 Python 程序中,展示如何编码和构建这些模块。本节包含两个完整示例:
-
一个实现自定义方法以操作字典的扩展
-
一个定义自定义类型的扩展
“不使用 Python 的 C API 扩展 Python”
讨论(或至少提到和链接到)几个工具和库,支持创建 Python 扩展,而无需直接使用 C 或 C++ 编程,¹ 包括第三方工具 F2PY, SIP, CLIF, cppyy, pybind11, Cython, CFFI 和 HPy,以及标准库模块 ctypes。本节包含一个使用 Cython 创建扩展的完整示例。
“嵌入 Python”
包括参考表格和嵌入 Python 解释器到更大应用中的概念概述,使用 Python 的 C API 进行嵌入。
¹ 还有许多其他类似工具,但我们试图仅挑选最流行和有前景的工具。
第二十六章:从 v3.7 到 v3.n 的迁移
这本书跨越了几个版本的 Python,并涵盖了一些重要的(仍在发展中的!)新功能,包括:
-
保持顺序的字典
-
类型注解
-
:= 赋值表达式(俗称“海象操作符”)
-
结构化模式匹配
个别开发者可能会在每个新的 Python 版本发布后进行安装,并在解决兼容性问题时逐步升级。但对于在企业环境中工作或维护共享库的 Python 开发者来说,从一个版本迁移到下一个版本需要深思熟虑和计划。
本章讨论了 Python 语言的变化,从 Python 程序员的视角来看。(Python 内部也有许多变化,包括 Python C API 的变化,但这些超出了本章的范围:详情请参见每个发布版本的在线文档中的“Python 3.n 新特性”部分。)
Python 3.11 中的重大更改
大多数版本都有几个显著的新功能和改进,这些特性和改进可以作为选择特定版本的高层次原因。表 26-1 详细介绍了版本 3.6–3.11 的主要新功能和破坏性更改,这些更改可能影响许多 Python 程序;更完整的列表请参见附录。
表 26-1. 近期 Python 发布的重大更改
| 版本 | 新功能 | 破坏性更改 |
|---|---|---|
| 3.6 |
-
字典保留顺序(作为 CPython 的实现细节)
-
添加了 F-字符串
-
支持数字文字中的下划线
-
注解可以用于类型,可以通过外部工具如 mypy 进行检查
-
asyncio 不再是一个临时模块
初始发布时间:2016 年 12 月
支持结束时间:2021 年 12 月
|
- 不再支持在大多数 re 函数的模式参数中使用未知转义的 \ 和 ASCII 字母(仅在 re.sub() 中仍然允许)
|
| 3.7 |
|---|
-
字典保留顺序(作为正式语言的保证)
-
添加了 dataclasses 模块
-
添加了 breakpoint() 函数
初始发布时间:2018 年 6 月
计划支持结束时间:2023 年 6 月
|
-
不再支持在 re.sub() 的模式参数中使用未知转义的 \ 和 ASCII 字母
-
不再支持在 bool()、float()、list() 和 tuple() 中使用命名参数
-
不再支持在 int() 中使用前置命名参数
|
| 3.8 |
|---|
-
添加了赋值表达式(:=,也称为海象操作符)
-
在函数参数列表中使用 / 和 * 表示位置参数和命名参数
-
在 f-字符串中使用尾部 = 进行调试(f'{x=}' 的简写形式为 f'x={x!r}')
-
添加了类型类(Literal, TypedDict, Final, Protocol)
初始发布时间:2019 年 10 月
计划支持结束时间:2024 年 10 月
|
-
移除了 time.clock();使用 time.perf_counter()
-
移除了 pyvenv 脚本;使用 python -m venv 替代
-
yield 和 yield from 不再允许在推导式或生成器表达式中使用
-
添加了对is和is not对 str 和 int 字面值的语法警告
|
| 3.9 |
|---|
-
字典上支持联合运算符|和|=
-
添加了 str.removeprefix()和 str.removesuffix()方法
-
添加了 zoneinfo 模块以支持 IANA 时区(替换第三方 pytz 模块)
-
类型提示现在可以在泛型中使用内置类型(例如 list[int]而不是 List[int])
初始发布时间:2020 年 10 月
预计支持结束时间:2025 年 10 月
|
-
移除了 array.array.tostring()和 fromstring()方法
-
移除了 threading.Thread.isAlive()(请使用 is_alive()代替)
-
移除了 ElementTree 和 Element 的 getchildren()和 getiterator()方法
-
移除了 base64.encodestring()和 decodestring()(请改用 encodebytes()和 decodebytes())
-
移除了 fractions.gcd()(请使用 math.gcd()代替)
-
移除了 typing.NamedTuple._fields(请改用 annotations 代替)
|
| 3.10 |
|---|
-
支持match/case结构模式匹配
-
允许将联合类型写为 X | Y(在类型注解中和作为 isinstance()的第二个参数)
-
添加了 zip()内置函数的可选 strict 参数,以检测长度不同的序列
-
官方支持了带括号的上下文管理器;例如,with(ctxmgr, ctxmgr, ...)
初始发布时间:2021 年 10 月
预计支持结束时间:2026 年 10 月
|
-
从 collections 中移除了 ABCs 导入(现在必须从 collections.abc 导入)
-
大多数 asyncio 高级 API 中的循环参数已被移除
|
| 3.11 |
|---|
-
改进了错误消息
-
总体性能提升
-
添加了异常组和 except*表达式
-
添加了类型提示类(Never,Self)
-
将 tomllib TOML 解析器添加到标准库
初始发布时间:2022 年 10 月
预计支持结束时间:2027 年 10 月(估计)
|
-
移除了 binhex 模块
-
将 int 转换为 str 的限制扩展到 4300 位数字
|
规划 Python 版本升级
首先为什么要升级?如果您有一个稳定运行的应用程序和一个稳定的部署环境,那么一个合理的决定可能是不做任何更改。但是版本升级确实带来了好处:
-
新版本通常引入新功能,这可能使您能够简化代码。
-
更新的版本包括错误修复和重构,可以提高系统的稳定性和性能。
-
旧版本中发现的安全漏洞可能在新版本中得到修复。²
最终,旧版 Python 版本将不再受支持,运行在旧版本上的项目将变得难以维护且成本更高。因此,升级可能成为必要。
选择目标版本
在决定迁移哪个版本之前,有时你必须首先弄清楚,“我现在正在运行哪个版本?”你可能会不愉快地发现,你公司系统中存在运行不受支持的 Python 版本的旧软件。通常情况是,这些系统依赖于某些第三方包,而这些包本身版本落后或没有可用的升级版本。当这种系统在公司运营中扮演重要角色时,情况会更为严峻。你可以通过远程访问 API 隔离落后的包,允许该包在旧版本上运行,同时让你自己的代码安全升级。必须向高级管理层展示存在升级约束的系统,以便他们了解保留、升级、隔离或替换的风险和权衡。
目标版本的选择通常默认为“最新版本”。这是一个合理的选择,因为在进行升级时,它通常是成本效益最高的选项:最新发布的版本将具有最长的支持期。更保守的立场可能是“最新版本减一”。你可以相当确信,版本 N–1 在其他公司进行了一段生产测试期间,并且其他人已经解决了大部分问题。
确定工作范围
在选择了 Python 的目标版本之后,识别从当前软件使用的版本到目标版本(包括目标版本)之间的所有突破性变化(请参阅附录中的详细功能和版本变化表;更多详细信息可以在在线文档的“Python 3.n 新特性”部分找到)。通常会有适用于当前版本和目标版本的兼容形式文档化的突破性变化。记录并传达开发团队在升级之前需要进行的源代码更改。(如果您的代码受到大量突破性变化或与相关软件的兼容性问题的影响,直接升级到所选目标版本可能涉及比预期更多的工作。您甚至可能需要重新考虑目标版本的选择,或者考虑采取较小的步骤。也许您会决定首先升级到 目标–1,然后推迟升级到 目标 或 目标+1 的任务,作为后续升级项目。)
确定您的代码库使用的任何第三方或开源库,并确保它们与目标 Python 版本兼容(或计划与之兼容)。即使您自己的代码库已准备好升级到目标版本,落后的外部库可能会阻碍您的升级项目。必要时,您可以将这样的库隔离在单独的运行时环境中(使用虚拟机或容器技术),如果该库提供远程访问编程接口的话。
在开发环境中提供目标 Python 版本,并可选择在部署环境中提供,以便开发人员确认其升级更改是完整和正确的。
应用代码更改
一旦确定了目标版本并识别了所有破坏性更改,您将需要在代码库中进行更改,使其与目标版本兼容。理想情况下,您的目标是使代码以与当前版本和目标 Python 版本均兼容的形式存在。
导入自 future
future 是一个标准库模块,包含各种功能,文档在 在线文档 中,用于简化版本之间的迁移。它不同于任何其他模块,因为导入功能可能影响您程序的语法,而不仅仅是语义。这些导入必须是代码的初始可执行语句。
每个“未来特性”都使用以下语句激活:
`from` __future__ `import` *`feature`*
其中 feature 是您想要使用的功能名称。
在本书涵盖的版本范围内,您可能考虑使用的唯一未来特性是:
`from` __future__ `import` annotations
允许引用尚未定义的类型而无需将其括在引号中(如 第五章 所述)。如果您当前的版本是 Python 3.7 或更高版本,则添加此 future 导入将允许在类型注释中使用未引用的类型,因此您以后无需重新执行它们。
首先检查在多个项目中共享的库。从这些库中移除阻碍性更改将是一个关键的第一步,因为在完成此步骤之前,您将无法在目标版本上部署任何依赖的应用程序。一旦库与两个版本兼容,它就可以用于迁移项目中。未来,库代码必须保持与当前 Python 版本和目标版本的兼容性:共享库可能是最后一个能够利用目标版本新功能的项目。
独立应用程序将有更早的机会使用目标版本中的新功能。一旦应用程序移除了所有受到破坏性更改影响的代码,请将其提交到您的源代码控制系统中作为跨版本兼容的快照。之后,您可以向应用程序代码添加新功能,并将其部署到支持目标版本的环境中。
如果版本兼容性变化影响到类型注释,你可以使用*.pyi*存根文件来隔离与版本相关的类型信息与源代码。
使用 pyupgrade 进行升级自动化
你可以使用自动化工具(如pyupgrade 包)来自动化大部分代码升级过程中的枯燥工作。pyupgrade 分析 Python 的 ast.parse 函数返回的抽象语法树(AST),以定位问题并对源代码进行修正。你可以通过命令行开关选择特定的目标 Python 版本。
每当你使用自动代码转换时,请审核转换过程的输出。像 Python 这样的动态语言使得完美的转换是不可能的;尽管测试有所帮助,但无法捕捉所有的不完美之处。
多版本测试
确保你的测试尽可能覆盖项目的大部分内容,以便在测试过程中可能会发现版本间错误。目标至少达到 80%的测试覆盖率;超过 90%可能难以达到,因此不要花费过多精力试图达到过高的标准。(模拟, 在“单元测试和系统测试”中提到,可以帮助你增加单元测试覆盖的广度,尽管深度不变。)
tox 包对于帮助你管理和测试多版本代码非常有用。它允许你在多个不同的虚拟环境下测试你的代码,并支持多个 CPython 版本以及 PyPy。
使用受控部署流程
在部署环境中使目标 Python 版本可用,通过应用环境设置来指示应用程序是否应该使用当前或目标 Python 版本运行。持续跟踪并定期向管理团队报告完成百分比。
你应该多久进行一次升级?
PSF 按年度发布 Python 的小版本,每个版本发布后享有五年的支持期。如果采用最新版本减一策略,它为你提供了一个稳定、经过验证的版本来迁移,拥有四年的支持时间窗口(以防未来需要推迟升级)。考虑到四年的时间窗口,每一到两年升级到最新版本减一应该能在升级成本和平台稳定性之间提供合理的平衡。
总结
维护组织系统所依赖的软件版本更新是一种持续的良好“软件卫生”习惯,无论是 Python 还是其他任何开发堆栈都是如此。通过每次只升级一到两个版本的常规升级,你可以将这项工作保持在稳定和可管理的水平上,并且它将成为你组织中公认和重视的活动。
¹ 尽管 Python 3.6 超出了本书涵盖范围的版本,但它引入了一些重要的新特性,我们在此提及它以供历史背景参考。
² 当这种情况发生时,通常是“全体出动”的紧急情况,必须赶快进行升级。正是通过实施稳定持续的 Python 版本升级计划,您才能避免或至少减少这些事件。
附录. Python 3.7 至 3.11 的新功能和更改
下表列出了 Python 版本 3.7 到 3.11 中语言和标准库的变更,这些变更最可能出现在 Python 代码中。使用这些表格来规划您的升级策略,受您代码库中断变更的限制。
以下类型的更改被视为“breaking”,并在最后一列标有 ! 符号:
-
引入新关键字或内置函数(可能与现有 Python 源代码中使用的名称冲突)
-
从标准库模块或内置类型中删除方法
-
更改内置或标准库方法的签名,这种更改不向后兼容(例如删除参数或重命名命名参数)
新警告(包括 DeprecatedWarning)也显示为“breaking”,但在最后一列用 ***** 符号标记。
另请参阅标准库中拟议的弃用和移除表(“死电池”)在 PEP 594 中列出,该表列出了计划在哪些版本中删除的模块(从 Python 3.12 开始)及其推荐的替代方案。
Python 3.7
以下表格总结了 Python 版本 3.7 的更改。更多细节请参阅在线文档中的“Python 3.7 新特性”。
| Python 3.7 | Added | Deprecated | Removed | Breaking change |
|---|---|---|---|---|
| 函数接受 > 255 个参数 | + | |||
| argparse.ArgumentParser.parse_intermixed_args() | + | |||
| ast.literal_eval() 不再评估加法和减法 | ! | |||
| async 和 await 成为保留语言关键字 | + | ! |
| asyncio.all_tasks(), asyncio.create_task(),
asyncio.current_task(),
asyncio.get_running_loop(),
asyncio.Future.get_loop(),
asyncio.Handle.cancelled(),
asyncio.loop.sock_recv_into(),
asyncio.loop.sock_sendfile(),
asyncio.loop.start_tls(),
asyncio.ReadTransport.is_reading(),
asyncio.Server.is_serving(),
asyncio.Server.get_loop(),
asyncio.Task.get_loop(),
asyncio.run()(暂定) | + | | | |
| asyncio.Server 是异步上下文管理器 | + |
|---|
| asyncio.loop.call_soon(), asyncio.loop.call_soon_threadsafe(),
asyncio.loop.call_later(),
asyncio.loop.call_at(), and
asyncio.Future.add_done_callback() 都接受可选的命名 context 参数 | + | | | |
| asyncio.loop.create_server(), asyncio.loop.create_unix_server(),
asyncio.Server.start_serving(),
并且 asyncio.Server.serve_forever() 都接受可选的命名 start_serving 参数 | + | | | |
| asyncio.Task.current_task() 和 asyncio.Task.all_tasks() 已弃用;请使用 asyncio.current_task() 和
asyncio.all_tasks() | | — | | ***** |
| binascii.b2a_uu() 接受命名的 backtick 参数 | + | |||
|---|---|---|---|---|
| bool() 构造函数不再接受命名参数(仅位置参数) | ! | |||
| breakpoint() built-in function | + | ! | ||
| bytearray.isascii() | + | |||
| bytes.isascii() | + | |||
| collections.namedtuple supports default values | + | |||
| concurrent.Futures.ProcessPoolExecutor and concurrent.Futures.ThreadPoolExecutor constructors accept optional initializer and initargs arguments | + |
-
contextlib.AbstractAsyncContextManager, contextlib.asynccontextmanager(),
-
contextlib.AsyncExitStack,
contextlib.nullcontext() | + | | | |
| contextvars module (similar to thread-local vars, with asyncio support) | + | |||
|---|---|---|---|---|
| dataclasses module | + | |||
| datetime.datetime.fromisoformat() | + | |||
| DeprecationWarning shown by default in main module | + | ***** | ||
| dict maintaining insertion order now guaranteed; dict.popitem() returns items in LIFO order | + | |||
| dir() at module level | + | |||
| dis.dis() method accepts named depth argument | + | |||
| float() constructor no longer accepts a named argument (positional only) | ! | |||
| fpectl module removed | X | ! | ||
| from future import annotations enables referencing as-yet-undefined types in type annotations without enclosing in quotes | + | |||
| gc.freeze() | + | |||
| getattr() at module level | + | |||
| hmac.digest() | + | |||
| http.client.HTTPConnection and http.client.HTTPSConnection constructors accept optional blocksize argument | + | |||
| http.server.ThreadingHTTPServer | + |
-
importlib.abc.ResourceReader, importlib.resources module,
-
importlib.source_hash() | + | | | |
| int() constructor no longer accepts a named x argument (positional only; named base argument is still supported) | ! | |||
|---|---|---|---|---|
| io.TextIOWrapper.reconfigure() | + | |||
| ipaddress.IPvNetwork.subnet_of(), ipaddress.IPvNetwork.supernet_of() | + | |||
| list() constructor no longer accepts a named argument (positional only) | ! | |||
| logging.StreamHandler.setStream() | + | |||
| math.remainder() | + | |||
| multiprocessing.Process.close(), multiprocessing.Process.kill() | + | |||
| ntpath.splitunc() removed; use ntpath.splitdrive() | X | ! | ||
| os.preadv(), os.pwritev(), os.register_at_fork() | + | |||
| os.stat_float_times() removed (compatibility function with Python 2; all timestamps in stat result are floats in Python 3) | X | ! | ||
| pathlib.Path.is_mount() | + | |||
| pdb.set_trace() accepts named header argument | + |
- plist.Dict, plist.Plist, and
plist._InternalDict removed | | | X | ! |
| queue.SimpleQueue | + | |||
|---|---|---|---|---|
| re 编译表达式和匹配对象可以使用 copy.copy 和 copy.deepcopy 进行复制 | + | |||
| re.sub() no longer supports unknown escapes of \ and an ASCII letter | X | ! |
| socket.close(), socket.getblocking(), socket.TCP_CONGESTION,
socket.TCP_USER_TIMEOUT,
socket.TCP_NOTSENT_LOWAT(仅限 Linux 平台)| + | | | |
| sqlite3.Connection.backup() | + | |||
|---|---|---|---|---|
| 生成器中的 StopIteration 处理 | + | |||
| str.isascii() | + | |||
| subprocess.run() 的命名参数 capture_output=True 简化了 stdin/stdout 捕获 | + |
| subprocess.run() 和 subprocess.Popen() 的命名参数 text,
通用换行符的别名 | + | | | |
| subprocess.run(), subprocess.call(), 和 subprocess.Popen() 已改进
中断处理 | + | | | |
| sys.breakpointhook(), sys.getandroidapilevel(),
sys.get_coroutine_origin_tracking_depth(),
sys.set_coroutine_origin_tracking_depth() | + | | | |
| time.clock_gettime_ns(), time.clock_settime_ns(),
time.monotonic_ns(),
time.perf_counter_ns(),
time.process_time_ns(), time.time_ns(),
time.CLOCK_BOOTTIME, time.CLOCK_PROF,
time.CLOCK_UPTIME | + | | | |
| time.thread_time() 和 time.thread_time_ns() 用于线程级别的 CPU 计时 | + | |||
|---|---|---|---|---|
| tkinter.ttk.Spinbox | + | |||
| tuple() 构造函数不再接受命名参数(仅限位置参数) | ! |
| types.ClassMethodDescriptorType, types.MethodDescriptorType, types.MethodWrapperType,
types.WrapperDescriptorType | + | | | |
| types.resolve_bases() | + | |||
|---|---|---|---|---|
| uuid.UUID.is_safe | + | |||
| yield 和 yield from 在推导式或生成器表达式中已废弃 | — | ***** | ||
| zipfile.ZipFile 构造函数接受了命名的 compresslevel 参数 | + |
Python 3.8
以下表格总结了 Python 3.8 版本中的变更。更多详情请参见在线文档中的“What’s New in Python 3.8”。
| Python 3.8 | 添加 | 废弃 | 移除 | 破坏性变更 |
|---|---|---|---|---|
| 赋值表达式(:= “海象” 运算符) | + | |||
| 位置参数和命名参数(/ 和 * 参数分隔符) | + | |||
| F-string trailing = for debugging | + | |||
| 对于 str 和 int 字面量的 is 和 is not 测试会发出 SyntaxWarning | ***** | |||
| ast AST 节点的 end_lineno 和 end_col_offset 属性 | + | |||
| ast.get_source_segment() | + | |||
| ast.parse() 现在接受了命名参数 type_comments, mode, 和 feature_version | + | |||
| async REPL 可通过 python -m asyncio 运行 | + | |||
| asyncio 任务可以命名 | + | |||
| asyncio.coroutine 装饰器已废弃 | — | ***** | ||
| asyncio.run() 可直接执行协程 | + | |||
| asyncio.Task.get_coro() | + | |||
| bool.as_integer_ratio() | + | |||
| collections.namedtuple._asdict() returns dict instead of OrderedDict | + | |||
| continue permitted in finally block | + | |||
| cgi.parse_qs, cgi.parse_qsl, and cgi.escape removed; import from urllib.parse and html modules | X | ! | ||
| csv.DictReader returns dicts instead of OrderedDicts | + | |||
| datetime.date.fromisocalendar(), datetime.datetime.fromisocalendar() | + | |||
| dict comprehensions compute key first, value second | ! | |||
| dict and dictviews returned from dict.keys(), dict.values() and dict.items() now iterable with reversed() | + | |||
| fractions.Fraction.as_integer_ratio() | + | |||
| functools.cached_property() decorator (see cautionary notes here and here) | + | |||
| functools.lru_cache can be used as a decorator without () | + | |||
| functools.singledispatchmethod decorator | + | |||
| gettext.pgettext() | + | |||
| importlib.metadata module | + | |||
| int.as_integer_ratio() | + | |||
| itertools.accumulate() accepts named initial argument | + | |||
| macpath module removed | X | ! | ||
| math.comb(), math.dist(), math.isqrt(), math.perm(), math.prod() | + | |||
| math.hypot() added support for > 2 dimensions | + | |||
| multiprocessing.shared_memory module | + | |||
| namedtuple._asdict() returns dict instead of OrderedDict | + | |||
| os.add_dll_directory() on Windows | + | |||
| os.memfd_create() | + | |||
| pathlib.Path.link_to() | + | |||
| platform.popen() removed; use os.popen() | X | ! | ||
| pprint.pp() | + | |||
| pyvenv script removed; use python -m venv | X | ! | ||
| re regular expression patterns support \N{name} escapes | + | |||
| shlex.join() (inverse of shlex.split()) | + | |||
| shutil.copytree() accepts named dirs_exist_ok argument | + | |||
| slots accepts a dict of {name: docstring} | + | |||
| socket.create_server(), socket.has_dualstack_ipv6() | + | |||
| socket.if_nameindex(), socket.if_nametoindex(), and socket.if_indextoname() are all supported on Windows | + | |||
| sqlite3 Cache and Statement objects no longer user-visible | X | ! | ||
| ssl.post_handshake_auth(), ssl.verify_client_post_handshake() | + |
| statistics.fmean(), statistics.geometric_mean(),
statistics.multimode(),
statistics.NormalDist,
statistics.quantiles() | + | | | |
| sys.get_coroutine_wrapper() and sys.set_coroutine_wrapper() removed | X | ! | ||
|---|---|---|---|---|
| sys.unraisablehook() | + | |||
| tarfile.filemode() 已移除 | X | ! |
| threading.excepthook(),threading.get_native_id(),
threading.Thread.native_id | + | | | |
| time.clock() 已移除;请使用 time.perf_counter() | X | ! |
|---|
| tkinter.Canvas.moveto(),tkinter.PhotoImage.transparency_get(),
tkinter.PhotoImage.transparency_set(),
tkinter.Spinbox.selection_from(),
tkinter.Spinbox.selection_present(),
tkinter.Spinbox.selection_range(),
tkinter.Spinbox.selection_to() | + | | | |
| typing.Final,typing.get_args(),typing.get_origin(),typing.Literal,
typing.Protocol,typing.SupportsIndex,typing.TypedDict | + | | | |
| typing.NamedTuple._field_types 已弃用 | — | ***** | ||
|---|---|---|---|---|
| unicodedata.is_normalized() | + | |||
| unittest 支持协程作为测试用例 | + | |||
| unittest.addClassCleanup(),unittest.addModuleCleanup(),unittest.AsyncMock | + |
| xml.etree.Element.getchildren(),xml.etree.Element.getiterator(),xml.etree.ElementTree.getchildren(),以及
xml.etree.ElementTree.getiterator() 已弃用 | | — | | ***** |
| XMLParser.doctype() 已移除 | X | ! | ||
|---|---|---|---|---|
| xmlrpc.client.ServerProxy 接受命名的 headers 参数 | + | |||
| yield 和 return 解包不再需要括号 | + | |||
| yield 和 yield from 不再允许在推导式或生成器表达式中使用 | X | ! |
Python 3.9
下表总结了 Python 版本 3.9 的更改。更多详情,请参阅在线文档中的“Python 3.9 新特性”部分。
| Python 3.9 | 添加 | 弃用 | 移除 | 破坏性变更 |
|---|---|---|---|---|
| 类型注解现在可以在泛型中使用内置类型(例如,list[int] 而非 List[int]) | + |
| array.array.tostring() 和 array.array.fromstring() 已移除;
使用 tobytes() 和 frombytes() | | | X | ! |
| ast.unparse() | + | |||
|---|---|---|---|---|
| asyncio.loop.create_datagram_endpoint() 参数 reuse_address 禁用 | ! |
| asyncio.PidfdChild Watcher,asyncio.shutdown_default_executor(),
asyncio.to_thread() | + | | | |
| asyncio.Task.all_tasks 已移除;请使用 asyncio.all_tasks() | X | ! | ||
|---|---|---|---|---|
| asyncio.Task.current_task 已移除;请使用 asyncio.current_task() | X | ! | ||
| base64.encodestring() 和 base64.decodestring() 已移除;请使用 base64.encodebytes() 和 base64.decodebytes() | X | ! | ||
| concurrent.futures.Executor.shutdown() 接受命名的 cancel_futures 参数 | + |
| curses.get_escdelay(),curses.get_tabsize(),
curses.set_escdelay(),
curses.set_tabsize() | + | | | |
| dict 支持联合运算符 | 和 |= | + |
|---|
| fcntl.F_OFD_GETLK,fcntl.F_OFD_SETLK,
fcntl.F_OFD_SETKLW | + | | | |
| fractions.gcd() 已移除;请使用 math.gcd() | X | ! | ||
|---|---|---|---|---|
| functools.cache() (lightweight/faster version of lru_cache) | + | |||
| gc.is_finalized() | + | |||
| graphlib module with TopologicalSorter class | + | |||
| html.parser.HTMLParser.unescape() removed | X | ! | ||
| imaplib.IMAP4.unselect() | + | |||
| importlib.resources.files() | + | |||
| inspect.BoundArguments.arguments returns dict instead of OrderedDict | + | |||
| ipaddress module does not accept leading zeros in IPv4 address strings | ! | |||
| logging.getLogger('root') returns the root logger | + | ! | ||
| math.gcd() accepts multiple arguments | + | |||
| math.lcm(), math.nextafter(), math.ulp() | + | |||
| multiprocessing.SimpleQueue.close() | + | |||
| nntplib.NNTP.xpath() and nntplib.xgtitle() removed | X | ! | ||
| os.pidfd_open() | + | |||
| os.unsetenv() available on Windows | + | |||
| os.waitstatus_to_exitcode() | + | |||
| parser module deprecated | — | ***** | ||
| pathlib.Path.readlink() | + | |||
| plistlib API removed | X | ! | ||
| pprint supports types.SimpleNamespace | + | |||
| random.choices() with weights argument raises ValueError if weights are all 0 | ! | |||
| random.Random.randbytes() | + | |||
| socket.CAN_RAW_JOIN_FILTERS, socket.send_fds(), socket.recv_fds() | + | |||
| str.removeprefix(), str.removesuffix() | + | |||
| symbol module deprecated | — | ***** | ||
| sys.callstats(), sys.getcheckinterval(), sys.getcounts(), and sys.setcheckinterval() removed | X | ! |
| sys.getcheckinterval() and sys.setcheckinterval() removed;
use sys.getswitchinterval() and
sys.setswitchinterval() | | | X | ! |
| sys.platlibdir attribute | + | |||
|---|---|---|---|---|
| threading.Thread.isAlive() removed; use threading.Thread.is_alive() | X | ! | ||
| tracemalloc.reset_peak() | + | |||
| typing.Annotated type | + | |||
| typing.Literal deduplicates values; equality matching is order independent (3.9.1) | ! | |||
| typing.NamedTuple._field_types removed; use annotations | X | ! | ||
| urllib.parse.parse_qs() and urllib.parse.parse_qsl() accept ; or & query parameter separator, but not both (3.9.2) | ! | |||
| urllib.parse.urlparse() changed handling of numeric paths; a string like 'path:80' is no longer parsed as a path but as a scheme ('path') and a path ('80') | ! |
| with (await asyncio.Condition) and with (yield from asyncio.Condition) removed;
use async with condition | | | X | ! |
| with (await asyncio.lock) and with (yield from asyncio.lock) removed;
use async with lock | | | X | ! |
| with (await asyncio.Semaphore) and with (yield from asyncio.Semaphore) removed; use async with semaphore | X | ! |
|---|
| xml.etree.Element.getchildren(), xml.etree.Element.getiterator(),
xml.etree.ElementTree.getchildren(), and
xml.etree.ElementTree.getiterator() removed | | | X | ! |
| zoneinfo module for IANA time zone support | + |
|---|
Python 3.10
以下表格总结了 Python 3.10 版本的变更。更多详情请参见 在线文档 中的“Python 3.10 新特性”。
| Python 3.10 | Added | Deprecated | Removed | Breaking change |
|---|---|---|---|---|
| Building requires OpenSSL 1.1.1 or newer | + | |||
| Debugging improved with precise line numbers | + | |||
| Structural pattern matching using match, case, and _ soft keywords^(a) | + | |||
| aiter() and anext() built-ins | + | ! | ||
| array.array.index() accepts optional arguments start and stop | + | |||
| ast.literal_eval(s) strips leading spaces and tabs from input string s | + | |||
| asynchat module deprecated | — | ***** | ||
| asyncio functions remove loop parameter | X | ! | ||
| asyncio.connect_accepted_socket() | + | |||
| asyncore module deprecated | — | ***** | ||
| base64.b32hexdecode, base64.b32hexencode | + | |||
| bdb.clearBreakpoints() | + | |||
| bisect.bisect, bisect.bisect_left, bisect.bisect_right, bisect.insort, bisect.insort_left, and bisect.insert_right all accept optional key argument | + | |||
| cgi.log deprecated | — | ***** | ||
| codecs.unregister() | + | |||
| collections module compatibility definitions of ABCs removed; use collections.abc | X | ! | ||
| collections.Counter.total() | + | |||
| contextlib.aclosing() decorator, contextlib.AsyncContextDecorator | + | |||
| curses.has_extended_color_support() | + | |||
| dataclasses.dataclass() decorator accepts optional slots argument | + | |||
| dataclasses.KW_ONLY | + | |||
| distutils deprecated, to be removed in Python 3.12 | — | ***** | ||
| enum.StrEnum | + | |||
| fileinput.input() and fileinput.FileInput accept optional encoding and errors arguments | + | |||
| formatter module removed | X | ! | ||
| glob.glob() and glob.iglob() accept optional root_dir and dir_fd arguments to specify root search directory | + | |||
| importlib.metadata.package_distributions() | + | |||
| inspect.get_annotations() | + | |||
| int.bit_count() | + | |||
| isinstance(obj, (atype, btype)) can be written isinstance(obj, atype|btype) | + | |||
| issubclass(cls, (atype, btype)) 可以写作 issubclass(cls, atype|btype) | + | |||
| itertools.pairwise() | + | |||
| os.eventfd(), os.splice() | + | |||
| os.path.realpath() 接受可选的严格参数 | + | |||
| os.EVTONLY, os.O_FSYNC, os.O_SYMLINK 和 os.O_NOFOLLOW_ANY 在 macOS 上新增 | + | |||
| parser 模块已移除 | X | ! |
| pathlib.Path.chmod() 和 pathlib.Path.stat() 接受可选
follow_symlinks 关键字参数 | + | | | |
| pathlib.Path.hardlink_to() | + | |||
|---|---|---|---|---|
| pathlib.Path.link_to() 已弃用;请使用 hardlink_to() | — | ***** | ||
| platform.freedesktop_os_release() | + | |||
| pprint.pprint() 接受可选的 underscore_numbers 关键字参数 | + | |||
| smtpd 模块已弃用 | — | ***** | ||
| ssl.get_server_certificate 接受可选的超时参数 | + |
| statistics.correlation(), statistics.covariance(),
statistics.linear_regression() | + | | | |
| SyntaxError.end_line_no 和 SyntaxError.end_offset 属性 | + | |||
|---|---|---|---|---|
| sys.flags.warn_default_encoding 发出 EncodingWarning | + | ***** | ||
| sys.orig_argv 和 sys.stdlib_module_names 属性 | + | |||
| threading.excepthook | + | |||
| threading.getprofile(), threading.gettrace() | + | |||
| threading.Thread 将生成的线程名称附加了'(<target.name>)' | + |
| traceback.format_exception(), traceback.format_exception_only(),
和 traceback.print_exception() 签名变更 | | | | ! |
| types.EllipsisType, types.NoneType, types.NotImplementedType | + | |||
|---|---|---|---|---|
| typing 模块包括用于指定 Callable 类型的参数规范变量 | + | |||
| typing.io 模块已弃用;请使用 typing | — | ***** | ||
| typing.is_typeddict() | + | |||
| typing.Literal 去重值;相等匹配是无序的 | ! | |||
| typing.Optional[X] 可以写成 X | None | + | |||
| typing.re 模块已弃用;请使用 typing | — | ***** | ||
| typing.TypeAlias 用于定义显式类型别名 | + | |||
| typing.TypeGuard | + | |||
| typing.Union[X, Y] 可以使用 | 运算符表示为 X | Y | + | |||
| unittest.assertNoLogs() | + | |||
| urllib.parse.parse_qs() 和 urllib.parse.parse_qsl() 接受 ; 或 & 查询参数分隔符,但不能同时使用 | ! | |||
| with 语句接受括号内的上下文管理器: with(ctxmgr, ctxmgr, ...) | + | |||
| xml.sax.handler.LexicalHandler | + | |||
| zip 内建函数接受可选的严格命名参数以进行长度检查 | + |
| zipimport.find_spec(), zipimport.zipimporter.create_module(),
zipimport.zipimporter.exec_module(),
zipimport.zipimporter.invalidate_caches() | + | | | |
| ^(a) 由于这些被定义为 soft 关键字,因此它们不会破坏使用同样名称的现有代码。 |
|---|
Python 3.11
下表总结了 Python 版本 3.11 的更改。更多详情,请参见在线文档中的“Python 3.11 新功能”。
| Python 3.11 | 已添加 | 已弃用 | 已移除 | 破坏性变更 |
|---|
| 在 Python 3.11.0 中发布的安全补丁并回溯到版本 3.7–3.10: int 转换为 str 和 str 转换为
int 在除了 2、4、8、16 或 32 进制外的其他基数中引发
当生成的字符串 > 4,300 位时引发 ValueError(涉及 CVE-2020-10735) | | | | ! |
| 性能改进 | + | |||
|---|---|---|---|---|
| 改进的错误消息 | + | |||
| 新语法:for x in *values | + | |||
| aifc 模块已弃用 | — | ***** | ||
| asynchat 和 asyncore 模块已弃用 | — | ***** |
| asyncio.Barrier,asyncio.start_tls(),
asyncio.TaskGroup | + | | | |
| asyncio.coroutine 装饰器已移除 | X | ! | ||
|---|---|---|---|---|
| asyncio.loop.create_datagram_endpoint() 参数 reuse_address 已移除 | X | ! | ||
| asyncio.TimeoutError 已弃用;使用 TimeoutError | — | ***** | ||
| 音频操作模块已弃用 | — | ***** | ||
| BaseException.add_note(),BaseException.notes 属性 | + |
| binascii.a2b_hqx(),binascii.b2a_hqx(),
binascii.rlecode_hqx(),和
binascii.rledecode_hqx() 已移除 | | | X | ! |
| binhex 模块已移除 | X | ! | ||
|---|---|---|---|---|
| cgi 和 cgitb 模块已弃用 | — | ***** | ||
| chunk 模块已弃用 | — | ***** | ||
| concurrent.futures.ProcessPoolExecutor() max_tasks_per_child 参数 | + | |||
| concurrent.futures.TimeoutError 已弃用;使用内置的 TimeoutError | — | ***** | ||
| contextlib.chdir 上下文管理器(更改当前工作目录然后恢复它) | + | |||
| crypt 模块已弃用 | — | ***** | ||
| dataclasses 对于可变默认值的检查不允许任何非可哈希值(以前允许任何非 dict、list 或 set 的值) | ! | |||
| datetime.UTC 作为 datetime.timezone.utc 的方便别名 | + | |||
| enum.Enum 的 str() 输出仅提供名称 | + | |||
| enum.EnumCheck,enum.FlagBoundary,enum.global_enum() 装饰器,enum.member() 装饰器,enum.nonmember() 装饰器,enum.property,enum.ReprEnum,enum.StrEnum 和 enum.verify() | + | |||
| ExceptionGroups 和 except* | + | |||
| fractions.Fraction 从字符串初始化 | + | |||
| gettext.l*gettext() 方法已移除 | X | ! | ||
| glob.glob() 和 glob.iglob() 接受可选的 include_hidden 参数 | + | |||
| hashlib.file_digest() | + | |||
| imghdr 模块已弃用 | — | ***** | ||
| inspect.formatargspec() 和 inspect.getargspec() 已移除;请使用 inspect.signature() | X | ! | ||
| inspect.getmembers_static()、inspect.ismethodwrapper() | + | |||
| locale.getdefaultlocale() 和 locale.resetlocale() 已弃用 | — | ***** | ||
| locale.getencoding() | + | |||
| logging.getLevelNamesMapping() | + | |||
| mailcap 模块已弃用 | — | ***** | ||
| math.cbrt()(立方根)、math.exp2()(计算 2ⁿ) | + | |||
| msilib 模块已弃用 | — | ***** | ||
| nis 模块已弃用 | — | ***** | ||
| nntplib 模块已弃用 | — | ***** | ||
| operator.call | + | |||
| ossaudiodev 模块已弃用 | — | ***** | ||
| pipes 模块已弃用 | — | ***** | ||
| re 模式语法支持 *+, ++, ?+ 和 {m,n}+ 占有量词,以及 (?>...) 原子分组 | + | |||
| re.template() 已弃用 | — | ***** | ||
| smtpd 模块已弃用 | — | ***** | ||
| sndhdr 模块已弃用 | — | ***** | ||
| spwd 模块已弃用 | — | ***** |
| sqlite3.Connection.blobopen()、sqlite3.Connection.create_window_function()、sqlite3.Connection.deserialize()、
sqlite3.Connection.getlimit()、
sqlite3.Connection.serialize()、
sqlite3.Connection.setlimit() | + | | | |
| sre_compile、sre_constants 和 sre_parse 已弃用 | — | ***** | ||
|---|---|---|---|---|
| statistics.fmean() 可选的 weights 参数 | + | |||
| sunau 模块已弃用 | — | ***** | ||
| sys.exception()(相当于 sys.exc_info()[1]) | + | |||
| telnetlib 模块已弃用 | — | ***** | ||
| time.nanosleep()(仅适用于类 Unix 系统) | + | |||
| tomllib TOML 解析模块 | + |
| typing.assert_never()、typing.assert_type()、
typing.LiteralString、typing.Never、
typing.reveal_type()、typing.Self | + | | | |
| typing.Text 已弃用;请使用 str | — | ***** | ||
|---|---|---|---|---|
| typing.TypedDict 的条目可以标记为 Required 或 NotRequired | + | |||
| typing.TypedDict(a=int, b=str) 形式已弃用 | — | ***** | ||
| unicodedata 更新至 Unicode 14.0.0 | + |
| unittest.enterModuleContext()、unittest.IsolatedAsyncioTestCase.enterAsyncContext()、
unittest.TestCase.enterClassContext()、
unittest.TestCase.enterContext() | + | | | |
| unittest.findTestCases()、unittest.getTestCaseName()、
和 unittest.makeSuite() 已弃用;
请使用 unittest.TestLoader 的方法 |
| uu 模块已弃用 | — | ***** | ||
|---|---|---|---|---|
| with 语句现在对不支持上下文管理器协议的对象抛出 TypeError 异常 | ! | |||
| xdrlib 模块已弃用 | — | ***** | ||
| 添加了 z 字符串格式说明符,用于接近零值的负数标志 | + | |||
| 添加了 zipfile.ZipFile.mkdir() | + | |||
| 在此处添加您自己的笔记: | ||||