← Назад к курсу

Как Python обрабатывает коллизии в словарях.

Давайте разберем подробно, как Python обрабатывает коллизии в словарях.

1. Что такое коллизия хешей?

Коллизия возникает, когда два разных ключа имеют одинаковый хеш-код. Поскольку словарь Python использует хеш-таблицы для хранения пар ключ-значение, в теории они могли бы претендовать на одну и ту же ячейку.

Пример коллизии:

# Два разных ключа с одинаковым хешем
a = "ключ1"
b = "ключ2"

# В большинстве случаев хеши разные,
# но теоретически возможны коллизии
print(hash(a) == hash(b))  # False (но возможно True для некоторых ключей)

2. Механизм разрешения коллизий в Python

Python использует открытую адресацию (open addressing) с двойным хешированием (double hashing). Это отличается от метода цепочек (chaining), используемого в некоторых других языках.

Алгоритм работы:

  1. Первичный индекс:

    index = hash(key) % table_size
    
  2. Если ячейка занята другим ключом, Python вычисляет новый индекс с помощью формулы повторного хеширования (perturbation):

    perturb = hash(key)
    while занято:
        index = (5 * index + 1 + perturb) % table_size
        perturb >>= 5  # Сдвигаем биты
    
  3. Процесс продолжается, пока не найдется:

    • Свободная ячейка
    • Ячейка с удаленным ключом
    • Ячейка с тем же ключом (тогда обновляем значение)

3. Детальный пример работы

# Упрощенная иллюстрация процесса
class SimpleDict:
    def __init__(self, size=8):
        self.table = [None] * size
        self.size = size
    
    def _probe(self, key, hash_val):
        """Поиск ячейки с использованием двойного хеширования"""
        index = hash_val % self.size
        perturb = hash_val
        
        while True:
            if self.table[index] is None:
                return index
            # Проверяем, тот ли это ключ (учитывая возможные коллизии хешей)
            if self.table[index][0] == key:
                return index
            # Коллизия - ищем дальше
            index = (5 * index + 1 + perturb) % self.size
            perturb >>= 5

# Пример коллизии
d = {}
d["ключ1"] = "значение1"
d["ключ2"] = "значение2"  # Может вызвать коллизию, если хеши совпадают

4. Почему коллизии возможны?

Причины:

  1. Конечное пространство хешей:

    • Хеш-функция возвращает целое число фиксированного размера (обычно 64 бита)
    • Количество возможных ключей бесконечно (строки, объекты и т.д.)
    • По принципу Дирихле: если ключей больше, чем возможных хешей, коллизии неизбежны
  2. Ограниченный размер таблицы:

    # Даже с разными хешами ключи могут попасть в одну ячейку
    hash1 % table_size == hash2 % table_size
    # при разных hash1, hash2
    
  3. Особенности хеш-функции Python:

    # Для целых чисел (кроме -1)
    hash(1) == 1
    hash(1.0) == 1  # Коллизия между int и float!
    
    # Для строк хеш зависит от содержимого
    # Возможны "искусственные" коллизии
    

5. Пример создания коллизии

# Намеренное создание коллизии
class CollidingKey:
    def __init__(self, value):
        self.value = value
    
    # Фиксированный хеш для всех объектов
    def __hash__(self):
        return 42  # Всегда возвращаем один хеш!
    
    def __eq__(self, other):
        return isinstance(other, CollidingKey) and self.value == other.value

# Тестируем
d = {}
keys = [CollidingKey(i) for i in range(10)]

for i, key in enumerate(keys):
    d[key] = f"value{i}"
    print(f"Добавлен ключ {key.value}, размер таблицы: {d.__sizeof__()}")
    
# Все ключи будут храниться, несмотря на одинаковый хеш
print(f"Итоговый размер словаря: {len(d)}")  # 10

6. Как Python справляется с коллизиями?

Эффективные стратегии:

  1. Динамическое расширение таблицы:

    • При заполнении на 2/3 таблица увеличивается (обычно в 2 раза)
    • Все ключи перехешируются и перераспределяются
    # Коэффициент заполнения
    LOAD_FACTOR = 2/3  # Когда заполнено на 67%
    
  2. Адаптивный алгоритм поиска:

    • Комбинация первичного хеша и вторичного хеширования
    • Гарантирует равномерное распределение
  3. Кэширование хешей:

    • Для строк и некоторых типов хеш кэшируется
    • Ускоряет повторные операции

7. Производительность

Ситуация Время доступа
Нет коллизий O(1)
Несколько коллизий Почти O(1)
Много коллизий O(n) в худшем случае
# Измерение производительности
import time

# Нормальный случай
normal_dict = {i: i for i in range(1000)}

# Случай с коллизиями
colliding_dict = {CollidingKey(i): i for i in range(1000)}

# Время доступа будет разным!

8. Практические рекомендации

  1. Для пользовательских классов всегда определяйте __hash__ и __eq__ согласованно
  2. Избегайте мутируемых ключей (списки, словари, множества)
  3. Помните о перехешировании при больших объемах данных
# Хороший пример
class GoodKey:
    def __init__(self, name, id):
        self.name = name
        self.id = id
    
    def __hash__(self):
        # Используем кортеж из неизменяемых полей
        return hash((self.name, self.id))
    
    def __eq__(self, other):
        return (self.name, self.id) == (other.name, other.id)

Вывод

Коллизии в словарях Python возможны из-за конечности пространства хешей, но механизм открытой адресации с двойным хешированием эффективно их разрешает. Алгоритм обеспечивает амортизированную O(1) сложность операций даже при наличии коллизий, делая Python словари одним из самых быстрых ассоциативных массивов среди языков программирования.