Выбор Python 3.5 для выбора ключей при сравнении их в словаре

При построении словаря следующим образом:

dict = { True: 'yes', 1: 'No'}

Когда я запускаю его в интерактивном интерпретаторе Python, dict представляется таким образом:

dict = {True: 'No'}

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

Я не понимаю, почему True выбран как клавиша вместо 1.

Я что-то упустил?

Ответ 1

Словари реализуются как хеш-таблицы, и при добавлении ключей/значений здесь есть два важных понятия: хеширование и равенство.

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

Если строка хеш-таблицы пуста, отлично: новый ключ/значение может быть вставлен в словарь, заполняя пустую строку.

Однако, если в этой строке уже есть что-то, Python должен проверить ключи на равенство. Если ключи равны (используя ==), то они считаются одним и тем же ключом, и Python просто нуждается в обновлении соответствующего значения в этой строке.

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


Когда вы пишете {True: 'yes', 1: 'No'}, вы сообщаете Python о создании нового словаря и затем заполняете его двумя парами ключ/значение. Они обрабатываются слева направо: True: 'yes', затем 1: 'No'.

Мы имеем hash(True) равно 1. Ключ True входит в строку 1 в хеш-таблице, а строка 'yes' - его значение.

Для следующей пары Python видит, что hash(1) также равно 1 и поэтому смотрит на строку 1 таблицы. Там что-то уже есть, так что теперь Python проверяет ключи на равенство. Мы имеем 1 == True, поэтому 1 считается тем же ключом, что и True, и поэтому его соответствующее значение изменяется на строку 'No'.

В результате получается словарь с одной записью: {True: 'No'}.


Если вы хотите посмотреть на кишки CPython 3.5, чтобы увидеть, что создание словаря выглядит ниже уровня поверхности Python, здесь более подробно.

  • Код Python {True: 'yes', 1: 'No'} анализируется в токенах и передается компилятору. Учитывая синтаксис, Python знает, что словарь должен быть создан с использованием значений внутри фигурных скобок. Байт-код для загрузки четырех значений в стек виртуальной машины (LOAD_CONST) и последующего создания словаря (BUILD_MAP) помещается в очередь.

  • Четыре постоянных значения помещаются в верхнюю часть стека в том порядке, в котором они видны:

    'No'
    1
    'yes'
    True
    
  • Затем вызывается код операции BUILD_MAP с аргументом 2 (Python подсчитывает две пары ключ/значение). Этот код операции отвечает за создание словаря из элементов в стеке. Это выглядит как this:

    TARGET(BUILD_MAP) {
        int i;
        PyObject *map = _PyDict_NewPresized((Py_ssize_t)oparg);
        if (map == NULL)
            goto error;
        for (i = oparg; i > 0; i--) {
            int err;
            PyObject *key = PEEK(2*i);
            PyObject *value = PEEK(2*i - 1);
            err = PyDict_SetItem(map, key, value);
            if (err != 0) {
                Py_DECREF(map);
                goto error;
            }
        }
    
        while (oparg--) {
            Py_DECREF(POP());
            Py_DECREF(POP());
        }
        PUSH(map);
        DISPATCH();
    }
    

Ниже перечислены три ключевых шага:

  • Пустая хэш-таблица создается с помощью _PyDict_NewPresized. Маленькие словари (всего несколько элементов, например 2 в этом случае) нуждаются в таблице с восемью рядами.

  • Вводится цикл for, начинающийся с 2 (в данном случае) и отсчет до 0. PEEK(n) - макрос, который указывает на n-й элемент вниз по стеку. Поэтому на первой итерации цикла мы будем иметь

PyObject *key = PEEK(2*2);       /* item 4 down the stack */  
PyObject *value = PEEK(2*2 - 1); /* item 3 down the stack */

Это означает, что *key будет True, а *value будет 'yes' в первом цикле. На втором будет 1 и 'No'.

  1. PyDict_SetItem вызывается в каждом цикле, чтобы поместить текущие *key и *value в словарь. Это та же самая функция, которая вызывается при написании dictionary[key] = value. Он вычисляет хэш ключа для разработки, где сначала искать хэш-таблицу, а затем, при необходимости, сравнивать ключ с любым существующим ключом в этой строке (как обсуждалось выше).

Ответ 2

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

>>> True == 1
True
>>> hash(1)
1
>>> hash(True)
1

Теперь рассмотрим байт-код:

import dis
dis.dis("Dic = { True: 'yes', 1: 'No'}")

Отпечатки:

      0 LOAD_CONST               0 (True)
      3 LOAD_CONST               1 ('yes')
      6 LOAD_CONST               2 (1)
      9 LOAD_CONST               3 ('No')
     12 BUILD_MAP                2
     15 STORE_NAME               0 (Dic)
     18 LOAD_CONST               4 (None)
     21 RETURN_VALUE

В основном происходит то, что dict literal символизируется ключами и значениями, и они помещаются в стек. После этого BUILD_MAP 2 скрывает две пары (ключи, значения) в словаре.

Скорее всего, порядок данных в стеке (который, по-видимому, определяется порядком ключей в dict literal) и детали реализации BUILD_MAP, решает получающиеся ключи и значения ключей.

Похоже, что назначение ключа-значения выполняется в порядке, определенном в dict-литерале. Назначение ведет себя так же, как операция d[key] = value, поэтому в основном:

  • если key не находится в dict (по равенству): добавьте key do dict
  • сохранить value под key

Учитывая {True: 'yes', 1: 'No'}:

  • Начните с {}
  • Добавить (True, 'yes')

    • True не в dict → (True, hash(True)) == (True, 1) - это новый ключ в dict
    • Обновить значение для ключа, равного 1 - 'yes'
  • Добавить (1, 'no')

    • 1 находится в dict (1 == True) → нет необходимости в новом ключе в словаре
    • Обновить значение для ключа, равного 1 (True) со значением 'no'

Результат: {True: 'No'}

Как я прокомментировал, я не знаю, гарантировано ли это спецификациями Python или это просто поведение, определяемое реализацией на основе CPython, оно может отличаться в других реализациях интерпретатора.

Ответ 3

True и 1 - разные объекты, но оба они имеют одинаковое значение:

>>> True is 1
False
>>> True == 1
True

Это похоже на две строки, которые могут иметь одно и то же значение, но сохраняются в разных ячейках памяти:

>>> x = str(12345)
>>> y = str(12345)
>>> x == y 
True
>>> x is y
False

Первый словарь добавлен в словарь; то, когда добавляется другое, это значение уже существует как ключ. Таким образом, ключ обновляется, значения ключа уникальны.

>>> {x: 1, y: 2}
{"12345": 2}

Ответ 4

Если ключ уже присутствует в словаре, он не переопределяет ключ только связанное с ним значение.

Я считаю, что запись x = {True:"a", 1:"b"} выполняется по строкам:

x = {}
x[True] = "a"
x[1] = "b"

и к моменту достижения x[1] = "b" ключ True уже находится в dict, поэтому зачем его менять? почему бы не просто переопределить связанное значение.

Ответ 5

Кажется, что причиной является упорядочение. Пример кода:

>>> d = {True: 'true', 1: 'one'}
>>> d
{True: 'one'}
>>> d = {1: 'one', True: 'true'}
>>> d
{1: 'true'}

Ответ 6

Это двусмысленное утверждение.

Логика: d = { True: 'no', 1: 'yes'}

Когда python оценивает выражение, он делает это последовательно, поэтому он эквивалентен этому.

d = dict() d[True] = 'no' d[1] = 'yes'

Константа True - это ключ, но она оценивается как 1, поэтому вы просто устанавливаете значение для ключа дважды.