Как гарантировать уникальное значение на выходе у хеш-функции
Что проверяют
Знаете ли вы, что для произвольных входных данных уникальность хеша гарантировать нельзя, и умеете ли объяснить почему. Хороший ответ начинается со слова «никак», а дальше идёт то, что можно сделать на практике. Следом часто спрашивают, как dict в Python живёт с коллизиями.
Как отвечать
- почему нельзя: выход хеш-функции фиксированной длины, а входов бесконечно много. По принципу Дирихле хотя бы две разные строки дадут одинаковый хеш;
- что можно: сделать коллизию практически невероятной. У SHA-256 случайное совпадение становится заметно вероятным лишь около 2^128 значений (парадокс дней рождения). MD5 и SHA-1 для этого не годятся: для них коллизии строят намеренно;
- когда гарантия есть: если множество ключей конечно и известно заранее, строится идеальная хеш-функция без коллизий. Для небольших целых чисел
hash(n)в CPython возвращает само число, но это деталь реализации, а не контракт:hash(-1) == -2; - как живёт
dict: коллизии там штатная ситуация. Словарь сравнивает ключи через__eq__, поэтому одинаковый хеш у разных ключей логику не ломает, только замедляет поиск.
Практический вывод: если нужен уникальный идентификатор, берите не хеш, а uuid.uuid4() или последовательность из базы. Если хеш нужен для дедупликации, храните рядом исходное значение и сравнивайте его при совпадении хешей.
Где спотыкаются
Предлагают соль как способ сделать хеш уникальным. Соль нужна для паролей, чтобы одинаковые пароли давали разные хеши и не подбирались по готовым таблицам. Уникальности она не добавляет, а пароли хешируют через bcrypt, argon2 или hashlib.scrypt, а не голым SHA-256.
Используют встроенный hash() для строк как стабильный идентификатор. Он рандомизирован между запусками процесса (PYTHONHASHSEED), и после рестарта значения будут другими.
Обрезают хеш до первых 8 шестнадцатеричных символов ради краткости и забывают, что это всего 32 бита: уже на десятках тысяч записей коллизия ощутимо вероятна, а к сотне тысяч её шанс выше половины.