На этом шаге мы закончим изучение этого вопроса.
Определим приведенный ниже класс как небольшой сыскной инструмент:
>>> class AlwaysEquals: def __eq__(self, other): return True def __hash__(self): return id(self)
Этот класс характерен двумя аспектами.
Во-первых, поскольку дандер-метод __eq__() всегда возвращает True, все экземпляры этого класса притворяются, что они эквивалентны любому объекту:
>>> AlwaysEquals() == AlwaysEquals() True >>> AlwaysEquals() == 42 True >>> AlwaysEquals() == 'штаа?' True
И во-вторых, каждый экземпляр AlwaysEquals также будет возвращать уникальное хеш-значение, генерируемое встроенной функцией id():
>>> objects = [AlwaysEquals(), AlwaysEquals(), AlwaysEquals()] >>> [hash(obj) for obj in objects] [2538980278960, 2538980278768, 2538980278720]
В Python функция id() возвращает адрес объекта в оперативной памяти, который гарантированно является уникальным.
При помощи этого класса теперь можно создавать объекты, которые притворяются, что они являются эквивалентными любому другому объекту, но при этом с ними будет связано уникальное хеш-значение. Это позволит проверить, переписываются ли ключи словаря, опираясь только на результат их сравнения на эквивалентность.
И, как вы видите, ключи в следующем ниже примере не переписываются, несмотря на то что сравнение всегда будет показывать их как эквивалентные друг другу:
>>> {AlwaysEquals(): 'да', AlwaysEquals(): 'нет'}
{<__main__.AlwaysEquals object at 0x0000024F27048310>: 'да',
<__main__.AlwaysEquals object at 0x0000024F27048340>: 'нет'}
Мы также можем взглянуть на эту идею с другой стороны и проверить, будет ли возврат одинакового хеш-значения достаточным основанием для того, чтобы заставить ключи быть переписанными:
>>> class SameHash: def __hash__(self): return 1
Сравнение экземпляров класса SameHash будет показывать их как не эквивалентные друг другу, но они все будут обладать одинаковым хеш-значением, равным 1:
>>> a = SameHash() >>> b = SameHash() >>> a == b False >>> hash(a), hash(b) (1, 1)
Давайте посмотрим, как словари Python реагируют, когда мы пытаемся использовать экземляры класса SameHash в качестве ключей словаря:
>>> {a: 'a', b: 'b'}
{<__main__.SameHash object at 0x0000024F270484F0>: 'a',
<__main__.SameHash object at 0x0000024F27048550>: 'b'}
Как показывает этот пример, эффект "ключи переписываются" вызывается не одними только конфликтами хеш-значений.
Словари выполняют проверку на эквивалентность и сравнивают хеш-значение, чтобы определить, являются ли два ключа одинаковыми. Попробуем резюмировать результаты нашего исследования.
Выражение-словарь {True: 'да', 1: 'нет', 1.0: 'возможно'} вычисляется как {True: 'возможно'}, потому что сравнение всех ключей этого примера, True, 1, и 1.0, будет показывать их как эквивалентные друг другу, и они все имеют одинаковое хеш-значение:
>>> True == 1 == 1.0 True >>> (hash(True), hash(1), hash(1.0)) (1, 1, 1)
Пожалуй, теперь уже не так удивительно, что мы получили именно такой результат в качестве конечного состояния словаря:
>>> {True: 'да', 1: 'нет', 1.0: 'возможно'}
{True: 'возможно'}
Здесь мы затронули много тем, и этот конкретный трюк Python поначалу может не укладываться в голове.
Если вы с трудом понимаете, что здесь происходит, попробуйте поэкспериментировать по очереди со всеми примерами кода в сеансе интерпретатора Python. Вы будете вознаграждены расширением своих познаний о внутренних механизмах языка Python.
На следующем шаге мы подитожим изученный материал.