`list in list`的行为与`dict中的`不同?

时间:2016-03-06 11:52:56

标签: python list if-statement dictionary cpython

我有一个包含一些对象的迭代器,我想创建一个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的行为不同(正如我所期望的那样)?

2 个答案:

答案 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.xself.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__

  1. in测试仅针对列表调用__eq__
    • 如果没有__eq__,则 in-ness 比较始终为False
  2. 对于dicts,您需要正确实施__hash__ __eq__才能正确比较中的对象:< / p>

    • 首先从__hash__

      获取对象的哈希值
      • 没有__hash__,对于新式类,它使用id(),它对于创建的所有对象都是唯一的,因此永远不会匹配现有对象,除非它是相同的对象。
      • 正如@poke在评论中指出:
          

        在Python 2中,新样式类(继承自object)继承了基于__hash__的对象的id()实现,因此它就是来自的地方。

    • 如果哈希值匹配,则使用 __eq__ 为该对象调用然后 other

      • 结果取决于__eq__返回的内容。
    • 如果哈希匹配,则__eq__ 未被称为
  3. 所以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