Функция hash()
в Python
позволяет вычислять хеш-значения для различных объектов. Обычно для целых чисел хеш совпадает с их значением, но есть исключения, которые могут удивить даже опытных программистов.
Разбираем, почему hash(-1)
и hash(-2)
в CPython возвращают одинаковое значение. Рассмотрим особенности работы hash()
, внутреннюю реализацию хэширования целых чисел и причину специальной обработки -1
.
Вопрос:
Что выведет функция hash() для следующих значений: 1, 0, -1, -2?
>>> hash(1)
1
>>> hash(0)
0
>>> hash(-1)
-1
>>> hash(-2)
-2
>>> hash(1)
1
>>> hash(0)
0
>>> hash(-1)
-2
>>> hash(-2)
-2
# вдобавок ко всему:
>>> hash(-1) == hash(-2)
True
>>> hash(-1) is hash(-2)
True
hash(-1)
и hash(-2)
возвращают одинаковое значение -2
. Дело в том, что поведение функции hash()
для целых чисел в Python
(точнее, в реализации CPython, которая является самой распространенной) имеет свои особенности, связанные с оптимизацией. Чтобы понять суть, нужно рассмотреть несколько моментов.
Python
, стремясь к эффективности, интернирует небольшие целые числа в диапазоне от -5 до 256
включительно. Это означает, что для каждого целого числа из этого диапазона создается только один объект в памяти. Когда вы присваиваете переменной значение, например, -1
, Python
не создает новый объект, а повторно использует уже существующий интернированный объект -1
.
Оператор is
проверяет идентичность объектов, то есть ссылаются ли две переменные на один и тот же объект в памяти. Именно поэтому hash(-1) is hash(-2)
возвращает True
, так как оба вызова hash()
возвращают один и тот же интернированный объект -2
.
Реализация хэш-функции для целых чисел в CPython определена таким образом, что hash(x) == x
для всех целых чисел, кроме -1
. Для -1
сделано исключение: hash(-1) == -2
.
Основная причина такого исключения для -1
заключается в предотвращении потенциальных коллизий хэшей с объектами других типов. Хэш-значения играют ключевую роль в работе словарей (dictionaries
) и множеств (sets
) – структур данных, которые полагаются на уникальные хэш-значения для быстрого поиска и хранения элементов.
Представьте себе ситуацию, когда у вас есть пользовательский класс, и его метод __hash__
возвращает -1
:
class MyObject:
def __hash__(self):
return -1
obj = MyObject()
print(hash(obj)) # Выводит -1
hash(-1)
также возвращало -1
, это привело бы к коллизии хэшей при использовании объекта MyObject
и целого числа -1
в словаре или множестве. Чтобы избежать этой проблемы, hash(-1)
было намеренно сделано равным -2
, чтобы отделить его от потенциальных хэш-значений других типов объектов. Чтобы понять, как это реализовано в CPython, давайте заглянем в его исходный код.
Почему именно исходный код? Потому что именно там находится окончательный ответ на вопрос, как реализована та или иная функция. Документация описывает поведение, но исходный код показывает как это поведение достигается.
Исходный код CPython находится на GitHub. Нам нужно найти файл, который отвечает за реализацию целых чисел и их хэширование. Этот файл называется longobject.c
(раньше, до Python 3, он мог называться intobject.c
). Файл расположен в каталоге Objects/
репозитория CPython.
В файле longobject.c
найдите функцию с именем long_hash
. Эта функция отвечает за вычисление хэша для целых чисел (тип long
в C
, который используется для представления целых чисел произвольной длины в Python). Для старых версий Python (до 3) функция может называться int_hash
Вот вариант кода long_hash
(код может немного отличаться в зависимости от версии Python):
static Py_hash_t
long_hash(PyObject *obj)
{
PyLongObject *v = (PyLongObject *)obj; // Преобразуем PyObject в PyLongObject
Py_uhash_t x; // Переменная для хранения хэш-значения (беззнаковый тип)
Py_ssize_t i; // Индекс для перебора цифр числа
int sign; // Знак числа
if (_PyLong_IsCompact(v)) { // Проверяем, является ли число "компактным"
x = (Py_uhash_t)_PyLong_CompactValue(v); // Получаем компактное значение
if (x == (Py_uhash_t)-1) { // Проверяем, равно ли значение -1
x = (Py_uhash_t)-2; // Если равно, то меняем на -2 (ключевой момент!)
}
return x; // Возвращаем хэш-значение
}
// Дальше идет обработка "некомпактных" чисел (больших чисел)
i = _PyLong_DigitCount(v); // Получаем количество цифр в числе
sign = _PyLong_NonCompactSign(v); // Получаем знак числа
x = 0; // Инициализируем хэш-значение нулем
while (--i >= 0) { // Перебираем цифры числа с конца
/* ... (сложные вычисления хэша для больших чисел) ... */
}
x = x * sign; // Учитываем знак числа
if (x == (Py_uhash_t)-1) // Проверяем, равно ли итоговое хэш-значение -1
x = (Py_uhash_t)-2; // Если равно, то меняем на -2 (ключевой момент!)
return (Py_hash_t)x; // Возвращаем хэш-значение
}
static Py_hash_t long_hash(PyObject *obj)
: Функция принимает PyObject
, который представляет собой общий тип объекта в Python
. Затем она преобразует его в PyLongObject
, который представляет целое число Python
. Тип Py_hash_t
— это тип, используемый для хранения хэш-значения.
if (_PyLong_IsCompact(v))
: Эта проверка определяет, является ли целое число «компактным». «Компактные» целые числа — это целые числа, которые можно представить в виде одного машинного слова (например, long
в C
). Интернированные числа, такие как -1
и -2
, будут компактными. Для компактных чисел используется более быстрый способ вычисления хэша.
x = (Py_uhash_t)_PyLong_CompactValue(v);
: Если число компактное, то _PyLong_CompactValue(v)
возвращает его значение как целое число без знака (Py_uhash_t)
. Это значение присваивается переменной x
.
if (x == (Py_uhash_t)-1)
: Вот здесь и происходит магия! Эта проверка смотрит, равно ли вычисленное хэш-значение -1
.
x = (Py_uhash_t)-2;
: Если хэш-значение равно -1
, то оно заменяется на -2
. Это и есть причина, почему hash(-1)
возвращает -2
!
return x;
: Функция возвращает вычисленное (и, возможно, измененное) хэш-значение.
//
Дальше идет обработка «некомпактных» чисел (больших чисел): Если число не является «компактным» (то есть, это большое число, требующее больше памяти для представления), то используется более сложный алгоритм для вычисления хэша.
while (--i >= 0) { ... }
: Этот цикл проходит по «цифрам» большого числа и выполняет серию операций, чтобы создать хэш. Алгоритм включает в себя битовые сдвиги и операции по модулю, чтобы получить хорошо распределенное хэш-значение.
x = x * sign;
: После вычисления хэш-значения оно умножается на знак числа.
if (x == (Py_uhash_t)-1)
: Даже для больших чисел происходит проверка, чтобы итоговое хэш-значение не равнялось -1
. Если это так, оно также меняется на -2
.
hash()
может показаться незначительной, важно помнить о ней при работе с хэш-функциями и структурами данных, основанных на хэшировании. В большинстве случаев вы не столкнетесь с проблемами, но знание этой детали поможет вам избежать потенциальных ошибок и лучше понимать внутреннее устройство Python
.Ключевые выводы:
Для небольших целых чисел в Python
используется оптимизация (интернирование).
hash(x) == x
для большинства целых чисел, но hash(-1) == -2
из-за внутренней реализации и для предотвращения коллизий.
Это поведение является специфичным для CPython
и может отличаться в других реализациях Python
(например, PyPy
).
Используйте ==
для сравнения значений и is
для сравнения идентичности объектов.
Надеюсь, теперь эта загадка с hash(-1)
стала немного понятнее!
hash(-1)
всегда возвращает -2
, поэтому hash(-1) == hash(-2)
.__hash__()
в пользовательских классах.