Метод 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() не вернул значение по умолчанию_value, но указатель (?) на него.

Проблема может быть решена путем изменения кода на:

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

но это не похоже на хороший способ сделать это. Любые идеи, комментарии?

Ответ 1

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 лучше работает при больших размерах.

Ответ 2

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

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

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

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

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

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

EDIT: Я думал, что сделаю некоторые сравнения скорости с тайм-аутом. Значение 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 и проверка, если оно отсутствует.

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 синтаксис имеет наименьшее время (не знаю почему).

Ответ 3

В 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']

Ответ 4

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

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


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

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

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

Ответ 5

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

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)