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

30
我需要实现一个可哈希的字典,这样我就可以将它用作另一个字典的键。
几个月前,我使用了这个实现:Python可哈希字典
然而,我的一位同事告诉我:“它并不是真正的不可变,因此不安全。你可以使用它,但这让我感觉像只悲伤的熊猫。”
所以我开始寻找创建一个真正不可变的字典。我不需要将“键-字典”与另一个“键-字典”进行比较。它仅用作另一个字典的键。
我想到了以下代码:
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)

这是我可以使用的最佳不可变字典,以满足我的要求吗?如果不是,有更好的解决方案吗?


4
一个稍微更好的方法是tuple(sorted(immutableDict.items()))(在Python 3.x之前使用iteritems())。 另外,只是提一下,由于 Python 中已经有了 frozenset 类,为了保持命名一致性,我会选择 FrozenDict 作为名称 - 尽管这并不重要。 - Gareth Latty
6
你的同事可能没有理解“成年人同意语言”的重点,即纯Python代码中没有真正私有(在强制执行的意义上)。你的代码非常接近创建不可变对象的预期方式。请考虑Lib/sets.py中ImmutableSet代码的编写者Guido van Rossum、Alex Martelli、Greg Wilson和我自己。标准库代码中核心开发者的代码是否让你的同事“感觉像个悲伤的熊猫”? - Raymond Hettinger
9个回答

39

如果你只是将它用作另一个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的答案提供了执行此操作的代码)。


1
我喜欢这个 - dict 本质上是无序的,因此对其进行排序,然后将其转换为元组似乎是一种通过强制排序来确保相等性的 hackish 方法 - 如果您存储的内容具有奇怪的排序方式,则可能会破坏它。这种方法不会这样做。这种方法更简单,更清洁,我认为是最好的。 - Gareth Latty
1
正如我在另一个答案中所说,这种写法既不可用也不符合Python的风格。使用enum或者pip包frozendict会更好。 - Marco Sulla
1
@MarcoSulla Python 3.4于2014年3月16日发布。这是第一个具有枚举的版本。这个答案是来自2012年的。frozendict在此答案发布后大约6个月发布。 - Marcin

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
我喜欢你的回答,但它仍然不是不可变的。人们仍然可以访问ImmutableDict({"a" : 1}).dict变量并更改它。是的,您可以通过使用__dict将其隐藏,但是然后您仍然可以通过使用ImmutableDict({"a" : 1})._ImmutableDict__dict来访问它。因此它并不是真正的不可变的;-) - Daan Timmer
你还缺少__eq__方法。它也会使用它。当你之后改变.dict时,self.hash不会被更新,这似乎仍然在使用它,但它似乎并没有用它来比较键。它还使用了__eq__。当我重写并比较__hash__方法时,它确实起作用了? - Daan Timmer
我已经实现了@RaymondHettinger的解决方案,并将其打包,以便可以通过pip install进行安装。有关更多详细信息,请查看我的答案 - Pedro Cattori
如果您想对字典集合进行排序,您可能还需要实现__cmp__ - Jeff

10

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

2
仍然不是100%的不可变,因为object.__setattr__可以绕过此设置。`>>> b = ImmutableDict()
b.hash() 3527539 object.setattr(b, "items", {"bacon": "eggs"}.items) b.hash() 28501310`
- Casey Kuball

5

3

看起来我晚了一步发布。不确定是否有其他人想出了主意。但这是我的看法。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:"耶"}

dict1 [immutabled1]

"耶"

dict1

{{'Cheese':'Blarg','This':'That'}:“耶”}


1
通过使用types.MappingProxyType包装self._dict的方式,对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)

我认为使用MappingProxyType是没有用的。_dict被标记为受保护的属性,因此如果您想要访问或更改它,可以进行检查,或者简单地说这是您的错误。 - Marco Sulla

0

你可以尝试使用 https://github.com/Lightricks/freeze

它提供了递归不可变且可哈希的字典

from freeze import FDict

a_mutable_dict = {
    "list": [1, 2],
    "set": {3, 4},
}

a_frozen_dict = FDict(a_mutable_dict)

print(a_frozen_dict)
print(hash(a_frozen_dict))

# FDict: {'list': FList: (1, 2), 'set': FSet: {3, 4}}
# -4855611361973338606

0

您可以使用枚举:

import enum

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

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

您可以像访问字典一样访问枚举值:
KeyDict1['InnerDictKey2'].value  # This is 2

你可以迭代遍历名称,并获取它们的值... 它会做你所期望的一切。

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