不可变字典,仅用作另一个字典的键

时间:2012-04-03 16:06:18

标签: python

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

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

但是我收到一位同事的通知说'它不是真的不可变,因此不安全。你可以使用它,但它确实让我感觉像一个悲伤的熊猫'。

所以我开始四处寻找创造一个不可变的东西。我没有必要将'key-dict'与另一个'key-dict'进行比较。它唯一的用途是作为另一个字典的关键。

我想出了以下内容:

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()

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

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}
  用词作为键的值(键a)
  以字典为键的键值(键b)
  以字典为键的键值(键a)

作为输出

这是我可以使用的“最好的”不可变词典,满足我的要求吗?如果没有,那么什么是更好的解决方案?

8 个答案:

答案 0 :(得分:38)

如果您仅将其用作其他dict的密钥,则可以转到frozenset(mutabledict.items())。如果需要访问基础映射,则可以将其用作dict的参数。

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

请注意,您也可以将其与来自dict的类结合使用,并使用frozenset作为哈希的来源,同时禁用__setitem__,如另一个答案所示。 (@RaymondHettinger's answer用于执行该操作的代码。)

答案 1 :(得分:22)

Mapping 抽象基类使这很容易实现:

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

答案 2 :(得分:9)

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

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

脚本的输出是:

{{'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

答案 3 :(得分:8)

我意识到这已经得到了回答,但types.MappingProxyType是Python 3.3的类似实现。关于最初的安全问题,PEP 416 -- Add a frozendict builtin type讨论了为什么frozendict被拒绝的想法。

答案 4 :(得分:5)

以下是pip install的链接 - 能够实施@RaymondHettinger's answerhttps://github.com/pcattori/icicle

只需pip install icicle即可from icicle import FrozenDict

更新: icicle已被弃用,取而代之的是mapshttps://github.com/pcattori/mapsdocumentationPyPI)。

答案 5 :(得分:2)

看来我迟到了。不确定是否有其他人提出了想法。但这是我的看法。 Dict是不可改变的,可以清洗。我通过使用自定义'_readonly'函数覆盖所有方法(魔术和其他方法)来使其变为不可变,这会引发异常。这是在实例化对象时完成的。为了解决无法应用值的问题,我在'__new__'下设置了'hash'。然后我覆盖'__hash __'函数。而已!

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]

     

“耶”

     

dict1

     

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

答案 6 :(得分:1)

self._dicttypes.MappingProxyType包装在一起,Raymond Hettinger's answer的变化。

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)

答案 7 :(得分:0)

您可以使用一个枚举:

import enum

KeyDict1 = enum.Enum('KeyDict1', {'InnerDictKey1':'bla', 'InnerDictKey2 ':2})

d = { KeyDict1: 'whatever', KeyDict2: 1, ...}

您可以像访问字典一样访问枚举:

KeyDict1['InnerDictKey2'].value  # This is 2

您可以遍历名称,并获取其值...它可以完成您期望的所有事情。