【问题标题】:Immutable dictionary, only use as a key for another dictionary不可变字典,仅用作另一个字典的键
【发布时间】:2012-04-03 16:06:17
【问题描述】:

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

几个月前我使用了这个实现: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)
以 dict 为键的值(键 a)

作为输出

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

【问题讨论】:

  • 一个稍微好一点的方法是tuple(sorted(immutableDict.items()))(或iteritems() pre 3.x)。另外,作为一个注释,我会选择FrozenDict 作为默认存在于 Python 中的 frozenset 类的名称,只是为了命名一致性 - 这并不重要。
  • 您的同事可能错过了“同意成人语言”的观点,其中纯 Python 代码中没有任何内容是真正私有的(在强制执行的意义上)。您的代码所做的非常接近于创建不可变对象的预期方式。考虑由 Guido van Rossum、Alex Martelli、Greg Wilson 和我自己编写的 Lib/sets.py 中的 ImmutableSet 代码。标准库代码中的核心开发者代码是否让你的同事“感觉像一只悲伤的熊猫”?

标签: python


【解决方案1】:

如果您只是将它用作另一个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 用于执行此操作的代码)。

【讨论】:

  • 我喜欢这个 - dict 本质上是无序的,因此对其进行排序然后使其成为元组似乎是一种通过强制排序来确保平等的骇人方式 - 如果您存储的内容很奇怪,这可能会中断订购。这种方式不会那样做。这种方式更简单、更干净,我会说是最好的。
  • 正如我在另一个答案中所说,无法使用且不是 pythonic。 enumpip package frozendict 更好。
  • @MarcoSulla Python 3.4 于 2014 年 3 月 16 日发布。这是第一个带有枚举的版本。此答案来自 2012 年。frozendict 在此答案后约 6 个月发布。,
【解决方案2】:

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

【讨论】:

  • 我喜欢你的回答,但它仍然不是一成不变的。仍然可以访问ImmutableDict({"a" : 1}).dict 变量并对其进行更改。是的,您可以通过__dict 将其隐藏,但您仍然可以使用ImmutableDict({"a" : 1})._ImmutableDict__dict 访问它。因此它不是“真正”不可变的;-)
  • 您也缺少__eq__ 方法。它也在使用那个。当您之后更改 .dict 时,self.hash 将不会更新,这似乎仍然会使用它,但它不会使用它来比较它们似乎的键。它还使用__eq__。当我覆盖它并比较它确实有效的__hash__ 方法时?
  • 我已经实现了@RaymondHettinger 的解决方案并将其打包成pip install-able。查看my answer了解更多详情。
  • 如果你想对字典集合进行排序,你可能还想实现__cmp__
【解决方案3】:

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

【讨论】:

    【解决方案4】:

    为了使您的不可变字典安全,它需要做的就是永远不要更改其哈希值。你为什么不直接禁用__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
    

    【讨论】:

    • 仍然不是 100% 不可变的,因为 object.__setattr__ 可以绕过这个。 &gt;&gt;&gt; b = ImmutableDict() &gt;&gt;&gt; b.__hash__() 3527539 &gt;&gt;&gt; object.__setattr__(b, "items", {"bacon": "eggs"}.items) &gt;&gt;&gt; b.__hash__() 28501310
    【解决方案5】:

    这是pip install-able 实现@RaymondHettinger's answer 的链接:https://github.com/pcattori/icicle

    只需pip install icicle 即可from icicle import FrozenDict

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

    【讨论】:

      【解决方案6】:

      看来我发帖迟到了。不知道其他人是否提出了想法。但这是我的看法。 Dict 是不可变的和可散列的。我通过使用引发异常的自定义“_readonly”函数覆盖所有方法,魔术和其他方法,使其不可变。这是在实例化对象时完成的。为了解决无法应用值的问题,我在“__new__”下设置了“哈希”。然后我重写'__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'}

      【讨论】:

        【解决方案7】:

        Raymond Hettinger's answer 的变体,通过将 self._dict 包装为 types.MappingProxyType

        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 被标记为受保护的属性,所以如果你想访问或改变它,你可以进行检查,或者只是你的错。
        【解决方案8】:

        你可以使用枚举:

        import enum
        
        KeyDict1 = enum.Enum('KeyDict1', {'InnerDictKey1':'bla', 'InnerDictKey2 ':2})
        
        d = { KeyDict1: 'whatever', KeyDict2: 1, ...}
        

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

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

        您可以遍历名称并获取它们的值...它可以满足您的所有期望。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-04-14
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多