我有一个包含一些对象的迭代器,我想创建一个uniqueUsers的集合,其中我只列出每个用户一次。所以玩了一下我用列表和字典来尝试它:
>>> for m in ms: print m.to_user # let's first look what's inside ms
...
Pete Kramer
Pete Kramer
Pete Kramer
>>>
>>> uniqueUsers = [] # Create an empty list
>>> for m in ms:
... if m.to_user not in uniqueUsers:
... uniqueUsers.append(m.to_user)
...
>>> uniqueUsers
[Pete Kramer] # This is what I would expect
>>>
>>> uniqueUsers = {} # Now let's create a dict
>>> for m in ms:
... if m.to_user not in uniqueUsers:
... uniqueUsers[m.to_user] = 1
...
>>> uniqueUsers
{Pete Kramer: 1, Pete Kramer: 1, Pete Kramer: 1}
所以我通过在执行if语句时将dict转换为列表来测试它,并且这可以像我期望的那样工作:
>>> uniqueUsers = {}
>>> for m in ms:
... if m.to_user not in list(uniqueUsers):
... uniqueUsers[m.to_user] = 1
...
>>> uniqueUsers
{Pete Kramer: 1}
我可以通过针对uniqueUsers.keys()
的测试获得类似的结果。
问题是我不明白为什么会出现这种差异。我一直认为如果你做if object in dict
,它只是创建一个dicts键列表并再次测试,但显然不是这样。
任何人都可以解释object in dict
内部如何运作以及为什么它与object in list
的行为不同(正如我所期望的那样)?
答案 0 :(得分:16)
为了了解发生了什么,您必须了解in
运算符membership test对不同类型的行为。
对于列表,这很简单,因为基本上是什么列表:不关心重复的有序数组。在此处执行成员资格测试的唯一可能方法是遍历列表并检查相等上的每个项目。像这样:
# x in lst
for item in lst:
if x == item:
return True
return False
字典有点不同:它们是哈希表,键是唯一的。散列表要求键是 hashable ,这实际上意味着需要有一个显式函数将对象转换为整数。然后使用此哈希值将键/值映射放入哈希表中。
由于散列值确定了散列表中放置项的位置,因此,要求相同的对象生成相同的散列值至关重要。因此,以下含义必须如此:x == y => hash(x) == hash(y)
。反过来不一定是真的;让不同的对象生成相同的哈希值是完全有效的。
当对字典执行成员资格测试时,字典将首先查找哈希值。如果它可以找到它,那么它将对它找到的所有项目执行相等检查;如果它没有找到哈希值,那么它假设它是一个不同的对象:
# x in dct
h = hash(x)
items = getItemsForHash(dct, h)
for item in items:
if x == item:
return True
# items is empty, or no match inside the loop
return False
由于在对列表使用成员资格测试时获得了所需的结果,这意味着您的对象正确地实现了相等性比较(__eq__
)。但是由于在使用字典时没有得到正确的结果,似乎有一个__hash__
实现与等式比较实现不同步:
>>> class SomeType:
def __init__ (self, x):
self.x = x
def __eq__ (self, other):
return self.x == other.x
def __hash__ (self):
# bad hash implementation
return hash(id(self))
>>> l = [SomeType(1)]
>>> d = { SomeType(1): 'x' }
>>> x = SomeType(1)
>>> x in l
True
>>> x in d
False
请注意,对于Python 2中的新式类(继承自object
的类),此“错误哈希实现”(基于对象ID)是默认值。因此,当您未实现自己的__hash__
函数时,它仍会使用该函数。这最终意味着除非您的__eq__
仅执行身份检查(默认),否则哈希函数将不同步。
因此,解决方案是以与__hash__
中使用的规则一致的方式实施__eq__
。例如,如果您比较两个成员self.x
和self.y
,那么您应该对这两个成员使用复合哈希。最简单的方法是返回这些值的元组的哈希值:
class SomeType (object):
def __init__ (self, x, y):
self.x = x
self.y = y
def __eq__ (self, other):
return self.x == other.x and self.y == other.y
def __hash__ (self):
return hash((self.x, self.y))
请注意,如果对象是可变的,则不应使其具有可呈现性:
如果一个类定义了可变对象并实现了
__eq__()
方法,那么它不应该实现__hash__()
,因为hashable集合的实现要求键的哈希值是不可变的(如果对象的哈希值发生变化) ,它将在错误的哈希桶中。)
答案 1 :(得分:8)
TL; DR:in
测试调用__eq__
列表。对于dicts,它首先调用__hash__
,如果哈希匹配,则调用__eq__
。
in
测试仅针对列表调用__eq__
。
__eq__
,则 in-ness 比较始终为False
。对于dicts,您需要正确实施__hash__
和 __eq__
才能正确比较中的对象:< / p>
首先从__hash__
__hash__
,对于新式类,它使用id()
,它对于创建的所有对象都是唯一的,因此永远不会匹配现有对象,除非它是相同的对象。 在Python 2中,新样式类(继承自
object
)继承了基于__hash__
的对象的id()
实现,因此它就是来自的地方。
如果哈希值匹配,则使用 __eq__
为该对象调用然后 other
。
__eq__
返回的内容。__eq__
未被称为。 所以in
测试会调用__eq__
列表和dicts ... 但是对于dicts,仅在__hash__
返回匹配的哈希之后。并且没有__hash__
没有返回None
,不会抛出错误并且不会使其“不可用”。 ...在Python中2.要正确使用to_user
类作为dict键,您需要正确实现__hash__
method,与__eq__
同步。
详细说明:
检查列表中的m.to_user not in uniqueUsers
“对象”工作正常,因为你可能已经实现了__eq__
方法,正如@poke指出的那样。 (看起来to_user
会返回一个对象,而不是一个字符串。)
同样的检查不适用于“dict中的对象”,因为:
(a)正如@poke所指出的那样,该班级的__hash__
执行得很糟糕
(b)或您根本没有实施__hash__
。这不会引发Python2新式类的错误。
使用the class in this answer作为起点:
>>> class Test2(object):
... def __init__(self, name):
... self.name = name
...
... def __eq__(self, other):
... return self.name == other.name
...
>>> test_Dict = {}
>>> test_List = []
>>>
>>> obj1 = Test2('a')
>>> obj2 = Test2('a')
>>>
>>> test_Dict[obj1] = 'x'
>>> test_Dict[obj2] = 'y'
>>>
>>> test_List.append(obj1)
>>> test_List.append(obj2)
>>>
>>> test_Dict
{<__main__.Test2 object at 0x0000000002EFC518>: 'x', <__main__.Test2 object at 0x0000000002EFC940>: 'y'}
>>> test_List
[<__main__.Test2 object at 0x0000000002EFC518>, <__main__.Test2 object at 0x0000000002EFC940>]
>>>
>>> Test2('a') in test_Dict
False
>>> Test2('a') in test_List
True