Создание словаря с помощью клавиш и измененных объектов. Сюрприз
Я столкнулся с таким поведением, которое меня удивило в Python 2.6 и 3.2:
>>> xs = dict.fromkeys(range(2), [])
>>> xs
{0: [], 1: []}
>>> xs[0].append(1)
>>> xs
{0: [1], 1: [1]}
Тем не менее, в dict
, dict
в 3.2, показано более вежливое поведение:
>>> xs = {i:[] for i in range(2)}
>>> xs
{0: [], 1: []}
>>> xs[0].append(1)
>>> xs
{0: [1], 1: []}
>>>
Почему от fromkeys
ведут себя так?
Ответы
Ответ 1
Ваш пример Python 2.6 эквивалентен следующему, что может помочь уточнить:
>>> a = []
>>> xs = dict.fromkeys(range(2), a)
Каждая запись в результирующем словаре будет иметь ссылку на тот же объект. Эффекты мутирования этого объекта будут видны через каждую запись, как вы видели, потому что это один объект.
>>> xs[0] is a and xs[1] is a
True
Используйте понимание dict, или если вы застряли на Python 2.6 или старше, и у вас нет понимания словаря, вы можете получить поведение понимания dict, используя dict()
с выражением генератора:
xs = dict((i, []) for i in range(2))
Ответ 2
В первой версии вы используете тот же пустой объект списка, что и значение для обоих ключей, поэтому, если вы его измените, вы также измените другой.
Посмотрите на это:
>>> empty = []
>>> d = dict.fromkeys(range(2), empty)
>>> d
{0: [], 1: []}
>>> empty.append(1) # same as d[0].append(1) because d[0] references empty!
>>> d
{0: [1], 1: [1]}
Во второй версии создается новый пустой объект списка на каждой итерации понимания dict, поэтому оба они независимы друг от друга.
Что касается "почему" fromkeys()
работает так - ну, было бы удивительно, если бы это не сработало. fromkeys(iterable, value)
строит новый dict с ключами из итерируемого, что все имеют значение value
. Если это значение является изменчивым объектом, и вы изменяете этот объект, что еще вы можете ожидать разумно?
Ответ 3
Чтобы ответить на заданный вопрос: fromkeys
ведет себя так, потому что другого разумного выбора нет. fromkeys
(или даже возможно) иметь из fromkeys
решить, fromkeys
ли ваш аргумент и будет ли он делать новые копии каждый раз. В некоторых случаях это не имеет смысла, а в других это просто невозможно.
Второй аргумент, который вы проходите, является, таким образом, просто ссылкой и копируется как таковой. Назначение []
в Python означает "единственная ссылка на новый список", а не "создавать новый список при каждом доступе к этой переменной". Альтернативой было бы передать функцию, которая генерирует новые экземпляры, которые являются функциональностью, которую предоставляет для вас понимание понятий dict.
Вот несколько вариантов создания нескольких фактических копий изменяемого контейнера:
-
Как вы упомянули в этом вопросе, ошибки dict позволяют вам выполнить произвольный оператор для каждого элемента:
d = {k: [] for k in range(2)}
Важно то, что это эквивалентно назначению k = []
в цикле for
. Каждая итерация создает новый список и присваивает его значению.
-
Используйте форму конструктора dict
предложенную @Andrew Clark:
d = dict((k, []) for k in range(2))
Это создает генератор, который снова выполняет присвоение нового списка каждой паре ключ-значение при его выполнении.
-
Используйте файл collections.defaultdict
вместо обычного dict
:
d = collections.defaultdict(list)
Этот вариант немного отличается от других. Вместо того, чтобы создавать новые списки вверх, defaultdict
будет вызывать list
каждый раз, когда вы получаете доступ к ключу, который еще не существует. Вы можете добавить ключи так же лениво, как вы хотите, что иногда может быть очень удобным:
for k in range(2):
d[k].append(42)
Поскольку вы создали фабрику для новых элементов, это будет вести себя точно так же, как вы ожидали от fromkeys
чтобы вести себя в исходном вопросе.
-
Используйте dict.setdefault
при доступе к потенциально новым ключам. Это делает что-то похожее на то, что делает defaultdict
, но имеет то преимущество, что он более контролируется, в том смысле, что только доступ, который вы хотите создать новые ключи, фактически создает их:
d = {}
for k in range(2):
d.setdefault(k, []).append(42)
Недостатком является то, что новый пустой объект списка создается каждый раз, когда вы вызываете функцию, даже если она никогда не привязана к значению. Это не огромная проблема, но она может складываться, если вы часто ее вызываете и/или ваш контейнер не так прост, как list
.