Cas d'utilisation de la méthode dict 'setdefault'

196

L'ajout de collections.defaultdictPython 2.5 a considérablement réduit le besoin de dictla setdefaultméthode de. Cette question s'adresse à notre éducation collective:

  1. À quoi sert setdefaultencore aujourd'hui Python 2.6 / 2.7?
  2. Quels cas d'utilisation populaires de setdefaultont été remplacés collections.defaultdict?
Eli Bendersky
la source

Réponses:

211

Vous pourriez dire que defaultdictc'est utile pour les paramètres par défaut avant de remplir le dict et setdefaultest utile pour définir les valeurs par défaut pendant ou après le remplissage du dict .

Probablement le cas d'utilisation le plus courant: regrouper des éléments (dans des données non triées, sinon utiliser 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

Parfois, vous voulez vous assurer que des clés spécifiques existent après la création d'un dict. defaultdictne fonctionne pas dans ce cas, car il crée uniquement des clés sur un accès explicite. Pensez que vous utilisez quelque chose de HTTP-ish avec de nombreux en-têtes - certains sont facultatifs, mais vous voulez des valeurs par défaut pour eux:

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
la source
1
En effet, cette IMHO est le principal cas d'utilisation pour le remplacement par defaultdict. Pouvez-vous donner un exemple de ce que vous voulez dire dans le premier paragraphe?
Eli Bendersky
2
Muhammad Alkarouri: Ce que vous faites d'abord est de copier le dict puis d'écraser certains éléments. Je fais beaucoup cela aussi et je suppose que c'est en fait l'idiome que la plupart préfèrent setdefault. A defaultdictd'un autre côté ne fonctionnerait pas si tous les defaultvaluessont égaux (c'est-à-dire que certains le sont 0et certains le sont []).
Jochen Ritzel
2
@ YHC4k, oui. C'est pourquoi j'ai utilisé headers = dict(optional_headers). Pour le cas où les valeurs par défaut ne sont pas toutes égales. Et le résultat final est le même que si vous obtenez d'abord les en-têtes HTTP, puis définissez les valeurs par défaut pour ceux que vous n'avez pas obtenus. Et il est tout à fait utilisable si vous l'avez déjà optional_headers. Essayez mon code en 2 étapes et comparez-le au vôtre, et vous verrez ce que je veux dire.
Muhammad Alkarouri
20
ou juste fairenew.setdefault(key, []).append(value)
fmalina
2
Je trouve bizarre que la meilleure réponse defaultdictsoit encore meilleure que setdefault(alors où est le cas d'utilisation maintenant?). En outre, ChainMapserait mieux gérer l' httpexemple, l'OMI.
YvesgereY
29

J'utilise couramment setdefaultpour les dictées d'argument de mot-clé, comme dans cette fonction:

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)

C'est idéal pour peaufiner les arguments dans les wrappers autour des fonctions qui acceptent des arguments de mot-clé.

Matt Joiner
la source
16

defaultdict est génial lorsque la valeur par défaut est statique, comme une nouvelle liste, mais pas tellement si elle est dynamique.

Par exemple, j'ai besoin d'un dictionnaire pour mapper des chaînes sur des entiers uniques. defaultdict(int)utilisera toujours 0 comme valeur par défaut. De même, defaultdict(intGen())produit toujours 1.

Au lieu de cela, j'ai utilisé un dict régulier:

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

Notez que dict.get(key, nextID())c'est insuffisant car je dois également pouvoir me référer à ces valeurs plus tard.

intGen est une petite classe que je construis qui incrémente automatiquement un int et renvoie sa valeur:

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

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

Si quelqu'un a un moyen de faire cela avec, defaultdictj'aimerais le voir.

David Kanarek
la source
pour un moyen de le faire avec (une sous-classe de) defaultdict, voir cette question: stackoverflow.com/questions/2912231/…
weronika
8
Vous pouvez remplacer intGenpar itertools.count().next.
Antimoine
7
nextID()La valeur de s va être incrémentée à chaque myDict.setdefault()appel, même si la valeur qu'elle renvoie n'est pas utilisée comme un strID. Cela semble en quelque sorte un gaspillage et illustre l'une des choses que je n'aime pas setdefault()en général - à savoir qu'il évalue toujours son defaultargument s'il est réellement utilisé ou non.
martineau
Vous pouvez le faire avec defaultdict: myDict = defaultdict(lambda: nextID()). Plus tard, strID = myDict[myStr]dans la boucle.
musiphil
3
Pour obtenir le comportement que vous décrivez avec defaultdict, pourquoi pas simplement myDict = defaultdict(nextID)?
quarante_deux
11

Comme la plupart des réponses indiquent setdefaultou defaultdictvous permet de définir une valeur par défaut lorsqu'une clé n'existe pas. Cependant, je voudrais signaler une petite mise en garde concernant les cas d'utilisation de setdefault. Lorsque l'interpréteur Python s'exécute, setdefaultil évaluera toujours le deuxième argument de la fonction même si la clé existe dans le dictionnaire. Par exemple:

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

Comme vous pouvez le voir, printa également été exécuté même si 2 existait déjà dans le dictionnaire. Cela devient particulièrement important si vous prévoyez d'utiliser setdefaultpar exemple pour une optimisation comme memoization. Si vous ajoutez un appel de fonction récursive comme deuxième argument à setdefault, vous n'obtiendrez aucune performance car Python appellerait toujours la fonction de manière récursive.

Puisque la mémorisation a été mentionnée, une meilleure alternative est d'utiliser le décorateur functools.lru_cache si vous envisagez d'améliorer une fonction avec la mémorisation. lru_cache gère mieux les exigences de mise en cache pour une fonction récursive.

picmate 涅
la source
10

J'utilise setdefault()quand je veux une valeur par défaut dans un fichier OrderedDict. Il n'existe pas de collection Python standard qui fasse les deux, mais il existe des moyens d'implémenter une telle collection.

AndyGeek
la source
8

Comme l'a dit Muhammad, il existe des situations dans lesquelles vous ne souhaitez parfois définir une valeur par défaut. Un bon exemple de ceci est une structure de données qui est d'abord remplie, puis interrogée.

Considérez un trie. Lors de l'ajout d'un mot, si un sous-nœud est nécessaire mais pas présent, il doit être créé pour étendre le trie. Lors de l'interrogation de la présence d'un mot, un sous-nœud manquant indique que le mot n'est pas présent et qu'il ne doit pas être créé.

Un defaultdict ne peut pas faire cela. Au lieu de cela, un dict régulier avec les méthodes get et setdefault doit être utilisé.

David Kanarek
la source
5

Théoriquement parlant, cela setdefaultserait toujours pratique si vous souhaitez parfois définir une valeur par défaut et parfois non. Dans la vraie vie, je n'ai pas rencontré un tel cas d'utilisation.

Cependant, un cas d'utilisation intéressant provient de la bibliothèque standard (Python 2.6, _threadinglocal.py):

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

Je dirais que l'utilisation __dict__.setdefaultest un cas assez utile.

Edit : En l'occurrence, c'est le seul exemple de la bibliothèque standard et c'est dans un commentaire. Il se peut donc que ce ne soit pas suffisant pour justifier l’existence de setdefault. Pourtant, voici une explication:

Les objets stockent leurs attributs dans l' __dict__attribut. En l'occurrence, l' __dict__attribut est accessible en écriture à tout moment après la création de l'objet. C'est aussi un dictionnaire pas un defaultdict. Il n'est pas judicieux pour les objets dans le cas général d'avoir __dict__comme un defaultdictcar cela rendrait chaque objet ayant tous les identifiants légaux comme attributs. Je ne peux donc pas prévoir de changement dans la __dict__.setdefaultsuppression des objets Python , à part de les supprimer complètement si cela n'a pas été jugé utile.

Muhammad Alkarouri
la source
1
Pourriez-vous préciser - qu'est-ce qui rend _dict .setdefault particulièrement utile?
Eli Bendersky
1
@Eli: Je pense que le point est que __dict__par l'implémentation a dict, pas a defaultdict.
Katriel
1
Bien. Cela ne me dérange pas de setdefaultrester en Python, mais c'est curieux de voir que c'est maintenant presque inutile.
Eli Bendersky
@Eli: Je suis d'accord. Je ne pense pas qu'il y ait suffisamment de raisons pour qu'elle soit introduite aujourd'hui si elle n'était pas là. Mais étant déjà là, il serait difficile de plaider pour sa suppression, étant donné que tout le code l'utilise déjà.
Muhammad Alkarouri
1
Dossier sous programmation défensive. setdefaultrend explicite que vous affectez à un dict via une clé qui peut exister ou non, et si elle n'existe pas, vous voulez qu'elle soit créée avec une valeur par défaut: par exemple d.setdefault(key,[]).append(value). Ailleurs dans le programme, vous faites alist=d[k]où k est calculé, et vous voulez une exception levée si k n'est pas dans d (ce qui avec un defaultdict pourrait exiger assert k in dou mêmeif not ( k in d): raise KeyError
nigel222
3

J'ai réécrit la réponse acceptée et la facilité pour les débutants.

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

De plus, j'ai classé les méthodes comme référence:

dict_methods_11 = {
            'views':['keys', 'values', 'items'],
            'add':['update','setdefault'],
            'remove':['pop', 'popitem','clear'],
            'retrieve':['get',],
            'copy':['copy','fromkeys'],}
Calcul
la source
dict_methods_11 = {'views': ['keys', 'values', 'items'], 'add': ['update', 'setdefault'], 'remove': ['pop', 'popitem', 'clear '],' retrieve ': [' get '],' copy ': [' copy ',' fromkeys ']} Il y avait deux virgules supplémentaires que je les ai modifiées.
Ali Hassan
3

Un inconvénient de defaultdictover dict( dict.setdefault) est qu'un defaultdictobjet crée un nouvel élément CHAQUE FOIS, une clé non existante est donnée (par exemple avec ==, print). De plus, la defaultdictclasse est généralement beaucoup moins courante que la dictclasse, il est plus difficile de la sérialiser IME.

Fonctions PS IMO | méthodes non destinées à muter un objet, ne doivent pas muter un objet.

xged
la source
Il n'est pas nécessaire de créer un nouvel objet à chaque fois. Vous pouvez tout aussi facilement faire à la defaultdict(lambda l=[]: l)place.
Artyer
7
Ne faites jamais ce que @Artyer suggère - les valeurs par défaut mutables vous mordront.
Brandon Humpert
2

Voici quelques exemples de setdefault pour montrer son 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'])
Stefan Gruenwald
la source
1

J'utilise fréquemment setdefault quand, obtenez ceci, en définissant une valeur par défaut (!!!) dans un dictionnaire; un peu communément le dictionnaire os.environ:

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

Moins succinctement, cela ressemble à ceci:

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

Il est à noter que vous pouvez également utiliser la variable résultante:

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

Mais c'est moins nécessaire qu'avant l'existence des defaultdicts.

bois1979
la source
1

Un autre cas d'utilisation que je ne pense pas a été mentionné ci-dessus. Parfois, vous gardez un cache des objets par leur identifiant où l'instance principale est dans le cache et vous voulez définir le cache en cas d'absence.

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

C'est utile lorsque vous souhaitez toujours conserver une seule instance par identifiant distinct, quelle que soit la manière dont vous obtenez un objet à chaque fois. Par exemple, lorsque les attributs d'objet sont mis à jour en mémoire et que l'enregistrement dans le stockage est différé.

Tuttle
la source
1

Un cas d'utilisation très important sur dict.setdefault()lequel je viens de tomber: il est idéal pour le code multi-thread lorsque vous ne voulez qu'un seul objet canonique (par opposition à plusieurs objets qui se trouvent être égaux).

Par exemple, (Int)FlagEnum dans Python 3.6.0 a un bogue : si plusieurs threads sont en concurrence pour un (Int)Flagmembre composite , il peut y en avoir plus d'un:

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 solution consiste à utiliser setdefault()comme dernière étape de sauvegarde du membre composite calculé - si un autre a déjà été enregistré, il est utilisé à la place du nouveau, garantissant des membres Enum uniques.

Ethan Furman
la source
0

[Modifier] Très faux! Le setdefault déclencherait toujours long_computation, Python étant impatient.

Développant la réponse de Tuttle. Pour moi, le meilleur cas d'utilisation est le mécanisme de cache. Au lieu de:

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

qui consomme 3 lignes et 2 ou 3 recherches, j'écrirais volontiers :

return memo.setdefault(x, long_computation(x))
YvesgereY
la source
Bon exemple. Je pense toujours que les 3 lignes sont plus compréhensibles, mais peut-être que mon cerveau grandira pour apprécier setdefault.
Bob Stein
5
Ce ne sont pas des équivalents. Dans le premier, long_computation(x)n'est appelé que si x not in memo. Alors que dans le second, long_computation(x)est toujours appelé. Seule l'affectation est conditionnelle, le code équivalent à setdefaultressemblerait à: v = long_computation(x)/ if x not in memo:/ memo[x] = v.
Dan D.
0

Le cas d'utilisation différent setdefault()est celui où vous ne souhaitez pas écraser la valeur d'une clé déjà définie. defaultdictécrase, alors que setdefault()ne le fait pas. Pour les dictionnaires imbriqués, il est plus fréquent que vous souhaitiez définir une valeur par défaut uniquement si la clé n'est pas encore définie, car vous ne souhaitez pas supprimer le sous-dictionnaire actuel. C'est à ce moment que vous utilisez setdefault().

Exemple avec defaultdict:

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

setdefault n'écrase pas:

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