Подсчет коллизий в словаре Python

моя первая публикация здесь, поэтому надеюсь, что я задал свой вопрос правильным образом,

После добавления элемента в словарь Python можно ли заставить Python сказать, добавляет ли этот элемент столкновение? (И сколько мест исследовали стратегию разрешения столкновений, прежде чем найти место для размещения элемента?)

Моя проблема: я использую словари как часть более крупного проекта, и после обширного профилирования я обнаружил, что самая медленная часть кода имеет дело с редкой матрицей расстояний, реализованной с использованием словарей.

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

Итак, например, с учетом следующего словаря:

d = {}
for i in xrange(15000):
    d[random.randint(15000000, 18000000)] = 0

Вы можете заставить Python рассказать вам, сколько конфликтов произошло при его создании?

Мой фактический код запутан с приложением, но приведенный выше код делает словарь, который очень похож на те, которые я использую.

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

Спасибо за вашу помощь.

Изменить: Некоторый код для реализации решения @Winston Ewert:

n = 1500
global collision_count
collision_count = 0

class Foo():

    def __eq__(self, other):
        global collision_count
        collision_count += 1
        return id(self) == id(other)

    def __hash__(self):
        #return id(self) # @John Machin: yes, I know!
        return 1

objects = [Foo() for i in xrange(n)]

d = {}
for o in objects:
    d[o] = 1

print collision_count

Обратите внимание, что когда вы определяете __eq__ в классе, Python дает вам TypeError: unhashable instance, если вы также не определяете функцию __hash__.

Он работает не так, как я ожидал. Если у вас есть функция __hash__ return 1, то вы получите массу коллизий, как ожидалось (1125560 коллизий для n = 1500 в моей системе). Но с return id(self) существует 0 столкновений.

Кто-нибудь знает, почему это говорит о столкновении?

Edit: Возможно, я понял это.

Это потому, что __eq__ вызывается только в том случае, если значения __hash__ для двух объектов одинаковы, а не для их "хрусткой версии" (как выразился @John Machin)?

Ответы

Ответ 1

Короткий ответ:

Вы не можете имитировать использование идентификаторов объектов в виде ключей dict, используя случайные целые числа в качестве ключей dict. Они имеют разные хэш-функции.

Столкновения происходят. "Наличие уникальных штучек означает отсутствие столкновений" неверно для нескольких значений "thingy".

Вы не должны беспокоиться о столкновениях.

Длинный ответ:

Некоторые объяснения, полученные из чтение исходного кода:

A dict реализуется как таблица из 2 ** я записей, где я - целое число.

dicts не превышает 2/3. Следовательно, для 15000 ключей я должен быть 15 и 2 ** я - 32768.

Если o - произвольный экземпляр класса, который не определяет __hash__(), это НЕ верно, что hash (o) == id (o). Поскольку адрес, вероятно, имеет нули в младших 3 или 4 битах, хэш создается путем поворота адреса на 4 бита; см. исходный файл Objects/object.c, function _Py_HashPointer

Было бы проблемой, если бы было много нулей в младших битах, потому что для доступа к таблице размера 2 ** я (например, 32768) хеш-значение (часто намного больше) должно быть хрустело чтобы соответствовать, и это делается очень просто и быстро, взяв младший бит я (например, 15) хэш-значения.

Следовательно, столкновения неизбежны.

Однако это не вызывает паники. Остальные биты хэш-значения учитываются при расчете того, где будет следующий пробник. Вероятность необходимости 3-го и т.д. Зонда должна быть довольно небольшой, тем более, что дикты не превышают 2/3. Стоимость нескольких зондов снижается за счет дешевой стоимости расчета слота для первого и последующих зондов.

Ниже приведен простой пример, иллюстрирующий большую часть приведенного выше обсуждения. Он предполагает случайный доступ к dict после того, как он достиг своего максимального размера. С Python2.7.1 он показывает около 2000 столкновений для 15000 объектов (13.3%).

В любом случае суть в том, что вы должны действительно отвлечь свое внимание в другом месте. Коллизии не являются вашей проблемой, если вы не достигли чрезвычайно необычного способа получения памяти для своих объектов. Вы должны посмотреть, как вы используете дикты, например. используйте k in d или try/except, а не d.has_key(k). Рассмотрим, что один диск получил доступ как d[(x, y)] вместо двух уровней, доступных как d[x][y]. Если вам нужна помощь, задайте отдельный вопрос.

Обновить после тестирования на Python 2.6:

Вращение адреса не было введено до Python 2.7; см. этот отчет об ошибках для всестороннего обсуждения и тестов. Основные выводы IMHO по-прежнему действительны и могут быть дополнены "Обновить, если вы можете".

>>> n = 15000
>>> i = 0
>>> while 2 ** i / 1.5 < n:
...    i += 1
...
>>> print i, 2 ** i, int(2 ** i / 1.5)
15 32768 21845
>>> probe_mask = 2 ** i - 1
>>> print hex(probe_mask)
0x7fff
>>> class Foo(object):
...     pass
...
>>> olist = [Foo() for j in xrange(n)]
>>> hashes = [hash(o) for o in olist]
>>> print len(set(hashes))
15000
>>> probes = [h & probe_mask for h in hashes]
>>> print len(set(probes))
12997
>>>

Ответ 2

Эта идея на самом деле не работает, см. обсуждение в вопросе.

Быстрый просмотр реализации C на python показывает, что код для разрешения конфликтов не вычисляет или не сохраняет количество столкновений.

Однако он будет вызывать PyObject_RichCompareBool на клавишах, чтобы проверить, совпадают ли они. Это означает, что __eq__ на ключе будет вызываться для каждого столкновения.

Итак:

Замените свои ключи объектами, которые определяют __eq__ и увеличивают счетчик при его вызове. Это будет медленнее из-за накладных расходов, связанных с переходом на python для сравнения. Однако это должно дать вам представление о том, сколько коллизий происходит.

Убедитесь, что в качестве ключа используются разные объекты, иначе python будет использовать ярлык, потому что объект всегда равен самому себе. Кроме того, убедитесь, что хэш объектов имеет то же значение, что и исходные ключи.

Ответ 3

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