Python: `key不在my_dict`但`key在my_dict.keys()`中
我遇到了一个奇怪的情况。我有一个字典,叫做 self.containing_dict
。通过调试工具,我可以看到这个字典的内容,并且发现 self
是它的一个键。但是看看这个:
>>> self in self.containing_dict
False
>>> self in self.containing_dict.keys()
True
>>> self.containing_dict.has_key(self)
False
这是怎么回事呢?
(我想说明一下,这段代码是在一个弱引用回调中执行的。)
更新:有人让我展示 self
的 __hash__
实现。这里是:
def __hash__(self):
return hash(
(
tuple(sorted(tuple(self.args))),
self.star_args,
tuple(sorted(tuple(self.star_kwargs)))
)
)
args = property(lambda self: dict(self.args_refs))
star_args = property(
lambda self:
tuple((star_arg_ref() for star_arg_ref in self.star_args_refs))
)
star_kwargs = property(lambda self: dict(self.star_kwargs_refs))
3 个回答
很可能你为某个类(也就是self
所代表的类)定义了自定义的哈希和比较方法,而且在把self
放进字典后,你对它进行了修改。
如果你把一个可变对象当作字典的键,然后在修改了这个对象后,你可能就无法在字典中找到它了,但它仍然会出现在keys()
的结果里。
根据你写的 __hash__
方法,这个类是通过存储它的参数的引用来生成哈希值的。问题在于,这些参数是和创建这个对象的代码共享的。如果这些参数被修改了,哈希值也会改变,这样你就无法在任何包含这个对象的字典中找到它了。
这些参数其实不需要复杂,简单的列表就可以了。
In [13]: class Spam(object) :
....: def __init__(self, arg) :
....: self.arg = arg
....: def __hash__(self) :
....: return hash(tuple(self.arg,))
In [18]: l = range(5)
In [19]: spam = Spam(l)
In [20]: hash(spam)
Out[20]: -3958796579502723947
如果我修改了作为参数传入的列表,哈希值也会随之改变。
In [21]: l += [10]
In [22]: hash(spam)
Out[22]: -6439366262097674983
因为字典的键是通过哈希值来组织的,当我执行 x in d
时,Python 首先会计算 x 的哈希值,然后在字典中查找这个哈希值对应的内容。问题是,如果一个对象在放入字典后哈希值发生了变化,Python 就会查看新的哈希值,而找不到原来的键。为了避免这个问题,使用键的列表会强制 Python 逐个检查每个键的相等性,而不依赖哈希值的检查。
你所描述的问题可能是因为你的对象实现了 __eq__
(或者 __cmp__
),但没有实现相应的 __hash__
方法。如果你没有实现 __hash__
方法,建议你去实现一个。通常情况下,定义了 __eq__
但没有 __hash__
的对象不能作为字典的键,但如果你继承了一个 __hash__
方法,这个限制可能会被绕过。
如果你实现了 __hash__
,你需要确保它的行为是正确的:返回的结果在对象的生命周期内不能改变(至少在对象作为字典键或集合项使用期间),而且它必须和 __eq__
一致。一个对象的哈希值 必须 和它相等的对象(根据 __eq__
或 __cmp__
)是相同的。一个对象的哈希值 可以 和不相等的对象不同,但不一定要这样。这个要求还意味着你不能让 __eq__
的结果在对象的生命周期内改变,这就是为什么可变对象通常不能作为字典的键。
如果你的 __hash__
和 __eq__
不匹配,Python 就无法在字典和集合中找到这个对象,但它仍然会出现在 dict.keys()
和 list(set)
中,这就是你在这里描述的情况。实现 __hash__
方法的常见方式是返回你在 __eq__
或 __cmp__
方法中使用的属性的 hash()
值。