Создание словаря с помощью клавиш и измененных объектов. Сюрприз

Я столкнулся с таким поведением, которое меня удивило в 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.

Вот несколько вариантов создания нескольких фактических копий изменяемого контейнера:

  1. Как вы упомянули в этом вопросе, ошибки dict позволяют вам выполнить произвольный оператор для каждого элемента:

    d = {k: [] for k in range(2)}
    

    Важно то, что это эквивалентно назначению k = [] в цикле for. Каждая итерация создает новый список и присваивает его значению.

  2. Используйте форму конструктора dict предложенную @Andrew Clark:

    d = dict((k, []) for k in range(2))
    

    Это создает генератор, который снова выполняет присвоение нового списка каждой паре ключ-значение при его выполнении.

  3. Используйте файл collections.defaultdict вместо обычного dict:

    d = collections.defaultdict(list)
    

    Этот вариант немного отличается от других. Вместо того, чтобы создавать новые списки вверх, defaultdict будет вызывать list каждый раз, когда вы получаете доступ к ключу, который еще не существует. Вы можете добавить ключи так же лениво, как вы хотите, что иногда может быть очень удобным:

    for k in range(2):
        d[k].append(42)
    

    Поскольку вы создали фабрику для новых элементов, это будет вести себя точно так же, как вы ожидали от fromkeys чтобы вести себя в исходном вопросе.

  4. Используйте dict.setdefault при доступе к потенциально новым ключам. Это делает что-то похожее на то, что делает defaultdict, но имеет то преимущество, что он более контролируется, в том смысле, что только доступ, который вы хотите создать новые ключи, фактически создает их:

    d = {}
    for k in range(2):
        d.setdefault(k, []).append(42)
    

    Недостатком является то, что новый пустой объект списка создается каждый раз, когда вы вызываете функцию, даже если она никогда не привязана к значению. Это не огромная проблема, но она может складываться, если вы часто ее вызываете и/или ваш контейнер не так прост, как list.