Метод dict.get() возвращает указатель

Допустим, у меня есть этот код:

my_dict = {}
default_value = {'surname': '', 'age': 0}

# get info about john, or a default dict
item = my_dict.get('john', default_value)

# edit the data
item[surname] = 'smith'
item[age] = 68

my_dict['john'] = item

Проблема становится понятной, если мы теперь проверим значение default_value:

>>> default_value
{'age': 68, 'surname': 'smith'}

Очевидно, что my_dict.get() не возвращал значение default_value, но указатель (?) на него.

Проблему можно обойти, изменив код на:

item = my_dict.get('john', {'surname': '', 'age': 0})

но это не очень хороший способ сделать это. Есть идеи, комментарии?

5 ответов

Решение
item = my_dict.get('john', default_value.copy())

Вы всегда передаете ссылку на Python.

Это не имеет значения для неизменных объектов, таких как str, int, tuple и т. д., поскольку вы не можете изменить их, указывайте только имя на другом объекте, но это относится к изменяемым объектам, таким как list, set, а также dict, Вы должны привыкнуть к этому и всегда помнить об этом.

Изменить: Зак Блум и Джонатан Штернберг оба указывают методы, которые вы можете использовать, чтобы избежать вызова copy на каждом поиске. Вы должны использовать либо defaultdict метод, что-то вроде первого метода Джонатана, или:

def my_dict_get(key):
    try:
        item = my_dict[key]
    except KeyError:
        item = default_value.copy()

Это будет быстрее чем if когда ключ почти всегда уже существует в my_dict если dict большой. Вам не нужно заключать его в функцию, но вам, вероятно, не нужны эти четыре строки при каждом доступе my_dict,

См. Ответ Джонатана о времени с небольшим dict, get Метод работает плохо при всех размерах, которые я тестировал, но try метод лучше при больших размерах.

Не используйте get. Вы могли бы сделать:

item = my_dict.get('john', default_value.copy())

Но для этого необходимо скопировать словарь, даже если запись словаря существует. Вместо этого рассмотрите возможность проверки наличия значения.

item = my_dict['john'] if 'john' in my_dict else default_value.copy()

Единственная проблема с этим состоит в том, что он выполнит два поиска для "Джона" вместо одного. Если вы хотите использовать дополнительную строку (а значение "Нет" не является возможным значением из словаря), вы можете сделать следующее:

item = my_dict.get('john')
if item is None:
    item = default_value.copy()

РЕДАКТИРОВАТЬ: Я думал, что я сделаю некоторые сравнения скорости с timeit. Default_value и my_dict были глобальными. Я сделал их каждый для обоих, если ключ был там, и если была мисс.

Используя исключения:

def my_dict_get():
    try:
        item = my_dict['key']
    except KeyError:
        item = default_value.copy()

# key present: 0.4179
# key absent: 3.3799

Использование get и проверка, если это None.

def my_dict_get():
    item = my_dict.get('key')
    if item is None:
        item = default_value.copy()

# key present: 0.57189
# key absent: 0.96691

Проверка его существования с помощью специального синтаксиса if/else

def my_dict_get():
    item = my_dict['key'] if 'key' in my_dict else default_value.copy()

# key present: 0.39721
# key absent: 0.43474

Наивно копируем словарь.

def my_dict_get():
    item = my_dict.get('key', default_value.copy())

# key present: 0.52303 (this may be lower than it should be as the dictionary I used was one element)
# key absent: 0.66045

По большей части все, кроме того, которое использует исключения, очень похоже. Кажется, что специальный синтаксис if/else имеет наименьшее время по какой-то причине (понятия не имею, почему).

В Python dicts являются как объектами (поэтому они всегда передаются в виде ссылок), так и изменяемыми (то есть они могут быть изменены без повторного создания).

Вы можете копировать свой словарь каждый раз, когда используете его:

my_dict.get('john', default_value.copy())

Вы также можете использовать коллекцию defaultdict:

from collections import defaultdict

def factory():
  return {'surname': '', 'age': 0}

my_dict = defaultdict(factory)

my_dict['john']

Главное, что нужно понять, это то, что все в Python передается по ссылке. Имя переменной в языке C-стиля обычно является сокращением для области памяти в форме объекта, и присвоение этой переменной делает копию другой области в форме объекта... в Python переменные - это просто ключи в словаре (locals()), а акт о назначении просто хранит новую ссылку. (Технически, все это указатель, но это деталь реализации).

Это имеет ряд последствий, главное из которых состоит в том, что никогда не будет неявной копии объекта, созданного, потому что вы передали его функции, присвоили ее и т. Д. Единственный способ получить копию - это явно сделать это. Питон stdlib предлагает copy модуль, который содержит некоторые вещи, в том числе copy() а также deepcopy() функция, когда вы хотите явно сделать копию чего-либо. Кроме того, некоторые типы выставляют .copy() функции свои, но это не стандарт или последовательно реализуется. Другие, которые являются неизменными, как правило, предлагают .replace() метод, который делает мутированную копию.


В случае вашего кода передача в исходном экземпляре, очевидно, не работает, а создание копии заранее (когда вам это может не понадобиться) расточительно. Так что самое простое решение, вероятно,...

item = my_dict.get('john')
if item is None:
    item = default_dict.copy()

Было бы полезно в этом случае, если .get() поддерживается передача в функцию конструктора значений по умолчанию, но это, вероятно, слишком сложный базовый класс для пограничного случая.

Так как my_dict.get('john', default_value.copy()) будет создавать копию по умолчанию dict каждый раз, когда вызывается get (даже когда присутствует и возвращается 'john'), это быстрее и очень хорошо использовать эту опцию try/ исключением:

try:
    return my_dict['john']
except KeyError:
    return {'surname': '', 'age': 0}

Кроме того, вы также можете использовать defaultdict:

import collections

def default_factory():
    return {'surname': '', 'age': 0}

my_dict = collections.defaultdict(default_factory)
Другие вопросы по тегам