На этом шаге мы рассмотрим одну интересную особенность словаря.
Иногда вы наталкиваетесь на крошечный пример кода, который обладает поистине неожиданной глубиной - одна-единственная строка кода, которая способна многому научить, если хорошенько над ней поразмыслить. Такой фрагмент код - это как коан в дзен-буддизме: вопрос или утверждение, используемое в практике дзен, чтобы вызвать сомнение и проверить достижения ученика.
Крошечный фрагмент кода, который мы обсудим на этом шаге, является одним из таких примеров. На первый взгляд он может выглядеть как прямолинейное выражение-словарь, но при ближайшем рассмотрении он отправляет вас в расширяющий сознание психоделический круиз по интерпретатору СPython.
Итак, без дальнейших церемоний, вот этот фрагмент кода. Возьмите паузу, чтобы поразмышлять над приведенным ниже выражением-словарем и тем, к чему его вычисление должно привести:
>>> {True: 'да', 1: 'нет', 1.0: 'возможно'}
Готовы?
Ниже показан результат, который мы получим при вычислении приведенного выше выражения-словаря в сеансе интерпретатора Python:
>>> {True: 'да', 1: 'нет', 1.0: 'возможно'}
{True: 'возможно'}
В первый моммент этот результат может привести в замешательство. Но все встанет на свои места, когда вы проведете неспешное пошаговое изучение того, что тут происходит. Давайте поразмыслим, почему мы получаем этот, надо сказать, весьма не интуитивный результат.
Когда Python обрабатывает наше выражение-словарь, он сначала строит новый пустой объект-словарь, а затем присваивает ему ключи и значения в том порядке, в каком они переданы в выражение-словарь.
Тогда, когда мы его разложим на части, наше выражение-словарь будет эквивалентно приведенной ниже последовательности инструкций, которые исполняются по порядку:
>>> xs = dict() >>> xs[True] = 'да' >>> xs[1] = 'нет' >>> xs[1.0] = 'возможно'
Как ни странно, Python считает все ключи, используемые в этом примере словаря, эквивалентными:
>>> True == 1 == 1.0 True
Ладно, но погодите минуточку. Уверен, вы сможете интуитивно признать, что 1.0 == 1, но вот почему True считается также эквивалентным и 1?
Немного покопавшись в документации Python, становится понятно, что Python рассматривает тип bool как подкласс типа int. Именно так обстоит дело в Python 2 и Python 3:
Булев тип - это подтип целочисленного типа, и булевы значения ведут себя, соответственно, как значения 0 и 1 почти во всех контекстах, при этом исключением является то, что при преобразовании в строковый тип, соответственно, возвращаются строковые значения 'False' или 'True'.
См. документацию Python "Иерархия стандартных типов": https://docs.python.org/3/reference/datamodel.html#the-standard-type-hierarchy.
И разумеется, это означает, что в Python булевы значения технически можно использовать в качестве индексов списка или кортежа:
>>> ['нет', 'да'][True] 'да'
Но вам, пожалуй, не следует использовать подобного рода логические переменные во имя ясности (и душевного здоровья ваших коллег).
Так или иначе, вернемся к нашему выражению-словарю.
Что касается языка Python, то все эти значения - True, 1 и 1.0 - представляют одинаковый ключ словаря. Когда интерпретатор вычисляет выражение-словарь, он неоднократно переписывает значение ключа True. Это объясняет, почему в самом конце результирующий словарь содержит всего один ключ.
Прежде чем мы пойдем дальше, взглянем еще раз на исходное выражение-словарь:
>>> {True: 'да', 1: 'нет', 1.0: 'возможно'}
{True: 'возможно'}
Почему здесь в качестве ключа мы по-прежнему получаем True? Разве не должен ключ из-за повторных присваиваний в самом конце тоже поменяться на 1.0?
После небольших изысканий в исходном коде интерпретатора Python было определено, что, когда с объектом-ключом ассоциируется новое значение, словари Python сам этот объект-ключ не обновляют:
>>> ys = {1.0: 'нет'}
>>> ys[True] = 'да'
>>> ys
{1.0: 'да'}
Безусловно, это имеет смысл в качестве оптимизации производительности: если ключи рассматриваются идентичными, то зачем тратить время на обновление оригинала?
В последнем примере вы видели, что первоначальный объект True как ключ никогда не заменяется. По этой причине строковое представление словаря по-прежнему печатает ключ как True (вместо 1 или 1.0).
С тем, что мы знаем теперь, по всей видимости, значения в результирующем словаре переписываются только потому, что сравнение всегда будет показывать их как эквивалентные друг другу. Вместе с тем оказывается, что этот эффект не является следствием проверки на эквивалентность методом __eq__() тоже.
Словари Python опираются на структуру данных хеш-таблица. Первая мысль при виде этого удивительного словаря заключалась в том, что такое поведение было как-то связано с хеш-конфликтами.
Дело в том, что хеш-таблица во внутреннем представлении хранит имеющиеся в ней ключи в различных "корзинах" в соответствии с хеш-значением каждого ключа. Хеш-значение выводится из ключа как числовое значение фиксированной длины, которое однозначно идентифицирует ключ.
Этот факт позволяет выполнять быстрые операции поиска. Намного быстрее отыскать числовое хеш-значение ключа в поисковой таблице, чем сравнивать полный объект-ключ со всеми другими ключами и выполнять проверку на эквивалентность.
Вместе с тем способы вычисления хеш-значений, как правило, не идеальны. И в конечном счете два или более ключа, которые на самом деле различаются, будут иметь одинаковое производное хеш-значение, и они в итоге окажутся в той же самой корзине поисковой таблицы.
Когда два ключа имеют одинаковое хеш-значение, такая ситуация называется хеш-конфликтом и является особым случаем, с которым должны разбираться алгоритмы вставки и нахождения элементов в хеш-таблице.
Исходя из этой оценки, весьма вероятно, что хеширование как-то связано с неожиданным результатом, который мы получили из нашего выражения-словаря. Поэтому давайте выясним, играют ли хеш-значения ключей здесь тоже какую-то определенную роль.
На следующем шаге мы закончим изучение этого вопроса.