Utilizzare i casi per il metodo dict 'setdefault'


192

L'aggiunta di collections.defaultdictin Python 2.5 ha notevolmente ridotto la necessità dictdel setdefaultmetodo. Questa domanda è per la nostra educazione collettiva:

  1. A cosa serve setdefaultancora oggi Python 2.6 / 2.7?
  2. Con quali casi d'uso popolari sono setdefaultstati sostituiti collections.defaultdict?

1
Leggermente correlato anche stackoverflow.com/questions/7423428/…
utente

Risposte:


208

Si potrebbe dire che defaultdictè utile per le impostazioni predefinite prima di riempire il dict ed setdefaultè utile per impostare i valori predefiniti mentre o dopo aver riempito il dict .

Probabilmente il caso d'uso più comune: raggruppamento di elementi (in dati non ordinati, altro uso 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

A volte vuoi assicurarti che esistano chiavi specifiche dopo aver creato un dict. defaultdictnon funziona in questo caso, perché crea le chiavi solo sull'accesso esplicito. Pensi di usare qualcosa di HTTP con molte intestazioni - alcune sono opzionali, ma vuoi impostazioni predefinite:

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
In effetti, questo IMHO è il principale caso d'uso per la sostituzione di defaultdict. Puoi fare un esempio di cosa intendi nel primo paragrafo?
Eli Bendersky,

2
Muhammad Alkarouri: quello che fai prima è copiare il dict e poi sovrascrivere alcuni degli elementi. Lo faccio anche molto e immagino che in realtà sia il linguaggio più preferito setdefault. A defaultdictd'altra parte non funzionerebbe se non tutti fossero defaultvaluesuguali (cioè alcuni lo sono 0e altri lo sono []).
Jochen Ritzel,

2
@ YHC4k, sì. Ecco perché l'ho usato headers = dict(optional_headers). Nel caso in cui i valori predefiniti non siano tutti uguali. E il risultato finale è lo stesso se ottieni prima le intestazioni HTTP, quindi imposta le impostazioni predefinite per quelle che non hai ricevuto. Ed è abbastanza utilizzabile se lo hai già fatto optional_headers. Prova il mio codice in 2 passaggi e confrontalo con il tuo, e vedrai cosa intendo.
Muhammad Alkarouri,

19
o semplicemente fainew.setdefault(key, []).append(value)
fmalina

2
Trovo strano che la risposta migliore si riduca a defaultdictanche meglio di setdefault(quindi dov'è il caso d'uso ora?). Inoltre, ChainMapsarebbe meglio gestire l' httpesempio, IMO.
YvesgereY,

29

Uso comunemente setdefaultper dicts di argomenti con parole chiave, come in questa funzione:

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)

È ottimo per modificare argomenti in wrapper attorno a funzioni che accettano argomenti di parole chiave.


16

defaultdict è ottimo quando il valore predefinito è statico, come un nuovo elenco, ma non tanto se dinamico.

Ad esempio, ho bisogno di un dizionario per mappare le stringhe su interi univoci. defaultdict(int)utilizzerà sempre 0 per il valore predefinito. Allo stesso modo,defaultdict(intGen()) produce sempre 1.

Invece, ho usato un dict regolare:

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

Nota che dict.get(key, nextID())non è sufficiente perché devo poter fare riferimento anche a questi valori in un secondo momento.

intGen è una piccola classe che costruisco che incrementa automaticamente un int e restituisce il suo valore:

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

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

Se qualcuno ha un modo di farlo con defaultdictmi piacerebbe vederlo.


per un modo di farlo con (una sottoclasse di) defaultdict, vedere questa domanda: stackoverflow.com/questions/2912231/…
weronika,

8
È possibile sostituire intGencon itertools.count().next.
Antimonio

7
nextID()Il valore verrà incrementato ogni volta che myDict.setdefault()viene chiamato, anche se il valore che restituisce non viene utilizzato come strID. Questo sembra in qualche modo dispendioso e illustra una delle cose che non mi piacciono setdefault()in generale - vale a dire che valuta sempre il suo defaultargomento indipendentemente dal fatto che venga effettivamente utilizzato.
martineau,

È possibile farlo con defaultdict: myDict = defaultdict(lambda: nextID()). Più tardi, strID = myDict[myStr]nel ciclo.
musiphil,

3
Per ottenere il comportamento descritto con defaultdict, perché non solo myDict = defaultdict(nextID)?
quaranta_due

10

Uso setdefault()quando desidero un valore predefinito in un OrderedDict. Non c'è una collezione standard Python che fa entrambe le cose, ma ci sono modi per attuare tale raccolta a.


10

Come la maggior parte delle risposte afferma setdefaulto defaultdictti consente di impostare un valore predefinito quando non esiste una chiave. Tuttavia, vorrei sottolineare un piccolo avvertimento per quanto riguarda i casi d'uso di setdefault. Quando viene eseguito l'interprete Python setdefault, valuterà sempre il secondo argomento della funzione anche se la chiave esiste nel dizionario. Per esempio:

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

Come puoi vedere, è printstato eseguito anche se 2 esistevano già nel dizionario. Ciò diventa particolarmente importante se si prevede di utilizzare setdefaultad esempio un'ottimizzazione memoization. Se si aggiunge una chiamata di funzione ricorsiva come secondo argomento asetdefault , non si otterrebbero prestazioni da essa poiché Python sempre la funzione in modo ricorsivo.

Da quando è stata menzionata la memoization, un'alternativa migliore è usare il decoratore functools.lru_cache se si considera di migliorare una funzione con la memoization. lru_cache gestisce meglio i requisiti di memorizzazione nella cache per una funzione ricorsiva.


8

Come ha detto Muhammad, ci sono situazioni in cui a volte si desidera impostare un valore predefinito. Un ottimo esempio di ciò è una struttura di dati che viene prima popolata, quindi interrogata.

Prendi in considerazione un trie. Quando si aggiunge una parola, se un nodo secondario è necessario ma non presente, deve essere creato per estendere il trie. Quando si richiede la presenza di una parola, un nodo secondario mancante indica che la parola non è presente e non deve essere creata.

Un defaultdict non può farlo. Invece, deve essere usato un dict regolare con i metodi get e setdefault.


5

Teoricamente parlando, setdefaultsarebbe comunque utile se a volte desidera impostare un valore predefinito e talvolta no. Nella vita reale, non ho mai visto un caso del genere.

Tuttavia, un interessante caso d'uso emerge dalla libreria standard (Python 2.6, _threadinglocal.py):

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

Direi che l'utilizzo __dict__.setdefaultè un caso piuttosto utile.

Modifica : come succede, questo è l'unico esempio nella libreria standard ed è in un commento. Quindi potrebbe non essere sufficiente un caso per giustificare l'esistenza disetdefault . Tuttavia, ecco una spiegazione:

Gli oggetti memorizzano i loro attributi __dict__nell'attributo. Come accade, l' __dict__attributo è scrivibile in qualsiasi momento dopo la creazione dell'oggetto. È anche un dizionario, non un defaultdict. Non è sensato che gli oggetti nel caso generale abbiano __dict__come un defaultdictperché ciò renderebbe ogni oggetto con tutti gli identificatori legali come attributi. Quindi non posso prevedere alcuna modifica degli oggetti Python di cui liberarsi __dict__.setdefault, a parte eliminarlo del tutto se non fosse ritenuto utile.


1
Potresti elaborare - cosa rende particolarmente utile _dict .setdefault?
Eli Bendersky,

1
@Eli: penso che il punto sia che l' __dict__implementazione a dict, non a defaultdict.
Katriel,

1
Tutto a posto. Non mi importa di setdefaultstare in Python, ma è curioso vedere che ora è quasi inutile.
Eli Bendersky,

@Eli: sono d'accordo. Non credo che ci siano abbastanza ragioni per essere introdotto oggi se non fosse lì. Ma essendo già lì, sarebbe difficile discutere per rimuoverlo, dato tutto il codice che lo utilizza già.
Muhammad Alkarouri,

1
File in fase di programmazione difensiva. setdefaultrende esplicito che si sta assegnando a un dict tramite una chiave che può o non può esistere e, se non esiste, lo si desidera creare con un valore predefinito: ad esempio d.setdefault(key,[]).append(value). Altrove nel programma fai alist=d[k]dove è calcolato k, e vuoi che venga generata un'eccezione se k in non in d (che con un defaultdict potrebbe richiedere assert k in do addiritturaif not ( k in d): raise KeyError
nigel222

3

Uno svantaggio di defaultdictover dict( dict.setdefault) è che un defaultdictoggetto crea un nuovo elemento EVERYTIME viene fornita una chiave inesistente (ad es. Con ==, print). Inoltre la defaultdictclasse è generalmente molto meno comune della dictclasse, è più difficile serializzarla IME.

Le funzioni di PS IMO | metodi non intesi per mutare un oggetto, non dovrebbero mutare un oggetto.


Non è necessario creare un nuovo oggetto ogni volta. Puoi fare altrettanto facilmente defaultdict(lambda l=[]: l)invece.
Artyer

6
Non fare mai ciò che suggerisce @Artyer: le impostazioni predefinite mutabili ti morderanno.
Brandon Humpert,

2

Ecco alcuni esempi di setdefault per dimostrarne l'utilità:

"""
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

Ho riscritto la risposta accettata e facile per i neofiti.

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

Inoltre, ho classificato i metodi come riferimento:

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

1

Uso frequentemente setdefault quando, ottieni questo, impostando un valore predefinito (!!!) in un dizionario; piuttosto comunemente il dizionario os.environ:

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

Meno succintamente, sembra così:

# 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 la pena notare che è anche possibile utilizzare la variabile risultante:

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

Ma è meno necessario di quanto non fosse prima che esistessero i default.


1

Un altro caso d'uso che non credo sia stato menzionato sopra. A volte mantieni un dict della cache degli oggetti in base al loro ID in cui si trova l'istanza primaria nella cache e vuoi impostare la cache quando manca.

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

È utile quando si desidera sempre mantenere un'unica istanza per ID distinto, indipendentemente da come si ottiene un oggetto ogni volta. Ad esempio, quando gli attributi degli oggetti vengono aggiornati in memoria e il salvataggio nella memoria viene rinviato.


1

Un caso d'uso molto importante in cui mi sono appena imbattuto: dict.setdefault() è ottimo per il codice multi-thread quando vuoi solo un singolo oggetto canonico (al contrario di più oggetti che sembrano essere uguali).

Ad esempio, l' (Int)FlagEnum in Python 3.6.0 ha un bug : se più thread sono in competizione per un (Int)Flagmembro composito , potrebbe esserci più di uno:

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)

La soluzione è quella di utilizzare setdefault()l'ultimo passaggio del salvataggio del membro composito calcolato: se un altro è già stato salvato, viene utilizzato al posto di quello nuovo, garantendo membri Enum unici.


0

[Modifica] Molto sbagliato!Il setdefault innescherebbe sempre long_computation, essendo Python impaziente.

Espandendo la risposta di Tuttle. Per me il miglior caso d'uso è il meccanismo cache. Invece di:

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

che consuma 3 righe e 2 o 3 ricerche, scriverei felicemente :

return memo.setdefault(x, long_computation(x))

Buon esempio. Penso ancora che le 3 linee siano più comprensibili, ma forse il mio cervello crescerà per apprezzare setdefault.
Bob Stein,

5
Quelli non sono equivalenti. Nel primo, long_computation(x)si chiama solo se x not in memo. Considerando che nel secondo, long_computation(x)è sempre chiamato. Solo l'assegnazione è condizionata, il codice equivalente setdefaultdovrebbe apparire come: v = long_computation(x)/ if x not in memo:/ memo[x] = v.
Dan D.


0

Il diverso caso d'uso per setdefault()è quando non si desidera sovrascrivere il valore di una chiave già impostata. defaultdictsovrascrive, mentre setdefault()non lo fa. Per i dizionari nidificati è più spesso il caso che si desideri impostare un valore predefinito solo se la chiave non è stata ancora impostata, poiché non si desidera rimuovere il dizionario secondario presente. Questo è quando lo usi setdefault().

Esempio con defaultdict:

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

setdefault non sovrascrive:

>>> bar = dict()
>>> bar.setdefault('a', 4)
>>> bar.setdefault('a', 2)
>>> print(bar)
{'a': 4}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.