Метка: хэширование

«Сломанный» set

Вопрос: может ли set содержать два одинаковых объекта?

Ответ: да, запросто!

Делаем класс:

class Foo:
    def __init__(self, x):
        self.x = x
    def __hash__(self):
        return self.x
    def __eq__(self, other):
        return self.x == other.x
    def __repr__(self):
        return f'Foo({self.x})'

# создаем set из трех разных объектов
hacker = Foo(20)
s = {Foo(10), hacker, Foo(30)}

print(s)  # {Foo(10), Foo(20), Foo(30)}

hacker.x = 30  # взлом системы
print(s)  # {Foo(10), Foo(30), Foo(30)}

from collections import Counter
c = Counter(s)
print(c)  # Counter({Foo(30): 2, Foo(10): 1})

Как это? set запоминает хэш объекта при вставке, а потом не следит за тем, меняется ли как-то объект или нет, это было бы очень накладно. Изначально мы вставляли 20, но потом уже поменяли его на 30, тем самым сломав set.

«Починить» такой set можно, сделав из него список, а потом новый set, тогда хэши будут заново пересчитаны. Лучше до такого не доводить!

s2 = set(list(s))
print(s2)  # {Foo(10), Foo(30)}

Примечание: а метод s.copy() не сработает, потому что он копирует уже вычисленные хэши.

Мораль: если вы помещаете свои объекты в set, вы должны самостоятельно обеспечить их логическую иммутабельность. Иными словами обеспечить неизменяемость именно тех атрибутов, которые участвуют в сравнении и хэшировании: не менять их самому и сокрыть от внешних изменений. Те же правила относятся к объектам, которые вы хотите сделать ключами словаря dict.

Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈

А вы знали про hash(-1)?

(Речь идет о реализации CPython)

Встроенная функция hash возвращает целое число – хэш-сумму, которое используется при сравнении ключей словаря во время поиска, например. Для пользовательских классов hash вызывает магический метод класса  __hash__ , а для примитивных типов уже есть встроенная реализация на Си. 

Примечательно, что для чисел hash обычно возвращает само же значение числа-аргумента, кроме нескольких случаев. Запустим этот код:

def print_hash(x):
    print(f'hash({x}) = {hash(x)}')
for i in range(2, -4, -1):
    print_hash(i)

Вывод:

hash(2) = 2
hash(1) = 1
hash(0) = 0
hash(-1) = -2  <-- что?
hash(-2) = -2
hash(-3) = -3

Оказывается hash не возвращает -1, а конвертирует его явно в -2. Я изучил исходный код на Си и нашел это место. «Легенда гласит», что в CPython число -1 зарезервировано внутренне для индикации ошибок при выполнении этой функции.

Еще интереснее для рациональных чисел. От hash от NAN – ноль. Плюс еще пасхалка: hash от бесконечности возращает первые несколько цифр числа π. 

print_hash(-1.0)  # -2
print_hash(float('nan'))  # 0
print_hash(float('+inf'))  # 314159
print_hash(float('-inf'))  # -314159

🧙 Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈