“冻结字典”是什么?

224
  • 一个冻结集合是 frozenset。
  • 一个冻结列表可以是元组。
  • 那么一个冻结字典会是什么样子呢?它是一个不可变、可哈希的字典。

我猜它可能类似于 collections.namedtuple,但更像是一个冻结键的字典(半冻结字典)。不是吗?

一个“frozendict”应该是一个冻结的字典,它应该有keysvaluesget等方法,并支持infor等操作。

更新:
* 在这里链接到了:https://www.python.org/dev/peps/pep-0603


1
PEP603是一项建议,而不是实现:https://peps.python.org/pep-0603/ 此外,在PEP603中请注意:“frozenmap的复杂度为O(log N)” - tommy.carstensen
...笑死了?frozenmapO(log N)复杂度简直是疯了。这是个笑话吗?这可不是个笑话,对吧?仅凭这一点,PEP 603就应该被轻松地驳回。不需要进一步的同行评审。已经存在一个简单的纯Python实现的frozendict,保持着O(1)的复杂度。我叹气。 - undefined
14个回答

150

Python没有内置的frozendict类型。虽然它可能比frozenset更有用,但事实证明这种类型并不经常使用。

最常需要这种类型的原因是为了对具有未知参数的函数调用进行记忆化。存储一个可哈希等价的字典(其中值是可哈希的)的最常见解决方案是类似于tuple(sorted(kwargs.items()))的东西。

这取决于排序结果不会太疯狂。Python不能确保排序会产生合理的结果。 (但它也不能保证其他太多,所以不要太担心。)


你可以很容易地创建一些类似于字典的包装器。它可能看起来像下面这样:

import collections

class FrozenDict(collections.Mapping):
    """Don't forget the docstrings!!"""
    
    def __init__(self, *args, **kwargs):
        self._d = dict(*args, **kwargs)
        self._hash = None

    def __iter__(self):
        return iter(self._d)

    def __len__(self):
        return len(self._d)

    def __getitem__(self, key):
        return self._d[key]

    def __hash__(self):
        # It would have been simpler and maybe more obvious to 
        # use hash(tuple(sorted(self._d.iteritems()))) from this discussion
        # so far, but this solution is O(n). I don't know what kind of 
        # n we are going to run into, but sometimes it's hard to resist the 
        # urge to optimize when it will gain improved algorithmic performance.
        if self._hash is None:
            hash_ = 0
            for pair in self.items():
                hash_ ^= hash(pair)
            self._hash = hash_
        return self._hash

它应该能够很好地工作:
>>> x = FrozenDict(a=1, b=2)
>>> y = FrozenDict(a=1, b=2)
>>> x is y
False
>>> x == y
True
>>> x == {'a': 1, 'b': 2}
True
>>> d = {x: 'foo'}
>>> d[y]
'foo'

11
我不知道人们担心这种情况的线程安全级别是多少,但在这方面,你的__hash__方法可以稍微改进一下。只需在计算哈希时使用一个临时变量,并在获得最终值后才设置self._hash。这样,当第一个线程正在计算时,另一个线程获取哈希值将仅执行冗余计算,而不是得到错误的值。 - Jeff DQ
29
通常情况下,所有代码都不是线程安全的,为了安全地使用该代码,您应将其包装在一些同步结构中。此外,您关于线程安全的特定概念依赖于对象属性赋值的原子性,但这并不能得到保证。 - Devin Jeanpierre
9
@Anentropic,那完全不是真的。 - Mike Graham
28
注意:这个“FrozenDict”并不一定是冻结的。 如果你将可变列表放作为值,那么哈希会抛出一个错误。 这并没有什么问题,但用户应该知道这点。另外一件事:这个哈希算法选择得很差,很容易发生哈希冲突。例如{'a':'b'}与{'b':'a'}的哈希值相同,而{'a':1,'b':2}与{'a':2,'b':1}的哈希值也相同。更好的选择是 self._hash ^= hash((key, value))。 - Steve Byrnes
8
如果在不可变对象中添加一个可变的条目,可能会有两种行为:在创建对象时抛出错误,或在对对象进行哈希时抛出错误。元组采用后一种方法,frozenset采用前一种方法。考虑到各方面因素,我认为你做出了正确的决定选择前一种方法。然而,我认为人们可能会发现FrozenDict和frozenset具有相似的名称,并得出它们应该有类似行为的结论。因此,我认为值得向人们提醒这种差异。 :-) - Steve Byrnes
显示剩余23条评论

101

有趣的是,尽管我们有很少使用的frozenset,但仍然没有冻结映射。这个想法在PEP 416 -- 添加内置类型frozendict中被拒绝了。这个想法可能会在以后的Python版本中重新审视,请参见PEP 603 -- 在collections中添加frozenmap类型

所以Python 2的解决方案是:

def foo(config={'a': 1}):
    ...

仍然似乎是通常的情况:

def foo(config=None):
    if config is None:
        config = {'a': 1}  # default config
    ...

在Python 3中,你可以使用这个选项:

from types import MappingProxyType

default_config = {'a': 1}
DEFAULTS = MappingProxyType(default_config)

def foo(config=DEFAULTS):
    ...

现在,默认配置可以动态更新,但是如果想让它保持不可变状态,可以传递代理对象来实现。

因此,对default_config的更改将按预期更新DEFAULTS,但不能直接写入映射代理对象本身。

诚然,这并不是一个真正的“不可变、可散列字典”,但对于某些使用frozendict的情况可能是个不错的替代品。


5
为什么要将代理存储在模块变量中?为什么不直接使用def foo(config=MappingProxyType({'a': 1})):?你的示例仍然允许通过default_config进行全局修改。 - jpmc26

25
假设字典的键和值本身是不可变的(例如字符串),那么:
>>> d
{'forever': 'atones', 'minks': 'cards', 'overhands': 'warranted', 
 'hardhearted': 'tartly', 'gradations': 'snorkeled'}
>>> t = tuple((k, d[k]) for k in sorted(d.keys()))
>>> hash(t)
1524953596

1
这是一个很好的、规范的、不可变的字典表示(除了疯狂的比较行为会搞乱排序)。 - Mike Graham
6
完全同意,但我会让我的帖子保留下来,作为一个例子,说明有时候还有更好的方式。 - msw
19
更好的方法是将其放入frozenset中,它不需要为键或值定义一致的顺序。 - asmeurer
12
唯一的问题在于:你再也没有映射了。这也是一开始采用冻结字典的原因所在。 - Mad Physicist
3
回到字典时,这种方法非常优雅。只需使用dict(t)即可。 - codythecoder
根据你的具体操作,排序可能是不必要的。 - martineau

24

虽然不存在fronzedict,但您可以使用在Python 3.3中添加到标准库中的MappingProxyType

>>> from types import MappingProxyType
>>> foo = MappingProxyType({'a': 1})
>>> foo
mappingproxy({'a': 1})
>>> foo['a'] = 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'mappingproxy' object does not support item assignment
>>> foo
mappingproxy({'a': 1})

3
有一个警告信息:TypeError: can't pickle mappingproxy objects - Radu
12
问题在于 MappingProxyType 仍然是不可哈希的。 - Jab

18

安装frozendict

pip install frozendict

使用它!

from frozendict import frozendict

def smth(param = frozendict({})):
    pass

这也很好,因为它是可哈希的,并且可以从frozendict继承作为基类。如果使用MappingProxyType,则两者都不可能。 - spacether
哦,我希望它有超过40个Github星,再加上我的一个。 - matanster

13

每当我编写像这样的函数时,我就会想到frozendict:

def do_something(blah, optional_dict_parm=None):
    if optional_dict_parm is None:
        optional_dict_parm = {}

9
每次我看到这样的评论,我都可以确定我在某处出了错,并将{}作为默认值,于是我会回去查看我最近编写的代码。 - Ryan Hiebert
2
是的,这是一个很恶心的陷阱,每个人迟早都会遇到。 - Mark Visser
10
更为通俗易懂的表达:如果optional_dict_parm为空,则将其设为一个空字典 {}。 - Emmanuel
2
在这种情况下,您可以使用types.MappingProxyType({})作为参数的默认值。 - GingerPlusPlus
4
您希望 is None 检查可以捕获假值参数,例如 MappingProxyType({}),或者如果有人打错字,例如 0 - wjandrea
显示剩余3条评论

11
这是我一直在使用的代码。我对frozenset进行了子类化。其优点如下:
  1. 这是一个真正的不可变对象。不需要依赖未来用户和开发人员的良好行为。
  2. 很容易在常规字典和冻结字典之间进行转换。FrozenDict(orig_dict) --> 冻结字典。dict(frozen_dict) --> 常规字典。
更新于2015年1月21日:我在2014年发布的原始代码使用了for循环来查找匹配的键。那样非常慢。现在,我已经编写了一个利用frozenset哈希特性的实现。键值对存储在特殊容器中,其中__hash__和__eq__函数仅基于键。与我在2014年8月发布的内容不同,此代码也已正式进行单元测试。
MIT-style许可证。
if 3 / 2 == 1:
    version = 2
elif 3 / 2 == 1.5:
    version = 3

def col(i):
    ''' For binding named attributes to spots inside subclasses of tuple.'''
    g = tuple.__getitem__
    @property
    def _col(self):
        return g(self,i)
    return _col

class Item(tuple):
    ''' Designed for storing key-value pairs inside
        a FrozenDict, which itself is a subclass of frozenset.
        The __hash__ is overloaded to return the hash of only the key.
        __eq__ is overloaded so that normally it only checks whether the Item's
        key is equal to the other object, HOWEVER, if the other object itself
        is an instance of Item, it checks BOTH the key and value for equality.

        WARNING: Do not use this class for any purpose other than to contain
        key value pairs inside FrozenDict!!!!

        The __eq__ operator is overloaded in such a way that it violates a
        fundamental property of mathematics. That property, which says that
        a == b and b == c implies a == c, does not hold for this object.
        Here's a demonstration:
            [in]  >>> x = Item(('a',4))
            [in]  >>> y = Item(('a',5))
            [in]  >>> hash('a')
            [out] >>> 194817700
            [in]  >>> hash(x)
            [out] >>> 194817700
            [in]  >>> hash(y)
            [out] >>> 194817700
            [in]  >>> 'a' == x
            [out] >>> True
            [in]  >>> 'a' == y
            [out] >>> True
            [in]  >>> x == y
            [out] >>> False
    '''

    __slots__ = ()
    key, value = col(0), col(1)
    def __hash__(self):
        return hash(self.key)
    def __eq__(self, other):
        if isinstance(other, Item):
            return tuple.__eq__(self, other)
        return self.key == other
    def __ne__(self, other):
        return not self.__eq__(other)
    def __str__(self):
        return '%r: %r' % self
    def __repr__(self):
        return 'Item((%r, %r))' % self

class FrozenDict(frozenset):
    ''' Behaves in most ways like a regular dictionary, except that it's immutable.
        It differs from other implementations because it doesn't subclass "dict".
        Instead it subclasses "frozenset" which guarantees immutability.
        FrozenDict instances are created with the same arguments used to initialize
        regular dictionaries, and has all the same methods.
            [in]  >>> f = FrozenDict(x=3,y=4,z=5)
            [in]  >>> f['x']
            [out] >>> 3
            [in]  >>> f['a'] = 0
            [out] >>> TypeError: 'FrozenDict' object does not support item assignment

        FrozenDict can accept un-hashable values, but FrozenDict is only hashable if its values are hashable.
            [in]  >>> f = FrozenDict(x=3,y=4,z=5)
            [in]  >>> hash(f)
            [out] >>> 646626455
            [in]  >>> g = FrozenDict(x=3,y=4,z=[])
            [in]  >>> hash(g)
            [out] >>> TypeError: unhashable type: 'list'

        FrozenDict interacts with dictionary objects as though it were a dict itself.
            [in]  >>> original = dict(x=3,y=4,z=5)
            [in]  >>> frozen = FrozenDict(x=3,y=4,z=5)
            [in]  >>> original == frozen
            [out] >>> True

        FrozenDict supports bi-directional conversions with regular dictionaries.
            [in]  >>> original = {'x': 3, 'y': 4, 'z': 5}
            [in]  >>> FrozenDict(original)
            [out] >>> FrozenDict({'x': 3, 'y': 4, 'z': 5})
            [in]  >>> dict(FrozenDict(original))
            [out] >>> {'x': 3, 'y': 4, 'z': 5}   '''

    __slots__ = ()
    def __new__(cls, orig={}, **kw):
        if kw:
            d = dict(orig, **kw)
            items = map(Item, d.items())
        else:
            try:
                items = map(Item, orig.items())
            except AttributeError:
                items = map(Item, orig)
        return frozenset.__new__(cls, items)

    def __repr__(self):
        cls = self.__class__.__name__
        items = frozenset.__iter__(self)
        _repr = ', '.join(map(str,items))
        return '%s({%s})' % (cls, _repr)

    def __getitem__(self, key):
        if key not in self:
            raise KeyError(key)
        diff = self.difference
        item = diff(diff({key}))
        key, value = set(item).pop()
        return value

    def get(self, key, default=None):
        if key not in self:
            return default
        return self[key]

    def __iter__(self):
        items = frozenset.__iter__(self)
        return map(lambda i: i.key, items)

    def keys(self):
        items = frozenset.__iter__(self)
        return map(lambda i: i.key, items)

    def values(self):
        items = frozenset.__iter__(self)
        return map(lambda i: i.value, items)

    def items(self):
        items = frozenset.__iter__(self)
        return map(tuple, items)

    def copy(self):
        cls = self.__class__
        items = frozenset.copy(self)
        dupl = frozenset.__new__(cls, items)
        return dupl

    @classmethod
    def fromkeys(cls, keys, value):
        d = dict.fromkeys(keys,value)
        return cls(d)

    def __hash__(self):
        kv = tuple.__hash__
        items = frozenset.__iter__(self)
        return hash(frozenset(map(kv, items)))

    def __eq__(self, other):
        if not isinstance(other, FrozenDict):
            try:
                other = FrozenDict(other)
            except Exception:
                return False
        return frozenset.__eq__(self, other)

    def __ne__(self, other):
        return not self.__eq__(other)


if version == 2:
    #Here are the Python2 modifications
    class Python2(FrozenDict):
        def __iter__(self):
            items = frozenset.__iter__(self)
            for i in items:
                yield i.key

        def iterkeys(self):
            items = frozenset.__iter__(self)
            for i in items:
                yield i.key

        def itervalues(self):
            items = frozenset.__iter__(self)
            for i in items:
                yield i.value

        def iteritems(self):
            items = frozenset.__iter__(self)
            for i in items:
                yield (i.key, i.value)

        def has_key(self, key):
            return key in self

        def viewkeys(self):
            return dict(self).viewkeys()

        def viewvalues(self):
            return dict(self).viewvalues()

        def viewitems(self):
            return dict(self).viewitems()

    #If this is Python2, rebuild the class
    #from scratch rather than use a subclass
    py3 = FrozenDict.__dict__
    py3 = {k: py3[k] for k in py3}
    py2 = {}
    py2.update(py3)
    dct = Python2.__dict__
    py2.update({k: dct[k] for k in dct})

    FrozenDict = type('FrozenDict', (frozenset,), py2)

3
请注意,通过在此处发布内容,您还将其授权使用CC BY-SA 3.0许可。至少这是普遍观点。我猜这个法律基础是在您首次注册时同意了某些条款和条件。 - Evgeni Sergeev
2
我在努力思考一种在没有字典的情况下查找密钥散列值的方法时,差点把我的大脑弄破了。重新定义Item的哈希值为键的哈希值是一个很巧妙的技巧! - clacke
1
不幸的是,diff(diff({key})) 的运行时间仍然与 FrozenDict 的大小成线性关系,而常规字典的访问时间在平均情况下是恒定的。 - Dennis

6

继承 dict

我在GitHub上看到了这种模式,想提一下:

class FrozenDict(dict):
    def __init__(self, *args, **kwargs):
        self._hash = None
        super(FrozenDict, self).__init__(*args, **kwargs)

    def __hash__(self):
        if self._hash is None:
            self._hash = hash(tuple(sorted(self.items())))  # iteritems() on py2
        return self._hash

    def _immutable(self, *args, **kws):
        raise TypeError('cannot change object - object is immutable')

    # makes (deep)copy alot more efficient
    def __copy__(self):
        return self

    def __deepcopy__(self, memo=None):
        if memo is not None:
            memo[id(self)] = self
        return self

    __setitem__ = _immutable
    __delitem__ = _immutable
    pop = _immutable
    popitem = _immutable
    clear = _immutable
    update = _immutable
    setdefault = _immutable

示例用法:

d1 = FrozenDict({'a': 1, 'b': 2})
d2 = FrozenDict({'a': 1, 'b': 2})
d1.keys() 
assert isinstance(d1, dict)
assert len(set([d1, d2])) == 1  # hashable

优点

  • 支持get()keys()items()(在py2中为iteritems())以及所有来自dict的好处,无需显式实现它们。
  • 内部使用dict,这意味着性能更好(dict在CPython中是用C语言编写的)。
  • 简洁优雅且没有黑魔法。
  • isinstance(my_frozen_dict, dict)返回True - 虽然Python鼓励鸭子类型,但许多软件包使用isinstance(),这可以节省许多调整和自定义。

缺点

  • 任何子类都可以覆盖或在内部访问它(你无法真正100%保护Python中的某些东西,你应该信任你的用户并提供良好的文档)。
  • 如果您关心速度,您可能希望使__hash__更快一些。

2
我在另一个线程中进行了速度比较,结果表明,覆盖__setitem__并继承dict与许多其他替代方案相比,速度非常快。 - Torxed
1
你可以继承自collections.UserDict。它就是为此而设计的,普通字典在子类化时存在许多缺陷。 - Andrey

6

您可以使用来自 utilspie 包的 frozendict,如下:

>>> from utilspie.collectionsutils import frozendict

>>> my_dict = frozendict({1: 3, 4: 5})
>>> my_dict  # object of `frozendict` type
frozendict({1: 3, 4: 5})

# Hashable
>>> {my_dict: 4}
{frozendict({1: 3, 4: 5}): 4}

# Immutable
>>> my_dict[1] = 5
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/mquadri/workspace/utilspie/utilspie/collectionsutils/collections_utils.py", line 44, in __setitem__
    self.__setitem__.__name__, type(self).__name__))
AttributeError: You can not call '__setitem__()' for 'frozendict' object

根据文档:

frozendict(dict_obj): 接受字典类型的obj并返回可哈希和不可变的字典


5
namedtuple 的主要缺点是需要在使用前指定,因此对于单次使用的情况不太方便。但是,有一个实用的解决方法可以处理许多这样的情况。假设您想要一个不可变的等效于以下字典的东西:
MY_CONSTANT = {
    'something': 123,
    'something_else': 456
}

这可以这样模拟:
from collections import namedtuple

MY_CONSTANT = namedtuple('MyConstant', 'something something_else')(123, 456)

甚至可以编写一个辅助函数来自动完成这个过程:
def freeze_dict(data):
    from collections import namedtuple
    keys = sorted(data.keys())
    frozen_type = namedtuple(''.join(keys), keys)
    return frozen_type(**data)

a = {'foo':'bar', 'x':'y'}
fa = freeze_dict(data)
assert a['foo'] == fa.foo

当然,这只适用于扁平(flat)字典,但实现递归版本应该不难。

2
和其他元组答案一样的问题:你必须使用 getattr(fa, x) 而不是 fa[x],因为没有 keys 方法在你的指尖上,并且所有其他映射可能需要的原因。 - Mad Physicist

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接