什么是"冻结的字典"?

dug*_*res 140 python dictionary immutability data-structures

  • 冻结集是冻结集.
  • 冻结列表可以是元组.
  • 冻结的词典会是什么?一个不可变的,可洗的字典.

我想它可能是类似的collections.namedtuple,但这更像是一个冻结的词典(一个半冻结的词典).不是吗?

A"frozendict"应该是一个冰冻的字典,它应该有keys,values,get,等,并支持in,for等等.

Mik*_*ham 106

Python没有内置的frozendict类型.事实证明这不会经常使用(尽管它仍然可能更有用frozenset).

想要这种类型的最常见原因是当memoizing函数调用具有未知参数的函数时.最常见的解决方案是存储一个可清除的dict(其中值是可清除的)的类似的东西tuple(sorted(kwargs.iteritems())).

这取决于排序不是有点疯狂.Python不能肯定地承诺排序会在这里产生合理的结果.(但它不能承诺太多其他的东西,所以不要太过分.)


你可以轻松地制作某种类似于dict的包装器.它可能看起来像

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:
            self._hash = 0
            for pair in self.iteritems():
                self._hash ^= hash(pair)
        return self._hash
Run Code Online (Sandbox Code Playgroud)

它应该很棒:

>>> 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'
Run Code Online (Sandbox Code Playgroud)

  • @Jeff作为一项规则,所有代码都是不是线程安全的,你应该将它包装在一些同步结构中,以便安全地使用该代码.此外,您对线程安全的特定概念依赖于对象属性赋值的原子性,这远远不能保证. (20认同)
  • 警告:这个"FrozenDict"不一定是冻结的.没有什么可以阻止你将可变列表作为值,在这种情况下,散列将引发错误.这没有什么不妥,但用户应该知道.另一件事:这种哈希算法选择不当,很容易发生哈希冲突.例如{'a':'b'}哈希与{'b':'a'}相同,而{'a':1,'b':2}哈希与{'a':2,'相同b':1}.更好的选择是self._hash ^ = hash((key,value)) (13认同)
  • @Anentropic,这根本不是真的. (9认同)
  • 我不知道人们对这种事情担心什么程度的线程安全,但在这方面你的`__hash__`方法可能会略有改进.在计算哈希值时只需使用临时变量,只有在得到最终值后才设置"self._hash".这样,另一个线程在第一个计算时获取哈希将简单地进行冗余计算,而不是获取不正确的值. (6认同)
  • 如果在不可变对象中添加可变条目,则两种可能的行为是在创建对象时抛出错误,或者在散列对象时抛出错误.元组做后者,冷冻组做前者.考虑到所有事情,我绝对认为你采取后一种方法做出了一个很好的决定.尽管如此,我认为人们可能会看到FrozenDict和frozenset有相似的名字,并得出结论他们应该表现得相似.所以我认为值得警告人们这种差异.:-) (6认同)
  • *没有什么可以阻止你将可变列表作为一个值,在这种情况下,散列会抛出一个错误.*我认为这是一个功能 - 你更喜欢什么?其他不可变集合也是如此,例如元组. (3认同)
  • *另一件事:这种哈希算法选择不当,很容易发生哈希冲突.例如{'a':'b'}哈希与{'b':'a'}相同,而{'a':1,'b':2}哈希与{'a':2,'相同b':1}.更好的选择是self._hash ^ = hash((key,value))*你是对的,后一个例子特别引人注目.我编辑了这篇文章. (3认同)
  • 我知道这是一个利基用途,但我已经发现了大量使用离散数学,在集合系列上缓存函数.与扫描长元组相比,它们提供快速查找,甚至是分类元组的二等分.他们是我最喜欢的语言之一. (3认同)
  • @wallacoloo,`collections.Mapping` 提供了`__eq__` 和`__ne__` 混合,**这是我们想要的**。我们也*想要*哈希。相等的身份比较和哈希的 id 的默认值意味着保存相同数据的两个 `FrozenDict` 对象不能有用地用作 dict 键。我添加了一些示例代码来展示它是如何很好地工作的。 (2认同)
  • 我个人发现冻结集对于描述图中的无向边很有用。如果没有它们,我将不得不不断检查元组的顺序。 (2认同)
  • 我不同意frozendict 不会经常有用。我们切换了很多地方来使用frozendicts,它消除了很多错误。例如,[默认参数错误](/sf/ask/666852581/) 可以可以用`def function(defaults=frozendict())`很好地解决。此外,像算法参数化、在系统组件之间发送字典、没有锁的多线程等之类的事情都可以使用frozendicts 很好且安全地完成。 (2认同)

wim*_*wim 51

奇怪的是,尽管我们frozenset在python中很少有用,但仍然没有冻结映射.该想法在PEP 416中被驳回.

所以python 2解决方案:

def foo(config={'a': 1}):
    ...
Run Code Online (Sandbox Code Playgroud)

似乎仍然有些蹩脚:

def foo(config=None):
    if config is None:
        config = default_config = {'a': 1}
    ...
Run Code Online (Sandbox Code Playgroud)

在python3您的选择这个:

from types import MappingProxyType

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

def foo(config=DEFAULTS):
    ...
Run Code Online (Sandbox Code Playgroud)

现在,默认配置可以动态更新,但是通过传递代理而不希望它是不可变的.

因此,更改将按预期default_config更新DEFAULTS,但您无法写入映射代理对象本身.

不可否认,它与"不可变的,可以清洗的字典"并不完全相同 - 但考虑到我们可能需要冻结的相同类型的用例,它是一个不错的替代品.

  • 是否有任何特殊原因将代理存储在模块变量中?为什么不只是 `def foo(config=MappingProxyType({'a': 1}))):`?您的示例仍然允许通过 `default_config` 进行全局修改。 (4认同)

msw*_*msw 18

假设字典的键和值本身是不可变的(例如字符串),那么:

>>> 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
Run Code Online (Sandbox Code Playgroud)

  • +1,但是nit:`tuple(sorted(d.iteritems()))`更好. (41认同)
  • 更好的方法是将它放在冻结集中,这不需要键或值来定义一致的顺序. (14认同)
  • 这只有一个问题:你不再有映射.这将是首先冻结字典的重点. (7认同)
  • @devin:完全同意,但我会以我的帖子为例,通常会有更好的方式. (6认同)
  • 回到 dict 时,这种方法非常好。简单的`dict(t)` (2认同)

Jul*_*ins 12

没有fronzedict,但是您可以使用MappingProxyTypePython 3.3中添加到标准库中的:

>>> 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})
Run Code Online (Sandbox Code Playgroud)

  • 警告:“类型错误:无法腌制映射代理对象” (3认同)
  • 问题是“MappingProxyType”仍然不可散列。 (3认同)

Ste*_*nik 9

这是我一直在使用的代码.我继承了冷冻集.其优点如下.

  1. 这是一个真正不可改变的对象.不依赖于未来用户和开发人员的良好行为.
  2. 在常规字典和冻结字典之间来回转换很容易.FrozenDict(orig_dict) - >冻结字典.dict(frozen_dict) - >常规字典.

2015年1月21日更新:我在2014年发布的原始代码使用for循环查找匹配的密钥.那非常慢.现在我已经整理了一个利用了freezeset的散列功能的实现.键值对存储在特殊容器中,其中__hash____eq__函数仅基于键.这段代码也经过了正式的单元测试,不像我在2014年8月发布的那样.

麻省理工学院式的许可证.

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)
Run Code Online (Sandbox Code Playgroud)

  • 请注意,您还通过在此处发布它在 CC BY-SA 3.0 下对其进行了许可。至少这是[流行观点](http://meta.stackexchange.com/a/109787/247924)。我想这的法律依据是在您首次注册时同意某些条款和条件。 (2认同)
  • 我想办法在没有字典的情况下查找密钥散列,我打破了我的大脑。将`Item` 的散列重新定义为键的散列是一个巧妙的技巧! (2认同)

Shm*_*ikA 8

子类化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
Run Code Online (Sandbox Code Playgroud)

用法示例:

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
Run Code Online (Sandbox Code Playgroud)

优点

  • 支持get(), keys(), items()(在 py2 上)以及所有开箱即用的iteritems()好东西,无需明确实现它们dict
  • 内部使用dict意味着性能(dict在 CPython 中用 c 编写)
  • 优雅简约,没有黑魔法
  • isinstance(my_frozen_dict, dict)返回 True - 尽管 python 鼓励鸭式输入许多包的用途isinstance(),但这可以节省许多调整和定制

缺点

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

  • 我在[另一个线程](/sf/answers/4361142901/)中进行了速度比较,结果证明,与许多替代方案相比,覆盖`__setitem__`并继承`dict`速度非常快。 (2认同)
  • 您可以从collections.UserDict继承。它就是为了这个目的,普通的字典在子类化时有很多缺陷 (2认同)

And*_*hak 8

安装frozendict

pip install frozendict
Run Code Online (Sandbox Code Playgroud)

用它!

from frozendict import frozendict

def smth(param = frozendict({})):
    pass
Run Code Online (Sandbox Code Playgroud)


Mar*_*ser 5

我每次编写这样的函数时都会想到frozendict:

def do_something(blah, optional_dict_parm=None):
    if optional_dict_parm is None:
        optional_dict_parm = {}
Run Code Online (Sandbox Code Playgroud)

  • 配方更容易:`optional_dict_parm = optional_dict_parm或{}` (8认同)
  • 每当我看到这样的评论时,我确信我搞砸了某个地方并将{}作为默认值,然后回过头来查看我最近编写的代码. (5认同)
  • @Emmanuel您希望“is None”检查来捕获虚假参数,例如“MappingProxyType({})”,或者如果有人打错了“0”。 (4认同)
  • 在这种情况下,您可以使用[`types.MappingProxyType`](https://docs.python.org/3/library/types.html#types.MappingProxyType)`({})`作为参数的默认值. (2认同)

Ber*_*pac 5

其主要缺点namedtuple是需要在使用前指定,因此对于单一用途的情况不太方便。

然而,有一个实用的解决方法可以用来处理许多此类情况。假设您想要拥有以下字典的不可变等价物:

MY_CONSTANT = {
    'something': 123,
    'something_else': 456
}
Run Code Online (Sandbox Code Playgroud)

可以这样模拟:

from collections import namedtuple

MY_CONSTANT = namedtuple('MyConstant', 'something something_else')(123, 456)
Run Code Online (Sandbox Code Playgroud)

甚至可以编写一个辅助函数来自动执行此操作:

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
Run Code Online (Sandbox Code Playgroud)

当然,这仅适用于平面字典,但实现递归版本应该不会太困难。

  • 与其他元组答案相同的问题:您必须执行“getattr(fa, x)”而不是“fa[x]”,指尖没有“keys”方法,以及映射可能需要的所有其他原因。 (2认同)

Moi*_*dri 5

您可以使用frozendictfromutilspie包作为:

>>> 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
Run Code Online (Sandbox Code Playgroud)

根据文件

frozendict(dict_obj) : 接受 dict 类型的 obj 并返回一个可散列且不可变的dict