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

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

  1. Что такое setdefault все еще полезно для, сегодня в Python 2.6/2.7?
  2. Какие популярные случаи использования 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
Другие вопросы по тегам