Почему dict.get (ключ) работает, а не dict [ключ]?

17

Я пытаюсь сгруппировать двоичные строки определенных чисел на основе количества единиц в строке.

Это не работает:

s = "0 1 3 7 8 9 11 15"
numbers = map(int, s.split())
binaries = [bin(x)[2:].rjust(4, '0') for x in numbers]

one_groups = dict.fromkeys(range(5), [])
for x in binaries:
    one_groups[x.count('1')] += [x]

Ожидаемый словарь one_groupsдолжен быть

{0: ['0000'], 
 1: ['0001', '1000'], 
 2: ['0011', '1001'], 
 3: ['0111', '1011'], 
 4: ['1111']}

Но я получаю

{0: ['0000', '0001', '0011', '0111', '1000', '1001', '1011', '1111'], 
 1: ['0000', '0001', '0011', '0111', '1000', '1001', '1011', '1111'], 
 2: ['0000', '0001', '0011', '0111', '1000', '1001', '1011', '1111'], 
 3: ['0000', '0001', '0011', '0111', '1000', '1001', '1011', '1111'], 
 4: ['0000', '0001', '0011', '0111', '1000', '1001', '1011', '1111']}

Пока единственное, что сработало, это если я использую one_groups[x.count('1')] = one_groups.get(x.count('1')) + [x]вместоone_groups[x.count('1')] += [x]

Но почему это так? Если я правильно помню, не dict[key]должен возвращать значение этого словаря, как dict.get(key)работает? Я видел эту тему Почему dict.get (ключ) вместо dict [ключ]? но он не ответил на мой вопрос для этого конкретного случая, так как я точно знаю, что программа не предназначена для полученияKeyError

Я тоже пытался, one_groups[x.count('1')].append(x)но это тоже не работает.

SpectraXCD
источник
8
getвернуть, Noneесли ключ не существует, или любое предоставленное значение по умолчанию, в то время как оператор индекса []выдаст ошибку, если ключ не существует.
adnanmuttaleb
Sidenote, bin(x)[2:].rjust(4, '0')может быть упрощено до '{:0>4b}'.format(x).
19
1
Кстати, это помогает сделать минимальный воспроизводимый пример . В этом случае, как вы делаете binaries, не имеет отношения к вопросу, поэтому вы можете просто указать его значение.
wjandrea
1
Отвечает ли это на ваш вопрос? dict.fromkeys все указывают на тот же список
Георгий

Ответы:

24

Проблема в изменчивости:

one_groups = dict.fromkeys(range(5), [])- это передает тот же список в качестве значения для всех ключей . Поэтому, если вы измените одно значение, вы измените их все.

Это в основном то же самое, что сказать:

tmp = []
one_groups = dict.fromkeys(range(5), tmp)
del tmp

Если вы хотите использовать новый список, вам нужно сделать это в цикле - либо в явном forцикле, либо в понимании dict:

one_groups = {key: [] for key in range(5)}

Эта вещь будет «исполняться» [](что равно list()) для каждого ключа, создавая значения с различными списками.


Почему getработает? Потому что вы явно берете текущий список, но создаете +новый список результатов. И не важно, является ли это one_groups[x.count('1')] = one_groups.get(x.count('1')) + [x]или ... one_groups[x.count('1')] = one_groups[x.count('1')] + [x]важно то, что есть +.

Я знаю, как все говорят a+=b, просто a=a+b, но реализация может быть разной для оптимизации - в случае списков, +=просто .extendпотому, что мы знаем, что хотим получить наш результат в текущей переменной, поэтому создание нового списка будет пустой тратой памяти.

h4z3
источник
Ах да, понял. Я также помню, что у меня была похожая проблема, когда я хотел создать 2D-список с использованием mylist = [[] * 5] * 5и как mylist = [[] for x in range(5)] * 5бы это исправить. Просто для быстрого разъяснения, насколько я понял, это происходит из-за переменных, указывающих на адрес памяти этого пустого списка. Означает ли это также, что проблема не возникла бы, если бы я использовал вместо этого примитивы?
SpectraXCD
1
Да, если вы использовали примитивы, это решит это, но сломается, one_groups[x.count('1')] += [x]потому что вы не можете добавить список к примитивному типу. Лучшее решение - использовать defaultdict.
Фахер Мокадем
4
в частности, +вызывает __add__и возвращает новый объект, в то время как +=вызывает __iadd__и не требуется возвращать новый объект
njzk2
8

Проблема заключается в использовании one_groups = dict.fromkeys(range(5), [])

(Это передает тот же список в качестве значения всем ключам. Поэтому, если вы измените одно значение, вы измените их все)


Вы можете использовать это вместо: one_groups = {i:[] for i in range(5)}

(Эта вещь будет «выполнять» [] (что равно list ()) для каждого ключа, таким образом создавая значения с различными списками.)

Hameda169
источник
6
Вы абсолютно правы, хотя объяснение было бы действительно полезным. Это действительно не очевидно, в чем разница между двумя линиями.
Саймон Финк
Да, это плохо. извините
Hameda169
4

Это помощь по fromkeysметоду dict .

Справка по встроенной функции от клавиш:

Метод fromkeys (iterable, value = None, /) экземпляра builtins.type Создать новый словарь с ключами из iterable и значениями, установленными в value

Это говорит о том, что fromkeys примет значение, и даже если оно вызывается, оно сначала его оценит, а затем присвоит это значение всем ключам dict.

Списки изменчивы в Python, поэтому он назначит одну и ту же ссылку на пустой список, и одно изменение повлияет на них всех.

Вместо этого используйте defaultdict:

>>> from collections import defaultdict
>>> one_groups = defaultdict(list)
>>> for x in binaries:
      one_groups[x.count('1')] += [x]
>>> one_groups = dict(one_groups) # to stop default dict behavior

Это примет назначения для несуществующих ключей и значения по умолчанию будут пустыми списками (в этом случае).

Фахер Мокадем
источник