Casos de uso para o método dict 'setdefault'

192

A adição do collections.defaultdictPython 2.5 reduziu bastante a necessidade dictdo setdefaultmétodo. Esta pergunta é para a nossa educação coletiva:

  1. O que setdefaultainda é útil hoje em Python 2.6 / 2.7?
  2. Quais casos de uso populares de setdefault foram substituídos collections.defaultdict?
Eli Bendersky
fonte
1
Também ligeiramente relacionado stackoverflow.com/questions/7423428/…
user

Respostas:

208

Você poderia dizer que defaultdicté útil para definir padrões antes de preencher o ditado e setdefaulté útil para definir padrões durante ou após o preenchimento do ditado .

Provavelmente o caso de uso mais comum: Agrupando itens (em dados não classificados, senão use 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

Às vezes, você deseja garantir a existência de chaves específicas após a criação de um ditado. defaultdictnão funciona nesse caso, porque cria apenas chaves em acesso explícito. Pense que você usa algo HTTP-ish com muitos cabeçalhos - alguns são opcionais, mas você deseja padrões para eles:

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 )
Jochen Ritzel
fonte
1
De fato, este IMHO é o principal caso de uso para substituição por defaultdict. Você pode dar um exemplo do que você quer dizer no primeiro parágrafo?
Eli Bendersky
2
Muhammad Alkarouri: O que você faz primeiro é copiar o ditado e substituir alguns dos itens. Também faço muito isso e acho que esse é o idioma que mais prefere setdefault. A, defaultdictpor outro lado, não funcionaria se nem todos defaultvaluesfossem iguais (ou seja, alguns são 0e outros são []).
Jochen Ritzel
2
@ YHC4k, sim. Por isso eu usei headers = dict(optional_headers). Para o caso em que os valores padrão não são todos iguais. E o resultado final é o mesmo que se você obtiver os cabeçalhos HTTP primeiro e depois definir os padrões para aqueles que não obteve. E é bastante utilizável se você já tiver optional_headers. Experimente o meu código de 2 etapas e compare com o seu, e você verá o que quero dizer.
Muhammad Alkarouri 15/08/10
19
ou simplesmente não fazernew.setdefault(key, []).append(value)
fmalina
2
Acho estranho que a melhor resposta defaultdictseja ainda melhor do que setdefault(então, onde está o caso de uso agora?). Além disso, ChainMaplidaria melhor com o httpexemplo, IMO.
YvesgereY
29

Eu normalmente uso setdefaultpara dict de argumento de palavra-chave, como nesta função:

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)

É ótimo para ajustar argumentos em wrappers em torno de funções que recebem argumentos de palavras-chave.

Matt Joiner
fonte
16

defaultdict é ótimo quando o valor padrão é estático, como uma nova lista, mas não tanto se for dinâmico.

Por exemplo, preciso de um dicionário para mapear seqüências de caracteres para ints exclusivos. defaultdict(int)sempre usará 0 para o valor padrão. Da mesma forma, defaultdict(intGen())sempre produz 1.

Em vez disso, usei um ditado regular:

nextID = intGen()
myDict = {}
for lots of complicated stuff:
    #stuff that generates unpredictable, possibly already seen str
    strID = myDict.setdefault(myStr, nextID())

Observe que isso dict.get(key, nextID())é insuficiente, porque também preciso consultar esses valores posteriormente.

intGen é uma classe minúscula que eu construo que incrementa automaticamente um int e retorna seu valor:

class intGen:
    def __init__(self):
        self.i = 0

    def __call__(self):
        self.i += 1
    return self.i

Se alguém tem uma maneira de fazer isso, defaultdicteu adoraria vê-lo.

David Kanarek
fonte
uma maneira de fazê-lo com (uma subclasse de) defaultdict, consulte esta pergunta: stackoverflow.com/questions/2912231/...
weronika
8
Você pode substituir intGenpor itertools.count().next.
Antimony
7
nextID()O valor de será incrementado toda vez que myDict.setdefault()for chamado, mesmo que o valor retornado não seja usado como a strID. Isso parece um desperdício de alguma forma e ilustra uma das coisas que eu não gosto setdefault()em geral - a saber, que ele sempre avalia seu defaultargumento se é ou não usado.
martineau
Você pode fazê-lo com defaultdict: myDict = defaultdict(lambda: nextID()). Mais tarde, strID = myDict[myStr]no circuito.
Musiphil
3
Para obter o comportamento que você descreve com o defaultdict, por que não apenas myDict = defaultdict(nextID)?
Quarenta_two 15/05
10

Eu uso setdefault()quando quero um valor padrão em um OrderedDict. Não é uma coleção Python padrão que faz as duas coisas, mas não são maneiras de implementar tais coleção a.

AndyGeek
fonte
9

Como a maioria das respostas declara setdefaultou defaultdictpermite definir um valor padrão quando uma chave não existe. No entanto, gostaria de salientar uma pequena ressalva em relação aos casos de uso de setdefault. Quando o interpretador Python setdefaulté executado, ele sempre avalia o segundo argumento da função, mesmo que a chave exista no dicionário. Por exemplo:

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

Como você pode ver, printtambém foi executado mesmo que 2 já existissem no dicionário. Isso se torna particularmente importante se você planeja usar, setdefaultpor exemplo, uma otimização como memoization. Se você adicionar uma chamada de função recursiva como o segundo argumento setdefault, não obteria nenhum desempenho, pois o Python sempre chamaria a função recursivamente.

Como a memorização foi mencionada, uma alternativa melhor é usar o decorador functools.lru_cache se você considerar aprimorar uma função com memorização. O lru_cache lida melhor com os requisitos de armazenamento em cache para uma função recursiva.

picmate 涅
fonte
8

Como Muhammad disse, há situações em que você apenas deseja definir um valor padrão. Um ótimo exemplo disso é uma estrutura de dados que é preenchida primeiro e depois consultada.

Considere um trie. Ao adicionar uma palavra, se um subnó for necessário, mas não presente, ele deverá ser criado para estender o trie. Ao consultar a presença de uma palavra, um subnó ausente indica que a palavra não está presente e não deve ser criada.

Um padrão não pode fazer isso. Em vez disso, um ditado regular com os métodos get e setdefault deve ser usado.

David Kanarek
fonte
5

Teoricamente falando, setdefaultainda seria útil se algumas vezes você deseja definir um padrão e outras não. Na vida real, não encontrei um caso de uso assim.

No entanto, um caso de uso interessante surge da biblioteca padrão (Python 2.6, _threadinglocal.py):

>>> mydata = local()
>>> mydata.__dict__
{'number': 42}
>>> mydata.__dict__.setdefault('widgets', [])
[]
>>> mydata.widgets
[]

Eu diria que usar __dict__.setdefaulté um caso bastante útil.

Editar : por acaso, este é o único exemplo na biblioteca padrão e está em um comentário. Portanto, pode não ser um caso suficiente para justificar a existência desetdefault . Ainda assim, aqui está uma explicação:

Os objetos armazenam seus atributos no __dict__atributo Por acaso, o __dict__atributo pode ser gravado a qualquer momento após a criação do objeto. Também é um dicionário, não um defaultdict. Não é sensato que objetos no caso geral tenham __dict__como um defaultdictporque isso tornaria cada objeto tendo todos os identificadores legais como atributos. Portanto, não posso prever nenhuma alteração nos objetos Python se livrando __dict__.setdefault, além de excluí-los completamente se não forem úteis.

Muhammad Alkarouri
fonte
1
Você poderia elaborar - o que torna _dict .setdefault particularmente útil?
Eli Bendersky
1
@ Eli: Eu acho que o ponto é que __dict__é pela implementação de um dict, não um defaultdict.
Katriel
1
Tudo bem. Não me importo em setdefaultficar em Python, mas é curioso ver que agora é quase inútil.
Eli Bendersky
@ Eli: eu concordo. Eu não acho que haja razões suficientes para que ela seja introduzida hoje, se não estivesse lá. Mas já estando lá, seria difícil argumentar sobre a remoção, considerando todo o código que já está sendo usado.
Muhammad Alkarouri
1
Arquivo sob programação defensiva. setdefaultexplicita que você está atribuindo a um ditado por meio de uma chave que pode ou não existir e, se não existir, você deseja que ele seja criado com um valor padrão: por exemplo d.setdefault(key,[]).append(value). Em outras partes do programa que você faz alist=d[k], onde k é calculado, e você quer uma exceção lançada se k não está em d (que com um defaultdict pode exigir assert k in dou mesmoif not ( k in d): raise KeyError
nigel222
3

Uma desvantagem de defaultdictover dict( dict.setdefault) é que um defaultdictobjeto cria um novo item TODAS as chaves inexistentes são fornecidas (por exemplo ==, com , print). Além disso, a defaultdictclasse geralmente é muito menos comum que a dictclasse, é mais difícil serializá-la IME.

As funções do PS IMO | métodos que não pretendem alterar um objeto, não devem modificar um objeto.

xged
fonte
Ele não precisa criar um novo objeto todas as vezes. Você pode fazer com a mesma facilidade defaultdict(lambda l=[]: l).
Artyer 01/03/19
6
Nunca faça o que o @Artyer sugere: padrões mutáveis ​​o morderão.
Brandon Humpert
2

Aqui estão alguns exemplos de setdefault para mostrar sua utilidade:

"""
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'])
Stefan Gruenwald
fonte
2

Reescrevi a resposta aceita e facilei para os novatos.

#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 []

Além disso, categorizei os métodos como referência:

dict_methods_11 = {
            'views':['keys', 'values', 'items'],
            'add':['update','setdefault'],
            'remove':['pop', 'popitem','clear'],
            'retrieve':['get',],
            'copy':['copy','fromkeys'],}
Cálculo
fonte
1

Uso setdefault frequentemente quando, obtém isso, definindo um padrão (!!!) em um dicionário; de certa forma, o dicionário os.environ:

# Set the venv dir if it isn't already overridden:
os.environ.setdefault('VENV_DIR', '/my/default/path')

Menos sucintamente, isso se parece com isso:

# Set the venv dir if it isn't already overridden:
if 'VENV_DIR' not in os.environ:
    os.environ['VENV_DIR'] = '/my/default/path')

Vale ressaltar que você também pode usar a variável resultante:

venv_dir = os.environ.setdefault('VENV_DIR', '/my/default/path')

Mas isso é menos necessário do que era antes da existência dos decretos-padrão.

woodm1979
fonte
1

Outro caso de uso que acho que não foi mencionado acima. Às vezes, você mantém um ditado de cache pelos objetos por seu ID, onde a instância principal está no cache e deseja definir o cache quando estiver ausente.

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

Isso é útil quando você sempre deseja manter uma única instância por ID distinto, independentemente de como você obtém um objetivo a cada vez. Por exemplo, quando os atributos do objeto são atualizados na memória e o salvamento no armazenamento é adiado.

Tuttle
fonte
1

Um caso de uso muito importante que encontrei: dict.setdefault() é ótimo para código multithread quando você quer apenas um único objeto canônico (ao contrário de vários objetos que são iguais).

Por exemplo, o (Int)FlagEnum no Python 3.6.0 possui um erro : se vários threads competem por um (Int)Flagmembro composto , pode haver mais de um:

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)

A solução é usar setdefault()como a última etapa de salvar o membro composto calculado - se outro já tiver sido salvo, ele será usado no lugar do novo, garantindo membros Enum exclusivos.

Ethan Furman
fonte
0

[Editar] Muito errado!O setdefault sempre acionava a long_computation, o Python estava ansioso.

Expandindo a resposta de Tuttle. Para mim, o melhor caso de uso é o mecanismo de cache. Ao invés de:

if x not in memo:
   memo[x]=long_computation(x)
return memo[x]

que consome 3 linhas e 2 ou 3 pesquisas, gostaria de escrever :

return memo.setdefault(x, long_computation(x))
YvesgereY
fonte
Bom exemplo. Ainda acho que as três linhas são mais compreensíveis, mas talvez meu cérebro cresça para apreciar a falta de definição.
Bob Stein
5
Aqueles não são equivalentes. No primeiro, long_computation(x)é chamado apenas se x not in memo. Considerando que no segundo, long_computation(x)é sempre chamado. Somente a atribuição é condicional, o código equivalente setdefaultseria semelhante a: v = long_computation(x)/ if x not in memo:/ memo[x] = v.
11116 Dan D.
0

O caso de uso diferente para setdefault()é quando você não deseja substituir o valor de uma chave já configurada. defaultdictsobrescreve, enquanto setdefault()não. Para dicionários aninhados, geralmente é o caso de você desejar definir um padrão apenas se a chave ainda não estiver definida, porque você não deseja remover o subdicionário atual. É quando você usasetdefault() .

Exemplo com defaultdict:

>>> from collection import defaultdict()
>>> foo = defaultdict()
>>> foo['a'] = 4
>>> foo['a'] = 2
>>> print(foo)
defaultdict(None, {'a': 2})

setdefault não substitui:

>>> bar = dict()
>>> bar.setdefault('a', 4)
>>> bar.setdefault('a', 2)
>>> print(bar)
{'a': 4}
Iodnas
fonte