我需要实现一个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
35class 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
17d = {"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)
号
作为输出
这是"最好的"不变的字典,我可以使用它来满足我的要求吗?如果不是,那么什么是更好的解决方案呢?
更好的方法是tuple(sorted(immutableDict.items()))(或iteritems()pre 3.x)。另外,正如一个注释,我会将FrozenDict作为一个名称,给出默认存在于python中的frozenset类,只是为了命名的一致性——这并不重要。
您的同事可能错过了"同意成人语言"的要点,即纯Python代码中的任何内容都不是真正私有的(在强制执行的意义上)。您的代码所做的非常接近于创建不可变对象的预期方式。考虑一下lib/sets.py中不变的代码,它是由guido van rossum、alex martelli、greg wilson和我自己编写的。标准库代码中的核心开发人员代码是否会让您的同事"感觉像一只悲伤的熊猫"?
如果您只将它用作另一个dict的密钥,则可以使用frozenset(mutabledict.items())。如果需要访问底层映射,那么可以将其用作dict的参数。
1
2
3
4mutabledict = dict(zip('abc', range(3)))
immutable = frozenset(mutabledict.items())
read_frozen = dict(immutable)
read_frozen['a'] # => 1
注意,您也可以将它与派生自dict的类结合起来,并使用frozenset作为散列的源,同时禁用__setitem__,如另一个答案中所建议的那样。(@raymondhettinger的代码答案。
我喜欢这样——dict本质上是无秩序的,所以对它进行排序,然后使其成为一个元组,似乎是一种通过强制排序来确保平等的黑客方式——如果您存储的内容有奇怪的顺序,这可能会中断。这样做不行。这样更简单,更干净,我会说最好。
映射抽象基类使得这很容易实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23import 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。查看我的答案了解更多详细信息。
如果要对字典集合进行排序,还可能需要实现__cmp__。
为了使不可变字典安全,它所需要做的就是永远不要更改其哈希值。为什么不按如下方式禁用__setitem__?
1
2
3
4
5
6
7
8
9
10
11class 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
a['a'] = 0
File"ex.py", line 3, in __setitem__
raise Exception("Can't touch this")
Exception: Can't touch this
仍然不是100%不变的,因为object.__setattr__可以绕过这一点。埃多克斯1〔2〕
我知道这已经得到了回答,但是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
15class 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
28class 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)
。