Варианты использования для метода dict 'setdefault'


192

Добавление collections.defaultdictв Python 2.5 значительно снижается необходимость dict«S setdefaultметода. Этот вопрос для нашего коллективного образования:

  1. Для чего setdefaultвсе еще полезно сегодня Python 2.6 / 2.7?
  2. Какие популярные варианты использования setdefaultбыли заменены collections.defaultdict?

1
Слегка связано тоже stackoverflow.com/questions/7423428/…
пользователь

Ответы:


208

Можно сказать, что defaultdictэто полезно для настроек по умолчанию перед заполнением dict и 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 
from collections import 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 )

1
Действительно, это ИМХО является основным вариантом использования для замены на defaultdict. Можете ли вы привести пример того, что вы имеете в виду в первом абзаце?
Эли Бендерский

2
Мухаммед Алкарури: Сначала вы должны скопировать текст, а затем перезаписать некоторые элементы. Я тоже часто этим занимаюсь, и я думаю, что это идиома, которую я предпочитаю setdefault. defaultdict с другой стороны, не сработало бы, если бы не все defaultvaluesбыли равны (то есть некоторые есть, 0а некоторые есть []).
Йохен Ритцель

2
@ YHC4k, да. Вот почему я использовалheaders = dict(optional_headers) . Для случая, когда значения по умолчанию не все равны. И конечный результат такой же, как если бы вы сначала получили заголовки HTTP, а затем установили значения по умолчанию для тех, кого вы не получили. И это очень удобно, если у вас уже есть optional_headers. Попробуйте мой двухэтапный код и сравните его с вашим, и вы поймете, что я имею в виду.
Мухаммед Алкарури

19
или просто сделать new.setdefault(key, []).append(value)
фмалина

2
Я нахожу странным, что лучший ответ сводится к тому, defaultdictчто даже лучше, чем setdefault(так, где сейчас сценарий использования?). Кроме того, ChainMapлучше бы справиться сhttp примером, ИМО.
YvesgereY

29

Я обычно использую 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)

Он отлично подходит для настройки аргументов в оболочках вокруг функций, которые принимают аргументы ключевых слов.


16

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 см. в этом вопросе: stackoverflow.com/questions/2912231/…
weronika

8
Вы можете заменить intGenна itertools.count().next.
Сурьма

7
nextID()Значение будет увеличиваться при каждом myDict.setdefault()вызове, даже если возвращаемое значение не используется как strID. Это кажется расточительным и иллюстрирует одну из вещей, которые мне не нравятся setdefault()в целом, а именно то, что он всегда оценивает свой defaultаргумент независимо от того, используется ли он на самом деле.
Мартино

Вы можете сделать это с defaultdict: myDict = defaultdict(lambda: nextID()). Позже strID = myDict[myStr]в цикле.
Musiphil

3
Чтобы получить поведение, которое вы описываете с defaultdict, почему бы не просто myDict = defaultdict(nextID)?
сорок два

10

Я использую, setdefault()когда я хочу значение по умолчанию в OrderedDict. Существует не стандартный набор Python , который делает как, но есть способы , чтобы реализовать такую коллекцию.


10

Поскольку большинство ответов указывают 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 всегда будет вызывать функцию рекурсивно.

Поскольку упоминание было упомянуто, лучшей альтернативой является использование декоратора functools.lru_cache, если вы планируете усовершенствовать функцию с помощью памятки. lru_cache лучше обрабатывает требования кеширования для рекурсивной функции.


8

Как сказал Мухаммед, есть ситуации, в которых вы только иногда хотите установить значение по умолчанию. Отличным примером этого является структура данных, которая сначала заполняется, а затем запрашивается.

Рассмотрим три. При добавлении слова, если подузел необходим, но отсутствует, он должен быть создан для расширения дерева. При запросе наличия слова отсутствующий подузел указывает, что слово отсутствует и его не следует создавать.

Defaultdict не может этого сделать. Вместо этого следует использовать обычный dict с методами get и setdefault.


5

Теоретически, 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, за исключением полного удаления, если это будет сочтено бесполезным.


1
Не могли бы вы уточнить - что делает _dict .setdefault особенно полезным?
Эли Бендерский

1
@ Эли: Я думаю, что дело в том, что __dict__реализация, а dictне defaultdict.
Катриэль

1
Хорошо. Я не против setdefaultостаться в Python, но любопытно, что теперь это почти бесполезно.
Эли Бендерский

@Eli: я согласен. Я не думаю, что есть достаточно причин, чтобы представить его сегодня, если бы его там не было. Но, находясь там, было бы трудно поспорить за его удаление, учитывая весь код, уже использующий его.
Мухаммед Алкарури

1
Файл под защитным программированием. setdefaultясно указывает, что вы присваиваете dict ключ, который может существовать или не существовать, и, если он не существует, вы хотите, чтобы он был создан со значением по умолчанию: например d.setdefault(key,[]).append(value). В другом месте в программе вы делаете, alist=d[k]где вычисляется k, и вы хотите, чтобы исключение было выброшено, если k не в d (что может потребоваться по умолчанию, assert k in dили дажеif not ( k in d): raise KeyError
nigel222

3

Один из недостатков defaultdictover dict( dict.setdefault) состоит в том, что defaultdictобъект создает новый элемент КАЖДОЙ ВРЕМЯ, если задан несуществующий ключ (например, с ==, print). Кроме того, defaultdictкласс, как правило, гораздо реже, чемdict класс, его сложнее сериализовать в IME.

PS IMO функции | методы, не предназначенные для изменения объекта, не должны изменять объект.


Не нужно каждый раз создавать новый объект. Вы можете так же легко сделать defaultdict(lambda l=[]: l)вместо этого.
Artyer

6
Никогда не делайте того, что предлагает @Artyer - изменяемые настройки по умолчанию вас укусят.
Брэндон Хамперт

2

Вот несколько примеров 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'])

2

Я переписал принятый ответ и предоставил его новичкам.

#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'],}

1

Я часто использую 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')

Но это менее необходимо, чем это было до существования дефолтов.


1

Другой вариант использования, о котором я не думаю, был упомянут выше. Иногда вы сохраняете кеш-объект объектов по их идентификатору, где основной экземпляр находится в кеше, и вы хотите установить кеш при отсутствии.

return self.objects_by_id.setdefault(obj.id, obj)

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


1

Один очень важный пример использования, на который я наткнулся: dict.setdefault() отлично подходит для многопоточного кода, когда вам нужен только один канонический объект (в отличие от нескольких объектов, которые оказываются равными).

Например, (Int)FlagEnum в 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.


0

[Править] Очень неправильно!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))

Хороший пример. Я все еще думаю, что 3 строки более понятны, но, возможно, мой мозг вырастет, чтобы оценить setdefault.
Боб Стейн

5
Это не эквивалентно. Во первых, long_computation(x)называется только если x not in memo. Тогда как во втором long_computation(x)всегда называется. Только присвоение является условным, эквивалентный код setdefaultбудет выглядеть так: v = long_computation(x)/ if x not in memo:/ memo[x] = v.
Дэн Д.

0

Мне нравится ответ, приведенный здесь:

http://stupidpythonideas.blogspot.com/2013/08/defaultdict-vs-setdefault.html

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


0

Другой вариант использования 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}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.