Подтвердить что ты не робот

`object in list` ведет себя иначе, чем` object in dict`?

У меня есть итератор с некоторыми объектами в нем, и я хотел создать коллекцию uniqueUsers, в которой я перечисляю только каждый пользователь один раз. Поэтому, играя немного, я попробовал это как с списком, так и с dict:

>>> for m in ms: print m.to_user  # let first look what 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 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}

Итак, я тестировал его, преобразовывая dict в список при выполнении инструкции if, и это работает так, как я ожидал бы этого:

>>> 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 (как я ожидал бы этого)?

4b9b3361

Ответ 1

Чтобы понять, что происходит, вы должны понять, как оператор in, критерий членства, ведет себя для разных типы.

Для списков это довольно просто из-за того, что в основном состоит из списков: упорядоченные массивы, которые не заботятся о дубликатах. Единственный возможный способ определить критерий членства здесь - перебрать список и проверить каждый элемент на равенстве. Что-то вроде этого:

# x in lst
for item in lst:
    if x == item:
        return True
return False

Словари немного разные: они хэш-таблицы были ключами, которые должны быть уникальными. Для таблиц хэш требуется, чтобы ключи были хешируемыми, что по существу означает, что должна быть явная функция, которая преобразует объект в целое. Это хеш-значение затем используется для размещения ссылки/значения где-то в хеш-таблице.

Так как хэш-значение определяет, где в хеш-таблице помещается элемент, его критическое значение для объектов, которые должны быть одинаковыми, выдает одно и то же значение хэш-функции. Таким образом, следующая импликация должна быть верной: 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) эта "неудачная хэш-реализация" (которая основана на идентификаторе объекта) является значением по умолчанию. Поэтому, когда вы не реализуете свою собственную функцию __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__(), так как для реализации коллекций хеширования требуется, чтобы хэш-значение ключей было неизменным (если значение хеша объектов изменяется, оно будет в неправильном ковше хэша).

Ответ 2

TL; DR: Тест in вызывает __eq__ для списков. Для dicts он сначала вызывает __hash__, и если хеш совпадает, то вызывает __eq__.

  • Тест in только вызывает __eq__ для списков.
    • Без __eq__ сравнение in-ness всегда False.
  • Для dicts вам нужно правильно реализовать __hash__ и __eq__, чтобы иметь возможность правильно сравнивать объекты в нем:

    • Сначала получает хэш объекта из __hash__

      • Без __hash__ для классов нового стиля он использует id(), который уникален для всех созданных объектов и, следовательно, никогда не соответствует существующему, если только тот же объект.
      • И как @poke указал в комментарии:

        В Python 2 новые классы стиля (наследующие от object) наследуют объекты __hash__ реализация, основанная на id(), так что откуда это происходит.

    • Если хеш совпадает, тогда для этого объекта вызывается __eq__ с other.

      • Результат тогда зависит от того, что возвращает __eq__.
    • Если хэш не совпадает, тогда __eq__ не вызывается.

Таким образом, тест in вызывает __eq__ для списков и для dicts... но для dicts, только после того, как __hash__ возвращает соответствующий хэш. И не имея __hash__ doesn 't return None, не выдает ошибку и не делает ее "неумелой".... в Python 2. Чтобы правильно использовать ваш класс to_user в качестве ключей dict, вам нужно __hash__ method, который выполняется правильно, синхронизируется с __eq__.

Подробнее:

Проверка объекта m.to_user not in uniqueUsers "в списке" работала правильно, потому что вы, вероятно, применили метод __eq__, как указывал @poke. (И появляется to_user возвращает объект, а не строку.)

Такая же проверка не работает для "объекта в dict", потому что:
(a) __hash__ в этом классе плохо реализовано, как отметил также @poke.
(b) Или, вы вообще не реализовали __hash__. Это не вызывает ошибки в классах нового стиля Python2.

Используя класс в этом ответе в качестве отправной точки:

>>> 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