Варианты использования для метода dict 'setdefault'
Добавление collections.defaultdict
в Python 2.5 значительно сократилась потребность в dict
"s setdefault
метод. Этот вопрос для нашего коллективного образования:
- Что такое
setdefault
все еще полезно для, сегодня в Python 2.6/2.7? - Какие популярные случаи использования
setdefault
были замененыcollections.defaultdict
?
18 ответов
Ты мог бы сказать defaultdict
полезно для настройки параметров по умолчанию перед заполнением setdefault
полезно для установки значений по умолчанию во время или после заполнения dict.
Вероятно, наиболее распространенный вариант использования: группировка элементов (в несортированных данных, иначе использовать itertools.groupby
)
# really verbose
new = {}
for (key, value) in data:
if key in new:
new[key].append( value )
else:
new[key] = [value]
# easy with setdefault
new = {}
for (key, value) in data:
group = new.setdefault(key, []) # key might exist already
group.append( value )
# even simpler with defaultdict
new = defaultdict(list)
for (key, value) in data:
new[key].append( value ) # all keys have a default already
Иногда вы хотите убедиться, что определенные ключи существуют после создания dict. defaultdict
не работает в этом случае, потому что он создает ключи только при явном доступе. Думаю, вы используете что-то HTTP-иш со многими заголовками - некоторые являются необязательными, но вы хотите для них значения по умолчанию:
headers = parse_headers( msg ) # parse the message, get a dict
# now add all the optional headers
for headername, defaultvalue in optional_headers:
headers.setdefault( headername, defaultvalue )
Я обычно использую setdefault
для ключевых слов аргументов, например, в этой функции:
def notify(self, level, *pargs, **kwargs):
kwargs.setdefault("persist", level >= DANGER)
self.__defcon.set(level, **kwargs)
try:
kwargs.setdefault("name", self.client.player_entity().name)
except pytibia.PlayerEntityNotFound:
pass
return _notify(level, *pargs, **kwargs)
Он отлично подходит для настройки аргументов в оболочках вокруг функций, которые принимают аргументы ключевых слов.
Как говорится в большинстве ответов setdefault
или же defaultdict
позволит вам установить значение по умолчанию, когда ключ не существует. Тем не менее, я хотел бы отметить небольшую оговорку в отношении случаев использования setdefault
, Когда интерпретатор Python выполняется setdefault
он всегда оценивает второй аргумент функции, даже если ключ существует в словаре. Например:
In: d = {1:5, 2:6}
In: d
Out: {1: 5, 2: 6}
In: d.setdefault(2, 0)
Out: 6
In: d.setdefault(2, print('test'))
test
Out: 6
Как вы видете, print
также был выполнен, хотя 2 уже существует в словаре. Это становится особенно важным, если вы планируете использовать setdefault
например, для оптимизации, как memoization
, Если вы добавите рекурсивный вызов функции в качестве второго аргумента setdefault
Вы не получите никакой производительности, поскольку Python всегда будет вызывать функцию рекурсивно.
defaultdict
отлично, когда значение по умолчанию статично, как новый список, но не так сильно, если оно динамическое.
Например, мне нужен словарь для сопоставления строк с уникальными целочисленными значениями. defaultdict(int)
всегда будет использовать 0 для значения по умолчанию. Точно так же, defaultdict(intGen())
всегда выдает 1.
Вместо этого я использовал обычный dict:
nextID = intGen()
myDict = {}
for lots of complicated stuff:
#stuff that generates unpredictable, possibly already seen str
strID = myDict.setdefault(myStr, nextID())
Обратите внимание, что dict.get(key, nextID())
недостаточно, потому что мне нужно иметь возможность ссылаться на эти значения позже.
intGen
это крошечный класс, который я создаю, который автоматически увеличивает int и возвращает его значение:
class intGen:
def __init__(self):
self.i = 0
def __call__(self):
self.i += 1
return self.i
Если у кого-то есть способ сделать это с defaultdict
Я хотел бы увидеть это.
Как сказал Мухаммед, есть ситуации, в которых вы только иногда хотите установить значение по умолчанию. Прекрасным примером этого является структура данных, которая сначала заполняется, а затем запрашивается.
Рассмотрим три. При добавлении слова, если подузел необходим, но отсутствует, он должен быть создан для расширения дерева. При запросе наличия слова отсутствующий подузел указывает, что слово отсутствует и его не следует создавать.
Defaultdict не может этого сделать. Вместо этого следует использовать обычный dict с методами get и setdefault.
Я использую setdefault()
когда я хочу значение по умолчанию в OrderedDict
, Не существует стандартной коллекции Python, которая делает оба, но есть способы реализовать такую коллекцию.
Теоретически говоря, setdefault
все равно будет удобно, если вы иногда хотите установить значение по умолчанию, а иногда нет. В реальной жизни я не встречал такого случая использования.
Однако из стандартной библиотеки возникает интересный пример использования (Python 2.6, _threadinglocal.py):
>>> mydata = local()
>>> mydata.__dict__
{'number': 42}
>>> mydata.__dict__.setdefault('widgets', [])
[]
>>> mydata.widgets
[]
Я бы сказал, что с помощью __dict__.setdefault
это довольно полезный случай.
Изменить: Как это происходит, это единственный пример в стандартной библиотеке, и это в комментарии. Так что, может быть, этого недостаточно, чтобы оправдать существование setdefault
, Тем не менее, вот объяснение:
Объекты хранят свои атрибуты в __dict__
приписывать. Как это происходит, __dict__
Атрибут доступен для записи в любое время после создания объекта. Это также словарь, а не defaultdict
, Для объектов в общем случае не имеет смысла иметь __dict__
как defaultdict
потому что это сделало бы каждый объект, имеющий все юридические идентификаторы в качестве атрибутов. Так что я не могу предвидеть каких-либо изменений в объектах Python, избавляющихся от __dict__.setdefault
кроме полного удаления, если это было сочтено бесполезным.
Я переписал принятый ответ и предоставил его новичкам.
#break it down and understand it intuitively.
new = {}
for (key, value) in data:
if key not in new:
new[key] = [] # this is core of setdefault equals to new.setdefault(key, [])
new[key].append(value)
else:
new[key].append(value)
# easy with setdefault
new = {}
for (key, value) in data:
group = new.setdefault(key, []) # it is new[key] = []
group.append(value)
# even simpler with defaultdict
new = defaultdict(list)
for (key, value) in data:
new[key].append(value) # all keys have a default value of empty list []
Кроме того, я классифицировал методы как ссылки:
dict_methods_11 = {
'views':['keys', 'values', 'items'],
'add':['update','setdefault'],
'remove':['pop', 'popitem','clear'],
'retrieve':['get',],
'copy':['copy','fromkeys'],}
Вот несколько примеров setdefault, чтобы показать его полезность:
"""
d = {}
# To add a key->value pair, do the following:
d.setdefault(key, []).append(value)
# To retrieve a list of the values for a key
list_of_values = d[key]
# To remove a key->value pair is still easy, if
# you don't mind leaving empty lists behind when
# the last value for a given key is removed:
d[key].remove(value)
# Despite the empty lists, it's still possible to
# test for the existance of values easily:
if d.has_key(key) and d[key]:
pass # d has some values for key
# Note: Each value can exist multiple times!
"""
e = {}
print e
e.setdefault('Cars', []).append('Toyota')
print e
e.setdefault('Motorcycles', []).append('Yamaha')
print e
e.setdefault('Airplanes', []).append('Boeing')
print e
e.setdefault('Cars', []).append('Honda')
print e
e.setdefault('Cars', []).append('BMW')
print e
e.setdefault('Cars', []).append('Toyota')
print e
# NOTE: now e['Cars'] == ['Toyota', 'Honda', 'BMW', 'Toyota']
e['Cars'].remove('Toyota')
print e
# NOTE: it's still true that ('Toyota' in e['Cars'])
Один недостаток defaultdict
над dict
(dict.setdefault
) это defaultdict
объект создает новый элемент КАЖДЫЙ время дается несуществующий ключ (например, с ==
, print
). Так же defaultdict
класс, как правило, гораздо реже, чем dict
класс, его сложнее сериализировать его IME.
PS IMO-функции | методы, не предназначенные для изменения объекта, не должны изменять объект.
Другой вариант использования, о котором я не думаю, был упомянут выше. Иногда вы сохраняете кеш-объект объектов по их идентификатору, где основной экземпляр находится в кеше, и вы хотите установить кеш при отсутствии.
return self.objects_by_id.setdefault(obj.id, obj)
Это полезно, когда вы всегда хотите сохранить один экземпляр для отдельного идентификатора, независимо от того, как вы получаете объект каждый раз. Например, когда атрибуты объекта обновляются в памяти, а сохранение в хранилище откладывается.
Я часто использую setdefault, когда получаю это, устанавливая значение по умолчанию (!!!) в словаре; довольно часто словарь os.environ:
# Set the venv dir if it isn't already overridden:
os.environ.setdefault('VENV_DIR', '/my/default/path')
Менее кратко, это выглядит так:
# Set the venv dir if it isn't already overridden:
if 'VENV_DIR' not in os.environ:
os.environ['VENV_DIR'] = '/my/default/path')
Стоит отметить, что вы также можете использовать результирующую переменную:
venv_dir = os.environ.setdefault('VENV_DIR', '/my/default/path')
Но это менее необходимо, чем было до того, как существовали дефолты.
В дополнение к тому, что было предложено,
setdefault
может быть полезно в ситуациях, когда вы не хотите изменять уже установленное значение. Например, если у вас есть повторяющиеся номера, и вы хотите рассматривать их как одну группу. В этом случае, если вы столкнетесь с повторяющимся
duplicate
ключ, который уже был установлен, вы не будете обновлять значение этого ключа. Вы сохраните первое встреченное значение. Как будто вы повторяете / обновляете повторяющиеся ключи только один раз.
Вот пример кода записи индекса для ключей / элементов отсортированного списка:
nums = [2,2,2,2,2]
d = {}
for idx, num in enumerate(sorted(nums)):
# This will be updated with the value/index of the of the last repeated key
# d[num] = idx # Result (sorted_indices): [4, 4, 4, 4, 4]
# In the case of setdefault, all encountered repeated keys won't update the key.
# However, only the first encountered key's index will be set
d.setdefault(num,idx) # Result (sorted_indices): [0, 0, 0, 0, 0]
sorted_indices = [d[i] for i in nums]
Один очень важный пример использования, на который я наткнулся: dict.setdefault()
отлично подходит для многопоточного кода, когда вам нужен только один канонический объект (в отличие от нескольких объектов, которые оказываются равными).
Например, (Int)Flag
Enum в Python 3.6.0 содержит ошибку: если несколько потоков конкурируют за составной (Int)Flag
член, может оказаться более одного:
from enum import IntFlag, auto
import threading
class TestFlag(IntFlag):
one = auto()
two = auto()
three = auto()
four = auto()
five = auto()
six = auto()
seven = auto()
eight = auto()
def __eq__(self, other):
return self is other
def __hash__(self):
return hash(self.value)
seen = set()
class cycle_enum(threading.Thread):
def run(self):
for i in range(256):
seen.add(TestFlag(i))
threads = []
for i in range(8):
threads.append(cycle_enum())
for t in threads:
t.start()
for t in threads:
t.join()
len(seen)
# 272 (should be 256)
Решение заключается в использовании setdefault()
как последний шаг сохранения вычисленного составного элемента - если другой элемент уже был сохранен, он используется вместо нового, гарантируя уникальные элементы Enum.
[Править] Очень неправильно! Setdefault всегда запускает long_computation, а Python стремится к этому.
Расширяя ответ Таттла. Для меня лучшим вариантом использования является механизм кэширования. Вместо:
if x not in memo:
memo[x]=long_computation(x)
return memo[x]
который потребляет 3 строки и 2 или 3 поиска, я бы с радостью написал:
return memo.setdefault(x, long_computation(x))
Другой вариант использования для setdefault()
это когда вы не хотите перезаписывать значение уже установленного ключа. defaultdict
перезаписывает, пока setdefault()
не. Для вложенных словарей чаще всего требуется установить значение по умолчанию только в том случае, если ключ еще не установлен, поскольку вы не хотите удалять текущий вложенный словарь. Это когда вы используете setdefault()
,
Пример с defaultdict
:
>>> from collection import defaultdict()
>>> foo = defaultdict()
>>> foo['a'] = 4
>>> foo['a'] = 2
>>> print(foo)
defaultdict(None, {'a': 2})
setdefault
не перезаписывать:
>>> bar = dict()
>>> bar.setdefault('a', 4)
>>> bar.setdefault('a', 2)
>>> print(bar)
{'a': 4}
Мне нравится ответ, приведенный здесь:
http://stupidpythonideas.blogspot.com/2013/08/defaultdict-vs-setdefault.html
Короче говоря, решение (в приложениях, не критичных к производительности) должно приниматься исходя из того, как вы хотите обрабатывать поиск пустых ключей в нисходящем направлении (а именно. KeyError
по сравнению со значением по умолчанию).
Другой вариант использования в CPython заключается в том, что он является атомарным во всех случаях, тогда как не будет атомарным, если вы используете значение по умолчанию, созданное из лямбда.
cache = {}
def get_user_roles(user_id):
if user_id in cache:
return cache[user_id]['roles']
cache.setdefault(user_id, {'lock': threading.Lock()})
with cache[user_id]['lock']:
roles = query_roles_from_database(user_id)
cache[user_id]['roles'] = roles
Если два потока выполняются
cache.setdefault
при этом только один из них сможет создать значение по умолчанию.
Если вместо этого вы использовали defaultdict:
cache = defaultdict(lambda: {'lock': threading.Lock()}
Это приведет к состоянию гонки. В моем примере выше первый поток может создать блокировку по умолчанию, а второй поток может создать другую блокировку по умолчанию, а затем каждый поток может заблокировать свою собственную блокировку по умолчанию вместо желаемого результата каждого потока, пытающегося заблокировать одну блокировку.
Концептуально,
setdefault
в основном ведет себя так (defaultdict также ведет себя так, если вы используете пустой список, пустой dict, int или другое значение по умолчанию, которое не является пользовательским кодом Python, например лямбда):
gil = threading.Lock()
def setdefault(dict, key, value_func):
with gil:
if key not in dict:
return
value = value_func()
dict[key] = value
Концептуально,
defaultdict
в основном ведет себя так (только при использовании кода Python, такого как лямбда - это неверно, если вы используете пустой список):
gil = threading.Lock()
def __setitem__(dict, key, value_func):
with gil:
if key not in dict:
return
value = value_func()
with gil:
dict[key] = value