web-dev-qa-db-fra.com

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

L'ajout de collections.defaultdict dans Python 2.5 a considérablement réduit la nécessité de la méthode dict's setdefault. Cette question est pour notre éducation collective:

  1. Pourquoi setdefault est-il encore utile aujourd'hui dans Python 2.6/2.7?
  2. Quels cas d'utilisation populaires de setdefault ont été remplacés par collections.defaultdict?
166
Eli Bendersky

Vous pouvez dire que defaultdict est utile pour les paramètres par défaut avant de remplir le dict et setdefault est utile pour définir les paramètres par défaut pendant ou après le remplissage du dict

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

Parfois, vous voulez vous assurer que des clés spécifiques existent après la création d'un dict. defaultdict ne fonctionne pas dans ce cas, car il crée uniquement des clés sur un accès explicite. Pensez que vous utilisez quelque chose HTTP-ish avec plusieurs en-têtes - certains sont optionnels, 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 )
177
Jochen Ritzel

J'utilise couramment setdefault pour les mots-clés avec arguments de mots clés, 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 parfait pour ajuster les arguments dans les wrappers autour de fonctions qui prennent des arguments de mots clés.

28
Matt Joiner

defaultdict est idéal lorsque la valeur par défaut est statique, comme une nouvelle liste, mais pas autant si elle est dynamique.

Par exemple, j'ai besoin d'un dictionnaire pour mapper des chaînes à des entiers uniques. defaultdict(int) utilisera toujours 0 pour la 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()) est insuffisant car je dois également pouvoir me référer ultérieurement à ces valeurs.

intGen est une classe minuscule 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 defaultdict, j'aimerais le voir.

15
David Kanarek

J'utilise setdefault() lorsque je veux une valeur par défaut dans un OrderedDict. Il n'y a pas de collection Python standard qui fait les deux, mais il y a arefaçons d'implémenter une telle collection.

9
AndyGeek

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

Considérez un trie. Lors de l'ajout d'un mot, si un sous-noeud est nécessaire mais non présent, il doit être créé pour étendre le test. Lors de la recherche de la présence d'un mot, un sous-noeud 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. À la place, vous devez utiliser un dict régulier avec les méthodes get et setdefault.

7
David Kanarek

Théoriquement, setdefault serait toujours utile si vous parfois voulez définir une valeur par défaut et parfois pas. Dans la vraie vie, je n'ai pas rencontré un tel cas d'utilisation.

Cependant, un cas d’utilisation intéressant ressort 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 de __dict__.setdefault est un cas très utile.

Edit : En l'occurrence, il s'agit du seul exemple dans la bibliothèque standard et il se trouve dans un commentaire. Alors peut-être qu’il n’ya pas assez de cause pour justifier l’existence de setdefault. Pourtant, voici une explication:

Les objets stockent leurs attributs dans l'attribut __dict__. En l'occurrence, l'attribut __dict__ est accessible en écriture à tout moment après la création de l'objet. C'est aussi un dictionnaire et non une defaultdict. Il n'est pas judicieux que les objets dans le cas général aient __dict__ en tant que defaultdict car cela donnerait à chaque objet tous les identificateurs légaux comme attributs. Je ne peux donc prévoir aucun changement dans les objets Python supprimant __dict__.setdefault, mis à part le supprimer complètement s'il est jugé inutile.

5
Muhammad Alkarouri

Un inconvénient de defaultdict sur dict (dict.setdefault) est qu’un objet defaultdict crée un nouvel élémentCHAQUE FOIS QUEune clé non existante est donnée (par exemple avec ==, print). De plus, la classe defaultdict est généralement beaucoup moins commune que la classe dict, il est donc plus difficile de la sérialiser en IME.

P.S. Les méthodes des fonctions IMO qui ne sont pas censées muter un objet ne doivent pas muter un objet.

2
xged

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

Comme la plupart des réponses indiquent setdefault ou defaultdict, cela vous permettrait 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, setdefaultit é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 constater, print a également été exécuté même si 2 existait déjà dans le dictionnaire. Cela devient particulièrement important si vous envisagez d'utiliser setdefault par exemple pour une optimisation telle que memoization. Si vous ajoutez un appel de fonction récursif au deuxième argument de setdefault, vous n'obtiendrez aucune performance, car Python appellerait toujours la fonction de manière récursive.

1
picmate 涅

Un autre cas d'utilisation dont je ne pense pas qu'il a été mentionné ci-dessus . Parfois, vous gardez une dictée de cache d'objets par leur identifiant, où l'instance principale est dans le cache et vous voulez définir le cache lorsqu'il est manquant.

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

C'est utile lorsque vous souhaitez toujours conserver une seule instance par identifiant distinct, peu importe la façon dont vous obtenez un obj à chaque fois. Par exemple, lorsque les attributs d'objet sont mis à jour en mémoire et que l'enregistrement dans la mémoire est différé.

1
Tuttle

J'utilise souvent setdefault lorsque, obtenez ceci, en définissant une valeur par défaut (!!!) dans un dictionnaire; assez 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 de defaultdicts.

1
woodm1979

Un cas d'utilisation très important que je viens de trébucher: dict.setdefault() est idéal pour le code multithread lorsque vous ne voulez qu'un seul objet canonique (par opposition à plusieurs objets égaux).

Par exemple, le (Int)Flag Enum dans Python 3.6.0 a un bogue : si plusieurs threads sont en compétition pour un membre (Int)Flag composite, il peut en arriver 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 la sauvegarde du membre composite calculé. Si un autre membre a déjà été enregistré, il est utilisé à la place du nouveau membre, ce qui garantit des membres Enum uniques.

1
Ethan Furman

Le cas d'utilisation différent de setdefault() est lorsque vous ne souhaitez pas écraser la valeur d'une clé déjà définie. defaultdict remplace, 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 ne pas écraser:

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

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

Développer 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))
0
YvesgereY

J'ai récrit la réponse acceptée et l'ai simplifiée 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'],}
0
JawSaw

J'aime la réponse donnée ici:

http://stupidpythonideas.blogspot.com/2013/08/defaultdict-vs-setdefault.html

En bref, la décision (dans les applications non critiques en termes de performances) doit être prise en fonction de la manière dont vous souhaitez gérer la recherche de clés vides en aval (viz.KeyError par rapport à la valeur par défaut).

0
Fred