关于python:不可变字典,仅用作另一个字典的键

Immutable dictionary, only use as a key for another dictionary

我需要实现一个hashable dict,这样我就可以使用一个字典作为另一个字典的键。

几个月前我使用了这个实现:python hashable dicts

然而,我收到一位同事的通知,他说:"这并不是一成不变的,因此它是不安全的。"你可以用它,但它确实让我觉得自己像一只悲伤的熊猫。

所以我开始四处寻找,创造一个不变的。我不需要把"钥匙口述"和另一个"钥匙口述"进行比较。它的唯一用途是作为另一本字典的键。

我提出了以下几点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class HashableDict(dict):
   """Hashable dict that can be used as a key in other dictionaries"""

    def __new__(self, *args, **kwargs):
        # create a new local dict, that will be used by the HashableDictBase closure class
        immutableDict = dict(*args, **kwargs)

        class HashableDictBase(object):
           """Hashable dict that can be used as a key in other dictionaries. This is now immutable"""

            def __key(self):
               """Return a tuple of the current keys"""
                return tuple((k, immutableDict[k]) for k in sorted(immutableDict))

            def __hash__(self):
               """Return a hash of __key"""
                return hash(self.__key())

            def __eq__(self, other):
               """Compare two __keys"""
                return self.__key() == other.__key() # pylint: disable-msg=W0212

            def __repr__(self):
               """@see: dict.__repr__"""
                return immutableDict.__repr__()

            def __str__(self):
               """@see: dict.__str__"""
                return immutableDict.__str__()

            def __setattr__(self, *args):
                raise TypeError("can't modify immutable instance")
            __delattr__ = __setattr__

        return HashableDictBase()

我使用以下方法测试功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
d = {"a" : 1}

a = HashableDict(d)
b = HashableDict({"b" : 2})

print a
d["b"] = 2
print a

c = HashableDict({"a" : 1})

test = {a :"value with a dict as key (key a)",
        b :"value with a dict as key (key b)"}

print test[a]
print test[b]
print test[c]

它给出:

{'a': 1}
{'a': 1}
value with a dict as key (key a)
value with a dict as key (key b)
value with a dict as key (key a)

作为输出

这是"最好的"不变的字典,我可以使用它来满足我的要求吗?如果不是,那么什么是更好的解决方案呢?


如果您只将它用作另一个dict的密钥,则可以使用frozenset(mutabledict.items())。如果需要访问底层映射,那么可以将其用作dict的参数。

1
2
3
4
mutabledict = dict(zip('abc', range(3)))
immutable = frozenset(mutabledict.items())
read_frozen = dict(immutable)
read_frozen['a'] # => 1

注意,您也可以将它与派生自dict的类结合起来,并使用frozenset作为散列的源,同时禁用__setitem__,如另一个答案中所建议的那样。(@raymondhettinger的代码答案。


映射抽象基类使得这很容易实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import collections

class ImmutableDict(collections.Mapping):
    def __init__(self, somedict):
        self._dict = dict(somedict)   # make a copy
        self._hash = None

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

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

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

    def __hash__(self):
        if self._hash is None:
            self._hash = hash(frozenset(self._dict.items()))
        return self._hash

    def __eq__(self, other):
        return self._dict == other._dict


为了使不可变字典安全,它所需要做的就是永远不要更改其哈希值。为什么不按如下方式禁用__setitem__

1
2
3
4
5
6
7
8
9
10
11
class ImmutableDict(dict):
    def __setitem__(self, key, value):
        raise Exception("Can't touch this")
    def __hash__(self):
        return hash(tuple(sorted(self.items())))

a = ImmutableDict({'a':1})
b = {a:1}
print b
print b[a]
a['a'] = 0

脚本的输出为:

1
2
3
4
5
6
7
8
{{'a': 1}: 1}
1
Traceback (most recent call last):
  File"ex.py", line 11, in <module>
    a['a'] = 0
  File"ex.py", line 3, in __setitem__
    raise Exception("Can't touch this")
Exception: Can't touch this


我知道这已经得到了回答,但是types.mappingProxyType是Python3.3的一个类似的实现。关于最初的安全问题,在PEP416中有一个讨论——添加一个frozendict内置类型,说明为什么拒绝frozendict的想法。


这里有一个指向pip install的链接,可以实现@raymondhettinger的答案:https://github.com/pcattori/icicle

只需pip install icicle,您就可以使用from icicle import FrozenDict

更新:icicle已被否决,支持maps:https://github.com/pcattori/maps(documentation,pypi)。


看来我迟到了。不确定是否有人想出了主意。但这是我的看法。该dict是不可变的和可散列的。我用一个自定义的"readonly"函数重写了所有的方法(magic等),使其不可变,从而引发了异常。这是在对象实例化时完成的。为了解决无法应用值的问题,我在"新"下设置了"hash"。然后我重写"uuu hash"函数。就这样!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ImmutableDict(dict):

_HASH = None

def __new__(cls, *args, **kwargs):
    ImmutableDict._HASH = hash(frozenset(args[0].items()))
    return super(ImmutableDict, cls).__new__(cls, args)

def __hash__(self):
    return self._HASH

def _readonly(self, *args, **kwards):
    raise TypeError("Cannot modify Immutable Instance")

__delattr__ = __setattr__ = __setitem__ = pop = update = setdefault = clear = popitem = _readonly

测试:

immutabled1 = ImmutableDict({"This":"That","Cheese":"Blarg"})

dict1 = {immutabled1:"Yay"}

dict1[immutabled1]

"Yay"

dict1

{{'Cheese': 'Blarg', 'This': 'That'}: 'Yay'}


types.MappingProxyType包住self._dict,从而改变了雷蒙德·赫廷格的回答。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class ImmutableDict(collections.Mapping):
   """
    Copies a dict and proxies it via types.MappingProxyType to make it immutable.
   """

    def __init__(self, somedict):
        dictcopy = dict(somedict) # make a copy
        self._dict = MappingProxyType(dictcopy) # lock it
        self._hash = None

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

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

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

    def __hash__(self):
        if self._hash is None:
            self._hash = hash(frozenset(self._dict.items()))
        return self._hash

    def __eq__(self, other):
        return self._dict == other._dict

    def __repr__(self):
        return str(self._dict)