Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 603 – 在 collections 中添加 frozenmap 类型

作者:
Yury Selivanov <yury at edgedb.com>
讨论至:
Discourse 帖子
状态:
草案
类型:
标准跟踪
创建日期:
2019年9月12日
发布历史:
2019年9月12日

目录

摘要

A 持久化数据结构 被定义为一种数据结构,它在数据被修改时保留数据的先前版本。这类数据结构实际上是不可变的,因为对其进行的操作不会原地更新结构,而是始终生成一个新的更新后的结构(有关更多详细信息,请参阅 [0])。

本 PEP 提议向 collections 模块添加一个新的完全持久且不可变的映射类型,名为 frozenmap

frozenmap 的大部分参考实现已在 CPython 中用于实现 contextvars 模块。

基本原理

Python 有两种不可变集合类型:tuplefrozenset。这些类型可用于表示不可变列表和集合。然而,目前还没有表示不可变映射的方法,本 PEP 提议使用 frozenmap 来实现不可变映射

提议的 frozenmap 类型

  • 实现 collections.abc.Mapping 协议,
  • 支持 pickle,并且
  • 提供用于高效创建“修改”版本的 API。

以下用例说明了为什么需要不可变映射

  • 不可变映射是可哈希的,这使得它们可以用作字典键或集合元素。

    此可哈希属性允许用 @functools.lru_cache() 装饰的函数接受不可变映射作为参数。与不可变映射不同,将普通 dict 传递给此类函数会导致错误。

  • 不可变映射可以保存复杂状态。由于不可变映射可以通过引用复制,因此可以高效地实现状态的事务性修改。
  • 不可变映射可用于在线程和异步任务边界之间安全地共享字典。不可变性使推理线程和异步任务变得更容易。

最后,CPython [1] 已经包含了 frozenmap 实现所需 C 代码的主要部分。C 代码已经存在以实现 contextvars 模块(有关更多详细信息,请参阅 PEP 567)。通过公共集合类型公开此 C 代码极大地增加了代码的用户数量。这通过发现错误和提高性能来提高代码质量,如果没有 frozenmap 集合,这将非常具有挑战性,因为大多数程序间接使用 contextvars 模块。

规范

一个新的公共不可变类型 frozenmap 被添加到 collections 模块。

构建

frozenmap 实现了一个类似 dict 的构造 API

  • frozenmap() 创建一个新的空不可变映射;
  • frozenmap(**kwargs)**kwargs 创建映射,例如 frozenmap(x=10, y=0, z=-1)
  • frozenmap(collection) 从传入的 collection 对象创建映射。传入的 collection 对象可以是
    • 一个 dict
    • 另一个 frozenmap
    • 一个带有 items() 方法的对象,该方法预计返回一系列键/值元组,或
    • 一个键/值元组的可迭代对象。

数据访问

frozenmap 实现了 collection.abc.Mapping 协议。因此,getter、成员检查和迭代的工作方式与 dict 相同

m = frozenmap(foo='bar')

assert m['foo'] == 'bar'
assert m.get('foo') == 'bar'
assert 'foo' in m

assert 'baz' not in m
assert m.get('baz', 'missing') == 'missing'

assert m == m
assert m != frozenmap()  # m is not equal to an empty frozenmap

assert len(m) == 1

# etc.

修改

frozenmap 实例是不可变的。尽管如此,可以高效地生成不可变实例的修改副本

修改操作的复杂度为 O(log N),并且由于结构共享的使用,生成的 frozenmap 副本通常消耗很少的额外内存(有关更多详细信息,请参阅 [6])。

frozenmap.including(key, value)

该方法创建一个新的 frozenmap 副本,其中包含一个新的/

m = frozenmap(foo=1)
m2 = m.including('bar', 100)

print(m)   # will print frozenmap({'foo': 1})
print(m2)  # will print frozenmap({'foo': 1, 'bar': 100})

frozenmap.excluding(key)

该方法生成 frozenmap 的副本,其中不包含已删除的

m = frozenmap(foo=1, bar=100)

m2 = m.excluding('foo')

print(m)   # will print frozenmap({'foo': 1, 'bar': 100})
print(m2)  # will print frozenmap({'bar': 1})

m3 = m.excluding('spam')  # will throw a KeyError('spam')

frozenmap.union(mapping=None, **kw)

该方法生成 frozenmap 的副本,并为创建的副本添加或修改多个键/值。该方法的签名与 frozenmap 构造函数的签名匹配

m = frozenmap(foo=1)

m2 = m.union({'spam': 'ham'})
print(m2)  # will print frozenmap({'foo': 1, 'spam': 'ham'})

m3 = m.union(foo=100, y=2)
print(m3)  # will print frozenmap({'foo': 100, 'y': 2})

print(m)   # will print frozenmap({'foo': 1})

调用 union() 方法添加/替换 N 个键比调用 including() 方法 N 次更高效。

frozenmap.mutating()

该方法允许高效复制应用了多次修改的 frozenmap 实例。当 frozenmap 包含数千个键/值对,并且需要在代码的性能关键部分更新其中许多键/值对时,此方法特别有用。

frozenmap.mutating() 方法返回 frozenmap 对象的类字典的可变副本:一个 collections.FrozenMapCopy 实例。

FrozenMapCopy 对象

  • 是它们创建自的 frozenmap 实例的数据的写时复制视图;
  • 是可变的,尽管对其进行的任何修改都不会影响它们创建自的 frozenmap 实例;
  • 可以传递给 frozenmap 构造函数;从 FrozenMapCopy 对象创建 frozenmap 是 O(1) 操作;
  • 获取/设置操作的复杂度为 O(log N);创建它们的复杂度为 O(1);
  • 有一个 FrozenMapCopy.close() 方法,可以防止对数据进行任何进一步的访问/修改;
  • 可以用作上下文管理器。

以下示例说明了 mutating() 如何与上下文管理器一起使用

numbers = frozenmap((i, i ** 2) for i in range(1_000_000))

with numbers.mutating() as copy:
    for i in numbers:
        if not (numbers[i] % 997):
            del copy[i]

    numbers_without_997_multiples = frozenmap(copy)

    # at this point, *numbers* still has 1_000_000 key/values, and
    # *numbers_without_997_multiples* is a copy of *numbers* without
    # values that are multiples of 997.

    for i in numbers:
        if not (numbers[i] % 593):
            del copy[i]

    numbers_without_593_multiples = frozenmap(copy)

    print(copy[10])  # will print 100.

print(copy[10])  # This will throw a ValueError as *copy*
                 # has been closed when the "with" block
                 # was executed.

迭代

由于 frozenmap 实现了标准的 collections.abc.Mapping 协议,因此支持所有预期的迭代方法

assert list(m) == ['foo']
assert list(m.items()) == [('foo', 'bar')]
assert list(m.keys()) == ['foo']
assert list(m.values()) == ['bar']

frozenmap 中的迭代,与 dict 不同,不保留插入顺序。

哈希

frozenmap 实例可以像 tuple 对象一样是可哈希的

hash(frozenmap(foo='bar'))  # works
hash(frozenmap(foo=[]))     # will throw an error

类型标注

可以使用标准的类型标注来表示 frozenmaps

m: frozenmap[str, int] = frozenmap()

实施

提议的 frozenmap 不可变类型使用哈希数组映射树(HAMT)数据结构。函数式编程语言,如 Clojure,使用 HAMT 来高效地实现不可变哈希表、向量和集合。

HAMT

HAMT 的关键设计契约是当给定键的哈希值时,保证可预测的。对于一对的哈希值可以用来确定在哈希映射树中的位置。

使用 HAMT 实现的不可变映射在 set()get() 操作中具有 O(log N) 的性能。这种效率之所以可能,是因为修改操作只影响树的一个分支,从而可以重用未修改的分支,因此避免了复制未修改的数据。

[5] 中阅读更多关于 HAMT 的信息。CPython 实现 [1] 也有一个相当详细的算法描述。

性能

../_images/pep-0603-hamt_vs_dict.png

图 1。基准测试代码可以在这里找到:[3]

以上图表表明

  • 使用 HAMT 实现的 frozenmap 对所有基准测试的字典大小都显示出接近 O(1) 的性能。
  • 当使用大约 100-200 个项目时,dict.copy() 变得效率较低。
../_images/pep-0603-lookup_hamt.png

图 2。基准测试代码可以在这里找到:[4]

图 2 比较了 dict 与基于 HAMT 的不可变映射的查找成本。HAMT 查找时间平均比 Python 字典查找慢约 30%。这种性能差异的存在是因为遍历浅层树不如在扁平连续数组中查找高效。

此外,引用 [6]:“[使用 HAMT] 意味着在实践中,虽然对持久化哈希数组映射树的插入、删除和查找具有 O(log n) 的计算复杂度,但对于大多数应用程序来说,它们实际上是常数时间,因为需要极其大量的条目才能使任何操作花费超过十几个步骤。”

设计考虑

为什么是 “frozenmap” 而不是 “FrozenMap”

小写的“frozenmap”与内置的 frozenset 以及 collections.defaultdict 等类型很好地呼应。

为什么是 “frozenmap” 而不是 “frozendict”

“Dict”在 Python 中有非常具体的含义

  • dict 是 abc.MutableMapping 的具体实现,具有 O(1) 的获取和设置操作(frozenmap 具有 O(log N) 的复杂度);
  • Python 字典保留插入顺序。

提议的 frozenmap 不具备上述属性。相反,frozenmap 的设置/获取操作成本为 O(log N),并且它只实现了 abc.Mapping 协议。

实施

提议的 frozenmap 类型的完整实现可在 [2] 中找到。该包包括 C 和纯 Python 的类型实现。

另请参阅 CPython 项目树中 HAMT 集合的实现:[1]

参考资料

致谢

我感谢 Carol Willing、Łukasz Langa、Larry Hastings 和 Guido van Rossum 对本 PEP 的反馈、想法、编辑和讨论。


来源:https://github.com/python/peps/blob/main/peps/pep-0603.rst

最后修改:2025-02-01 08:59:27 GMT