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日

目录

摘要

持久数据结构定义为在修改数据时保留数据先前版本的数据结构。此类数据结构实际上是不可变的,因为对其的操作不会就地更新结构,而是始终会产生一个新的更新结构(有关更多详细信息,请参见[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) 操作;
  • get/set 操作的复杂度为 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']

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

散列

frozenmap 实例可以像 tuple 对象一样可散列

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

类型提示

可以使用标准类型提示语法为 frozenmap 进行类型提示

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

实现

提议的 frozenmap 不可变类型使用哈希数组映射 Trie (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) 的性能。
  • dict.copy() 在使用大约 100-200 个项目时效率降低。
../_images/pep-0603-lookup_hamt.png

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

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

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

设计考虑

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

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

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

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

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

提议的 frozenmap 没有这些特性。相反,frozenmap 的 set/get 操作的复杂度为 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

上次修改时间:2023-09-09 17:39:29 GMT