Python 的“冻结”字典
A “frozen” dictionary for Python

原始链接: https://lwn.net/SubscriberLink/1047238/25c270b077849dc0/

## Python 即将获得不可变字典 LWN.net 报道了 PEP 814,该提案旨在向 Python 添加内置的不可变字典类型 `frozendict`。 这解决了长期以来对一种适合并发编程的字典类结构的需求,尤其是在 Python 不断发展的并发特性(如 async、无 GIL 的线程以及子解释器)方面。 `frozendict` 的功能类似于常规字典,但创建后禁止修改。 键必须是不可变的(可哈希的),虽然值可以是可变的,但完全不可变的 `frozendict` 是可哈希的,可以用作字典键或集合元素。 诸如联合 (`|`) 之类的操作会创建新的 `frozendict` 实例。 最初的讨论集中在将现有字典转换为 `frozendict` 的效率上——当前浅拷贝操作是 O(n)。 由于潜在的意外副作用,对 O(1) 就地“冻结”方法的提案进行了辩论,但最终推迟到未来考虑。 该 PEP 已提交给指导委员会,并可能获得批准,从而为更安全、更健壮的并发 Python 代码提供有价值的工具。

相关文章

原文

Welcome to LWN.net

The following subscription-only content has been made available to you by an LWN subscriber. Thousands of subscribers depend on LWN for the best news from the Linux and free software communities. If you enjoy this article, please consider subscribing to LWN. Thank you for visiting LWN.net!

By Jake Edge
December 4, 2025

Dictionaries are ubiquitous in Python code; they are the data structure of choice for a wide variety of tasks. But dictionaries are mutable, which makes them problematic for sharing data in concurrent code. Python has added various concurrency features to the language over the last decade or so—async, free threading without the global interpreter lock (GIL), and independent subinterpreters—but users must work out their own solution for an immutable dictionary that can be safely shared by concurrent code. There are existing modules that could be used, but a recent proposal, PEP 814 ("Add frozendict built-in type"), looks to bring the feature to the language itself.

Victor Stinner announced the PEP that he and Donghee Na have authored in a post to the PEPs category of the Python discussion forum on November 13. The idea has come up before, including in PEP 416, which has essentially the same title as 814 and was authored by Stinner back in 2012. It was rejected by Guido van Rossum at the time, in part due to its target: a Python sandbox that never really panned out.

frozendict

The idea is fairly straightforward: add frozendict as a new immutable type to the language's builtins module. As Stinner put it:

We expect frozendict to be safe by design, as it prevents any unintended modifications. This addition benefits not only CPython's standard library, but also third-party maintainers who can take advantage of a reliable, immutable dictionary type.

While frozendict has a lot in common with the dict built-in type, it is not a subclass of dict; instead, it is a subclass of the base object type. The frozendict() constructor can be used to create one in various ways:

    fd = frozendict()           # empty
    fd = frozendict(a=1, b=2)   # frozen { 'a' : 1, 'b' : 2 }
    d = { 'a' : 1, 'b' : 2 }
    fd = frozendict(d)          # same
    l = [ ( 'a', 1 ), ( 'b', 2 ) ]
    fd = frozendict(l)          # same
    fd2 = frozendict(fd)        # same
    assert d == fd == fd2       # True

As with dictionaries, the keys for a frozendict must be immutable, thus hashable, but the values may or may not be. For example, a list is a legitimate type for a value in either type of dictionary, but it is mutable, making the dictionary as a whole (frozen or not) mutable. However, if all of the values stored in a frozendict are immutable, it is also immutable, so it can be hashed and used in places where that is required (e.g. dictionary keys, set elements, or entries in a functools.lru_cache).

As might be guessed, based on the last line of the example above, frozen dictionaries that are hashable can be compared for equality with other dictionaries of either type. In addition, neither the hash() value nor the equality test depend on the insertion order of the dictionary, though that order is preserved in a frozen dictionary (as it is in the regular variety). So:

    d = { 'a' : 1, 'b' : 2 }
    fd = frozendict(d)
    d2 = { 'b' : 2, 'a' : 1 }
    fd2 = frozendict(d2)
    assert d == d2 == fd == fd2

    # frozendict unions work too, from the PEP
    >>> frozendict(x=1) | frozendict(y=1)
    frozendict({'x': 1, 'y': 1})
    >>> frozendict(x=1) | dict(y=1)
    frozendict({'x': 1, 'y': 1})
For the unions, a new frozen dictionary is created in both cases; the "|=" union-assignment operator also works by generating a new frozendict for the result.

Iteration over a frozendict works as expected; the type implements the collections.abc.Mapping abstract base class, so .items() returns an iterable of key-value tuples, while .keys() and .values() provide the keys and values of the frozen dictionary. For the most part, a frozendict acts like a dict that cannot change; the specific differences between the two are listed in the PEP. It also contains a lengthy list of places in the standard library where a dict could be switched to a frozendict to "enhance safety and prevent unintended modifications".

Discussion

The reaction to the PEP was generally positive, with the usual suggestions for tweaks and more substantive additions to the proposal. Stinner kept the discussion focused on the proposal at hand for the most part. One part of the proposal was troubling to some: converting a dict to a frozendict was described as an O(n) shallow copy. Daniel F Moisset thought that it would make sense to have an in-place transformation that could be O(1) instead. He proposed adding a .freeze() method that would essentially just change the type of a dict object to frozendict.

However, changing the type of an existing object is fraught with peril, as Brett Cannon described:

But now you have made that dictionary frozen for everyone who holds a reference to it, which means side-effects at a distance in a way that could be unexpected (e.g. context switch in a thread and now suddenly you're going to get an exception trying to mutate what was a dict a microsecond ago but is now frozen). That seems like asking for really nasty debugging issues just to optimize some creation time.

The PEP is not aimed at performance, he continued, but is meant to help "lessen bugs in concurrent code". Moisset noted, that dictionaries can already change in unexpected ways via .clear() or .update(), thus the debugging issues already exist. He recognized that the authors may not want to tackle that as part of the PEP, but wanted to try to ensure that an O(1) transformation was not precluded in the future.

Cannon's strong objection is to changing the type of the object directly. Ben Hsing and "Nice Zombies" proposed ways to construct a new frozendict without requiring the shallow copy—thus O(1)—by either moving the hash table to a newly created frozendict, while clearing the dictionary, or by using a copy-on-write scheme for the table. As Steve Dower noted, that optimization can be added later as long as the PEP does not specify that the operation must be O(n), which would be a silly thing to do, but that it sometimes happens "because it makes people stop complaining", he said in a footnote. In light of the discussion, the PEP specifically defers that optimization to a later time, suggesting that it could also be done for other frozen types (tuple and frozenset), perhaps by resurrecting PEP 351 ("The freeze protocol").

On December 1, Stinner announced that the PEP had been submitted to the steering council for pronouncement. Given that Na is on the council, though will presumably recuse himself from deciding on this PEP, he probably has a pretty good sense for how it might be received by the group. So it seems likely that the PEP has a good chance of being approved. The availability of the free-threaded version of the language (i.e. without the GIL) means that more multithreaded Python programs are being created, so having a safe way to share dictionaries between threads will be a boon.





联系我们 contact @ memedata.com