Почему dict-ключи поддерживают вычитание списка, но не вычитание кортежа?

Предположительно, dict_keys должны вести себя как объект, подобный множеству, но они не имеют метода difference, и поведение вычитания, по-видимому, расходится.

>>> d = {0: 'zero', 1: 'one', 2: 'two', 3: 'three'}
>>> d.keys() - [0, 2]
{1, 3}
>>> d.keys() - (0, 2)
TypeError: 'int' object is not iterable

Почему класс dict_keys пытается перебрать целое число здесь? Разве это не нарушает утиную печать?


>>> dict.fromkeys(['0', '1', '01']).keys() - ('01',)
{'01'}
>>> dict.fromkeys(['0', '1', '01']).keys() - ['01',]
{'1', '0'}

Ответы

Ответ 1

Это выглядит как ошибка. Реализация заключается в преобразовании dict_keys в set, а затем вызовите .difference_update(arg) на нем.

Похоже, что они неправильно использовали _PyObject_CallMethodId (оптимизированный вариант PyObject_CallMethod), передав строку формата только "O". Вещь, PyObject_CallMethod и друзья документированы, чтобы потребовать строку формата Py_BuildValue, которая должна "создать tuple" . С более чем одним кодом формата он автоматически переносит значения в tuple, но только с одним кодом формата, он не tuple, он просто создает значение (в этом случае, поскольку оно уже PyObject*, все, что он делает, - это увеличение счетчика ссылок).

Пока я не отслеживал, где это возможно, я подозреваю, что где-то во внутренних документах он идентифицирует вызовы CallMethod, которые не производят tuple, и обертывают их, чтобы создать один элемент tuple, поэтому вызываемая функция может фактически принимать аргументы в ожидаемом формате. При вычитании a tuple он уже a tuple, и этот код исправления никогда не активируется; при прохождении a list он делает, становясь одним элементом tuple, содержащим list.

difference_update принимает varargs (как если бы он был объявлен def difference_update(self, *args)). Поэтому, когда он получает развернутый tuple, он считает, что он должен вычитать элементы из каждой записи в tuple, а не обрабатывать упомянутые записи как значения для вычитания самих себя. Чтобы проиллюстрировать, когда вы выполните:

mydict.keys() - (1, 2)

ошибка вызывает его (грубо):

result = set(mydict)
# We've got a tuple to pass, so all well...
result.difference_update(*(1, 2)) # Unpack behaves like difference_update(1, 2)
# OH NO!

В то время как:

mydict.keys() - [1, 2]

делает:

result = set(mydict)
# [1, 2] isn't a tuple, so wrap
result.difference_update(*([1, 2],)) # Behaves like difference_update([1, 2])
# All well

Итак, почему tuple из str работает (неправильно), - ('abc', '123') выполняет вызов, эквивалентный:

result.difference_update(*('abc', '123'))
# or without unpacking:
result.difference_update('abc', '123')

и поскольку str являются итерабельными их символами, он просто blithely удаляет записи для 'a', 'b', 'c' и т.д. вместо 'abc' и '123', как вы ожидали.

В принципе, это ошибка, и (когда я получаю шанс), я отправлю ее против людей CPython.

Правильное поведение, вероятно, должно было быть вызвано (предполагается, что этот вариант Id существует для этого API):

_PyObject_CallMethodObjArgsId(result, &PyId_difference_update, other, NULL);

который вообще не имел бы проблем с упаковкой, и будет работать быстрее для загрузки; наименьшее изменение заключалось бы в изменении строки формата на "(O)", чтобы принудительное создание tuple даже для одного элемента, но поскольку строка формата ничего не получает, _PyObject_CallMethodObjArgsId лучше.