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

192

La adición de collections.defaultdicten Python 2.5 reduce en gran medida la necesidad de dict's setdefaultmétodo. Esta pregunta es para nuestra educación colectiva:

  1. ¿Para qué sigue setdefaultsiendo útil hoy en Python 2.6 / 2.7?
  2. ¿Con qué casos de uso populares setdefaultfueron reemplazados collections.defaultdict?
Eli Bendersky
fuente
1
Ligeramente relacionado también stackoverflow.com/questions/7423428/…
usuario

Respuestas:

208

Se podría decir que defaultdictes útil para configurar los valores predeterminados antes de completar el dict y setdefaultes útil para establecer valores predeterminados durante o después de completar el dict .

Probablemente el caso de uso más común: elementos de agrupación (en datos no clasificados, de lo contrario, 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 veces desea asegurarse de que existan claves específicas después de crear un dict. defaultdictno funciona en este caso, porque solo crea claves en el acceso explícito. Cree que usa algo HTTP-ish con muchos encabezados; algunos son opcionales, pero desea valores predeterminados para ellos:

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
fuente
1
De hecho, este en mi humilde opinión es el principal caso de uso para el reemplazo por defaultdict. ¿Puedes dar un ejemplo de lo que quieres decir en el primer párrafo?
Eli Bendersky
2
Muhammad Alkarouri: Lo que haces primero es copiar el dict y luego sobrescribir algunos de los elementos. Lo hago mucho también y supongo que ese es el idioma que más prefieren setdefault. A, defaultdictpor otro lado, no funcionaría si no todos defaultvaluesson iguales (es decir, algunos son 0y otros son []).
Jochen Ritzel
2
@ YHC4k, sí. Por eso lo usé headers = dict(optional_headers). Para el caso en que los valores predeterminados no son todos iguales. Y el resultado final es el mismo que si obtiene los encabezados HTTP primero y luego configure los valores predeterminados para aquellos que no obtuvo. Y es bastante útil si ya lo tienes optional_headers. Pruebe mi código de 2 pasos y compárelo con el suyo, y verá lo que quiero decir.
Muhammad Alkarouri
19
o simplemente hazlonew.setdefault(key, []).append(value)
fmalina
2
Me parece extraño que la mejor respuesta se reduzca a defaultdictincluso mejor que setdefault(¿dónde está el caso de uso ahora?). Además, ChainMapmanejaría mejor el httpejemplo, IMO.
YvesgereY
29

Comúnmente uso setdefaultpara dictados de argumentos de palabras clave, como en esta función:

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)

Es ideal para ajustar argumentos en contenedores alrededor de funciones que toman argumentos de palabras clave.

Matt Joiner
fuente
16

defaultdict es excelente cuando el valor predeterminado es estático, como una nueva lista, pero no tanto si es dinámico.

Por ejemplo, necesito un diccionario para asignar cadenas a entradas únicas. defaultdict(int)siempre usará 0 para el valor predeterminado. Asimismo, defaultdict(intGen())siempre produce 1.

En cambio, usé un dict regular:

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

Tenga en cuenta que dict.get(key, nextID())es insuficiente porque también necesito poder hacer referencia a estos valores más adelante.

intGen es una pequeña clase que construyo que incrementa automáticamente un int y devuelve su valor:

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

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

Si alguien tiene una manera de hacer esto defaultdict, me encantaría verlo.

David Kanarek
fuente
para una forma de hacerlo con (una subclase de) defaultdict, vea esta pregunta: stackoverflow.com/questions/2912231/…
weronika
8
Podrías reemplazar intGencon itertools.count().next.
Antimonio
77
nextID()El valor de 'se incrementará cada vez que myDict.setdefault()se llame, incluso si el valor que devuelve no se usa como a strID. Esto parece un desperdicio de alguna manera e ilustra una de las cosas que no me gustan setdefault()en general, a saber, que siempre evalúa su defaultargumento si realmente se usa o no.
Martineau
Puede hacerlo con defaultdict: myDict = defaultdict(lambda: nextID()). Más tarde, strID = myDict[myStr]en el bucle.
musiphil
3
Para obtener el comportamiento que describe con defaultdict, ¿por qué no solo myDict = defaultdict(nextID)?
cuarenta
10

Lo uso setdefault()cuando quiero un valor predeterminado en un OrderedDict. No hay una colección estándar de Python que hace las dos cosas, pero no son maneras de implementar una colección tan.

AndyGeek
fuente
9

Como la mayoría de las respuestas indican setdefaulto defaultdictle permitirían establecer un valor predeterminado cuando no existe una clave. Sin embargo, me gustaría señalar una pequeña advertencia con respecto a los casos de uso de setdefault. Cuando se ejecuta el intérprete de Python setdefault, siempre evaluará el segundo argumento de la función, incluso si la clave existe en el diccionario. Por ejemplo:

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 puede ver, printtambién se ejecutó aunque 2 ya existían en el diccionario. Esto se vuelve particularmente importante si planea utilizar, setdefaultpor ejemplo, una optimización como memoization. Si agrega una llamada de función recursiva como segundo argumento setdefault, no obtendría ningún rendimiento, ya que Python siempre llamaría a la función de forma recursiva.

Como se mencionó la memorización, una mejor alternativa es utilizar el decorador functools.lru_cache si considera mejorar una función con la memorización. lru_cache maneja mejor los requisitos de almacenamiento en caché para una función recursiva.

picmate 涅
fuente
8

Como dijo Muhammad, hay situaciones en las que a veces solo desea establecer un valor predeterminado. Un gran ejemplo de esto es una estructura de datos que primero se completa, luego se consulta.

Considera un trie. Al agregar una palabra, si se necesita un subnodo pero no está presente, debe crearse para extender el trie. Al consultar la presencia de una palabra, un subnodo faltante indica que la palabra no está presente y que no debe crearse.

Un defaultdict no puede hacer esto. En su lugar, se debe utilizar un dict regular con los métodos get y setdefault.

David Kanarek
fuente
5

Teóricamente hablando, setdefaultaún sería útil si a veces quieres establecer un valor predeterminado y otras no. En la vida real, no me he encontrado con ese caso de uso.

Sin embargo, un caso de uso interesante surge de la biblioteca estándar (Python 2.6, _threadinglocal.py):

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

Yo diría que usar __dict__.setdefaultes un caso bastante útil.

Editar : como sucede, este es el único ejemplo en la biblioteca estándar y está en un comentario. Por lo tanto, puede que no sea un caso suficiente para justificar la existencia de setdefault. Aún así, aquí hay una explicación:

Los objetos almacenan sus atributos en el __dict__atributo. De hecho, el __dict__atributo se puede escribir en cualquier momento después de la creación del objeto. También es un diccionario, no un defaultdict. No es sensato que los objetos en el caso general tengan __dict__como un defaultdictporque eso haría que cada objeto tenga todos los identificadores legales como atributos. Por lo tanto, no puedo prever ningún cambio en la eliminación de los objetos de Python __dict__.setdefault, aparte de eliminarlo por completo si no se considera útil.

Muhammad Alkarouri
fuente
1
¿Podría explicarnos qué hace _dict .setdefault particularmente útil?
Eli Bendersky
1
@Eli: Creo que el punto es que __dict__por implementación a dict, no a defaultdict.
Katriel
1
Bien. No me importa setdefaultquedarme en Python, pero es curioso ver que ahora es casi inútil.
Eli Bendersky
@Eli: estoy de acuerdo. No creo que haya suficientes razones para que se presente hoy si no estuviera allí. Pero estando allí ya, sería difícil discutir para eliminarlo, dado todo el código que ya lo usa.
Muhammad Alkarouri
1
Archivo bajo programación defensiva. setdefaulthace explícito que está asignando a un dict a través de una clave que puede existir o no, y si no existe desea que se cree con un valor predeterminado: por ejemplo d.setdefault(key,[]).append(value). En otra parte del programa que hace alist=d[k]donde se calcula k, y desea que se arroje una excepción si k no está en d (que con un assert k in dif not ( k in d): raise KeyError
fallo predeterminado
3

Un inconveniente de defaultdictover dict( dict.setdefault) es que un defaultdictobjeto crea un nuevo elemento CADA VEZ que se da una clave no existente (por ejemplo ==, con , print). Además, la defaultdictclase es generalmente mucho menos común que la dictclase, es más difícil serializarla IME.

Las funciones de PS IMO | métodos no destinados a mutar un objeto, no deben mutar un objeto.

xged
fuente
No tiene que crear un nuevo objeto cada vez. En su lugar, puede hacerlo con la misma facilidad defaultdict(lambda l=[]: l).
Artyer
66
Nunca haga lo que sugiere @Artyer: los valores predeterminados mutables lo morderán.
Brandon Humpert
2

Estos son algunos ejemplos de setdefault para mostrar su utilidad:

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

Reescribí la respuesta aceptada y la facilité para los 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 []

Además, clasifiqué los métodos como referencia:

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

Uso setdefault con frecuencia cuando, obtengo esto, establezco un valor predeterminado (!!!) en un diccionario; de alguna manera comúnmente el diccionario os.environ:

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

Menos sucintamente, esto se ve así:

# 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 señalar que también puede usar la variable resultante:

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

Pero eso es menos necesario de lo que era antes de que existieran los dictados por defecto.

woodm1979
fuente
1

Otro caso de uso que no creo que se mencionó anteriormente. A veces, mantiene una memoria caché de objetos por su identificación donde la instancia principal está en la memoria caché y desea establecer la memoria caché cuando falta.

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

Eso es útil cuando siempre desea mantener una única instancia por ID distinta, sin importar cómo obtenga un obj cada vez. Por ejemplo, cuando los atributos del objeto se actualizan en la memoria y se difiere el almacenamiento en el almacenamiento.

Tuttle
fuente
1

Un caso de uso muy importante con el que me topé: dict.setdefault()es ideal para código multiproceso cuando solo desea un único objeto canónico (a diferencia de varios objetos que son iguales).

Por ejemplo, el (Int)FlagEnum en Python 3.6.0 tiene un error : si varios hilos compiten por un (Int)Flagmiembro compuesto , puede haber más de 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 solución es usarlo setdefault()como el último paso para guardar el miembro compuesto calculado: si ya se ha guardado otro, se usará en lugar del nuevo, garantizando miembros únicos de Enum.

Ethan Furman
fuente
0

[Editar] Muy mal! Setdefault siempre activaría long_computation, Python ansioso.

Ampliando la respuesta de Tuttle. Para mí, el mejor caso de uso es el mecanismo de caché. En vez de:

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

que consume 3 líneas y 2 o 3 búsquedas, felizmente escribiría :

return memo.setdefault(x, long_computation(x))
YvesgereY
fuente
Buen ejemplo. Todavía creo que las 3 líneas son más comprensibles, pero tal vez mi cerebro crecerá para apreciar el valor predeterminado.
Bob Stein
55
Esos no son equivalentes. En el primero, long_computation(x)solo se llama si x not in memo. Mientras que en el segundo, long_computation(x)siempre se llama. Solo la asignación es condicional, el código equivalente setdefaultsería: v = long_computation(x)/ if x not in memo:/ memo[x] = v.
Dan D.
0

El caso de uso diferente setdefault()es cuando no desea sobrescribir el valor de una clave ya establecida. defaultdictsobrescribe, mientras setdefault()que no. En el caso de los diccionarios anidados, es más frecuente que desee establecer un valor predeterminado solo si la clave aún no está configurada, porque no desea eliminar el actual sub diccionario. Esto es cuando lo usas setdefault().

Ejemplo con defaultdict:

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

setdefault no sobrescribe:

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