使用Python字典作为键(非嵌套)

42

Python不允许将字典用作其他字典中的键。是否有解决方法可以使用非嵌套字典作为键?

对于更复杂的非可哈希对象的一般问题以及我的具体用例已经移至此处。我最初对用例的描述是错误的。


排序是必要的,因为Python字典默认情况下是无序的。 - Casebash
4
这对我来说看起来像是一个设计问题。你能给一个例子,在什么情况下使用字典作为键是有意义的吗? - Jonathan Feinberg
我认为该函数应该接受命名元组(或类实例),而不是嵌套字典。那样就不会有这个问题了。 - Jochen Ritzel
1
我猜你的意思是sorted,它返回一个生成器,所以你需要将其转换为list - Andrey Vlasovskikh
1
你的确切用例听起来就像记忆化。有相关的解决方案,而且你的一个答案也提到了它。如果我理解有误,请问你能否解释一下为什么不使用记忆化解决方案呢? - steveha
我想要做的一个关键是使一个函数能够记住其他结果。例如,如果我有一个计算平均值的函数,那么它也会同时计算标准差,我希望存储这两个值,并且能够在需要时访问其中一个,比如当want="stddev"时访问标准差,当want="average"时访问平均值。 - Casebash
10个回答

83
如果你有一个真正的不可变字典(虽然我不清楚为什么你不只是使用一对列表,例如[('content-type','text/plain'),('host','example.com')]),那么你可以将你的dict转换为:
  1. 一对元组。在你的问题中已经这样做了。必须使用tuple而不是list,因为结果依赖于元素的排序和不可变性。

    >>> tuple(sorted(a.items()))
    
  2. 一个冻结集合。从数学角度来看,这是一种更适合的方法,因为它仅需要对您的不可变dict元素使用相等关系,而第一种方法除了相等性还需要排序关系。

  3. >>> frozenset(a.items())
    

4
在排序方面,你的观点很好。一个字典总是可以转换成 frozenset ,因为键必须是唯一的,这保证了每个元组都会被保存在集合中。非常优雅。 - S.Lott
这是一个不错的解决方案,比我提出的具体问题更通用,但它不能处理字典中嵌套的字典。 - Casebash
现在将这个问题移到一个新的问题中。 - Casebash
4
请注意,如果字典包含列表或任何其他可变对象作为值,则冻结集合解决方案将无法使用。例如:a = {'key1':'val1','key2':['val2','val3']} - AnukuL

8

tuple(someDictionary.items())非常完美地将字典转化为不可变键。 - S.Lott
9
somedictionary.items()排序后转为元组(tuple),因为字典的键值顺序未被保证,这意味着相等的字典可能通过按不同顺序列出其条目来产生不同的表示形式。 - Brian
2
排序很重要。要理解原因,必须找到两个具有相等哈希值的不同键值(对于字符串可能很难找到,但可以轻松使用用户定义对象实现),然后通过以不同顺序插入它们来构造两个相等的字典。你将得到具有不同顺序的相等字典.items() - Denis Otkidach
这个问题的难点在于每个字典都必须进行排序。 - Casebash

7
将一个 someDictionary 转换为键,可以这样做:
key = tuple(sorted(someDictionary .items())

您可以使用 dict( key ) 轻松地实现反转。

+1,虽然我认为我的解决方案使用frozenset更加“正确”,请看我的答案。 - Andrey Vlasovskikh
只要字典的键是可比较的,tuple(sorted()) 就可以工作。frozenset 需要具有可哈希性的字典值。 - BallpointBen

4

一种方法是通过子类化字典并提供哈希方法来实现。例如:

class HashableDict(dict):
    def __hash__(self):
        return hash(tuple(sorted(self.iteritems())))

>>> d = HashableDict(a=1, b=2)
>>> d2 = { d : "foo"}
>>> d2[HashableDict(a=1, b=2)]
"foo"

需要注意的是,字典(或任何可变类型)不这样做的原因:在将对象添加到哈希表之后对其进行更改会更改哈希值,这意味着字典现在会将它放入错误的桶中,因此将返回不正确的结果。

如果您选择这种方式,请确保非常确定字典在放入其他字典后永远不会发生更改,或者积极地防止它们发生更改(例如,检查哈希值在第一次调用__hash__之后是否更改,并在不更改时抛出异常)。


1
覆盖所有可变方法以引发错误将会在更多的代码成本下,更早地捕获错误。 - Ants Aasma
这是一个更特定问题的解决方案,但更一般的问题是公开的。 - Casebash

3

嗯,你的使用场景难道不只是记忆函数调用吗?使用装饰器,您将轻松支持任意函数。是的,它们经常pickle参数,并使用循环推理,只要它们可以被pickled,这对于非标准类型也有效。

例如,请参见此记忆化示例


1
我将总结以下选项并添加一个自己的选项,你可以:
  • 创建字典子类并提供哈希函数
  • 将字典扁平化为元组
  • 使用pickle对字典进行序列化
  • 使用json模块将字典转换为字符串(如下所示)
import json
Dict = {'key' :'value123'}
stringifiedDict = json.dumps(Dict)
print(stringifiedDict)
# {"key": "value123"}
newDict = {stringifiedDict: 12345}
print(newDict[stringifiedDict])
# 12345
for key, val in newDict.items():
    print(json.loads(key))
    # {'key': 'value123'}
    print(json.loads(key)['key'])
    # value123

0

类名…OK :/

我的解决方案是创建一个类,具有字典功能,但实现为一个包含{key, value}对象的列表。keyvalue可以是任何内容。

class DictKeyDictException(Exception):
    pass


class DictKeyDict():

    def __init__(self, *args):
        values = [self.__create_element(key, value) for key, value in args]
        self.__values__ = values

    def __setitem__(self, key, value):
        self.set(key, value)

    def __getitem__(self, key):
        return self.get(key)

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

    def __delitem__(self, key):
        keys = self.keys()

        if key in keys:
            index = keys.index(key)
            del self.__values__[index]

    def clear(self):
        self.__values__ = []

    def copy(self):
        return self.__values__.copy()

    def has_key(self, k):
        return k in self.keys()

    def update(self, *args, **kwargs):
        if kwargs:
            raise DictKeyDictException(f"no kwargs allowed in '{self.__class__.__name__}.update' method")
        for key, value in args:
            self[key] = value

        return self.__values__

    def __repr__(self) -> list:
        return repr(self.__values__)

    @classmethod
    def __create_element(cls, key, value):
        return {"key": key, "value": value}

    def set(self, key, value) -> None:
        keys = self.keys()

        if key in keys:
            index = keys.index(key)
            self.__values__[index] = self.__create_element(key, value)
        else:
            self.__values__.append(self.__create_element(key, value))

        return self.__values__

    def keys(self):
        return [dict_key_value["key"] for dict_key_value in self.__values__]

    def values(self):
        return [value["value"] for value in self.__values__]

    def items(self):
        return [(dict_key_value["key"], dict_key_value["value"]) for dict_key_value in self.__values__]

    def pop(self, key, default=None):
        keys = self.keys()

        if key in keys:
            index = keys.index(key)
            value = self.__values__.pop(index)["value"]
        else:
            value = default

        return value

    def get(self, key, default=None):
        keys = self.keys()

        if key in keys:
            index = keys.index(key)
            value = self.__values__[index]["value"]
        else:
            value = default

        return value

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

和用法:

dad = {"name": "dad"}
mom = {"name": "mom"}
boy = {"name": "son"}
girl = {"name": "daughter"}

# set
family = DictKeyDict()
family[dad] = {"age": 44}
family[mom] = {"age": 43}
# or
family.set(dad, {"age": 44, "children": [boy, girl]})
# or
family = DictKeyDict(
    (dad, {"age": 44, "children": [boy, girl]}),
    (mom, {"age": 43, "children": [boy, girl]}),
)

# update
family.update((mom, {"age": 33}))  # oups sorry miss /!\ loose my children

family.set({"pet": "cutty"}, "cat")
del family[{"pet": "cutty"}]  # cutty left...

family.set({"pet": "buddy"}, "dog")
family[{"pet": "buddy"}] = "wolf"  # buddy was not a dog

print(family.keys())
print(family.values())
for k, v in family.items():
    print(k, v)

0

我不明白为什么你会想这样做,但如果你确实需要,你可以尝试将字典进行序列化(pickling):

mydict = {"a":1, "b":{"c":10}}
import pickle
key = pickle.dumps(mydict)

d[key] = value

1
这解决了嵌套问题,但如果值是非标准类型呢?它是否也会对值进行pickle处理? - Casebash
1
在我看来,这里序列化是一种负担。@Casebash提出了一个很好的观点,指出了非标准类型的问题。 - Andrey Vlasovskikh

0

这个函数将把一个嵌套的字典转换成一个不可变的元组,你可以把它作为一个键来使用:

def convert_dictionary_tuple(input_dict):
    """
    this function receives a nested dictionary and convert it to an immutable tuple of tuples with all the given
    dictionary data
    :param input_dict: a nested dictionary
    :return: immutable tuple of tuples with all the given dictionary data
    """
    tuples_dict = {}
    for key, value in input_dict.iteritems():
        if isinstance(value, dict):
            tuples_dict[key] = convert_dictionary_tuple(value)
        elif isinstance(value, list):
            tuples_dict[key] = tuple([convert_dictionary_tuple(v) if isinstance(v, dict) else v for v in value])
        else:
            tuples_dict[key] = value

    return tuple(sorted(tuples_dict.items()))

-1

我不确定我是否正确理解了你的问题,但我会尝试回答

    d[repr(a)]=value

您可以按照以下方式遍历字典。
for el1 in d:
        for el2 in eval(el1):
                print el2,eval(el1)[el2]

我认为在这里使用repl(a)会更好,因为str可能不是唯一的。 - mmmmmm
不同对象的“repr”可能不同。Python对象之间的“差异”通常编码为“__eq__”,而不是“__repr__”。 - Andrey Vlasovskikh
3
对于字典,reprstr实际上是相同的。然而,这种方式可能会遇到问题 - 可能会得到具有不同内部状态的字典,尽管它们包含相同的项,但按不同的顺序列出其键,因此会产生不同的键。如果在字典中存储没有属性使得repr(x)== repr(y)<=> x == y的对象(例如大多数用户创建的类),也会遇到问题。 - Brian
3
使用 tuple( someDictionary.items() ) 替代 repr。这样可以得到一个结构,可以很容易地转换回字典,而无需使用 eval - S.Lott
当哈希冲突发生时,这种方法将无法正常工作。请查看我在其他答案中的评论以了解如何演示此问题。 - Denis Otkidach

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