Les deux expressions suivantes me semblent équivalentes. Lequel est préférable?
data = [('a', 1), ('b', 1), ('b', 2)]
d1 = {}
d2 = {}
for key, val in data:
# variant 1)
d1[key] = d1.get(key, []) + [val]
# variant 2)
d2.setdefault(key, []).append(val)
Les résultats sont les mêmes mais quelle version est meilleure ou plutôt plus pythonique?
Personnellement, je trouve la version 2 plus difficile à comprendre, car setdefault est très difficile à comprendre. Si je comprends bien, il cherche la valeur de "clé" dans le dictionnaire, s'il n'est pas disponible, entre "[]" dans le dictionnaire, renvoie une référence à la valeur ou "[]" et ajoute "val" à celui-ci. référence. Bien que certainement lisse, ce n’est pas du tout intuitif (du moins pour moi).
Pour moi, la version 1 est plus facile à comprendre (si disponible, obtenez la valeur pour "clé", sinon, "[]", puis rejoignez une liste constituée de [val] et placez le résultat dans "clé" ). Mais bien que plus intuitive à comprendre, je crains que cette version ne soit moins performante, avec toute cette liste créée. Un autre inconvénient est que "d1" apparaît deux fois dans l'expression, ce qui est plutôt sujet aux erreurs. Il y a probablement une meilleure implémentation en utilisant get, mais cela m’échappe pour l’instant.
À mon avis, la version 2, bien que plus difficile à comprendre pour les inexpérimentés, est plus rapide et donc préférable. Des avis?
Vos deux exemples font la même chose, mais cela ne signifie pas que get
et setdefault
do.
La différence entre les deux réside essentiellement dans le réglage manuel de d[key]
pour qu'il pointe vers la liste à chaque fois, par rapport à setdefault
pour le réglage automatique de d[key]
dans la liste uniquement lorsqu'il n'est pas défini.
En rendant les deux méthodes aussi similaires que possible, j'ai couru
from timeit import timeit
print timeit("c = d.get(0, []); c.extend([1]); d[0] = c", "d = {1: []}", number = 1000000)
print timeit("c = d.get(1, []); c.extend([1]); d[0] = c", "d = {1: []}", number = 1000000)
print timeit("d.setdefault(0, []).extend([1])", "d = {1: []}", number = 1000000)
print timeit("d.setdefault(1, []).extend([1])", "d = {1: []}", number = 1000000)
et j'ai
0.794723378711
0.811882272256
0.724429205999
0.722129751973
Donc setdefault
est environ 10% plus rapide que get
à cette fin.
La méthode get
vous permet de faire moins que vous ne pouvez le faire avec setdefault
. Vous pouvez l'utiliser pour éviter d'obtenir une KeyError
lorsque la clé n'existe pas (si cela se produit fréquemment) même si vous ne souhaitez pas définir la clé.
Voir Cas d'utilisation de la méthode dict 'setdefault' et La méthode dict.get () renvoie un pointeur pour plus d'informations sur les deux méthodes.
Le fil de discussion sur setdefault
conclut que la plupart du temps, vous souhaitez utiliser un defaultdict
. Le fil de discussion sur get
conclut qu'il est lent et que souvent, vous faites mieux (en termes de vitesse) de faire une double recherche, en utilisant un defaultdict, ou de gérer l'erreur (selon la taille du dictionnaire et votre cas d'utilisation).
La réponse acceptée par agf ne compare pas pareil. Après:
print timeit("d[0] = d.get(0, []) + [1]", "d = {1: []}", number = 10000)
d[0]
contient une liste de 10 000 éléments alors que:
print timeit("d.setdefault(0, []) + [1]", "d = {1: []}", number = 10000)
d[0]
est tout simplement []
. c'est-à-dire que la version d.setdefault
ne modifie jamais la liste stockée dans d
. Le code devrait en réalité être:
print timeit("d.setdefault(0, []).append(1)", "d = {1: []}", number = 10000)
et est en fait plus rapide que l'exemple setdefault
défectueux.
La différence ici réside vraiment dans le fait que lorsque vous ajoutez en utilisant la concaténation, toute la liste est copiée à chaque fois (et une fois que vous avez 10 000 éléments qui commencent à devenir mesurables. En utilisant append
, les mises à jour de la liste sont amorties.
Enfin, il y a deux autres options non prises en compte dans la question initiale: defaultdict
ou simplement tester le dictionnaire pour voir s'il contient déjà la clé.
Donc, en supposant que d3, d4 = defaultdict(list), {}
# variant 1 (0.39)
d1[key] = d1.get(key, []) + [val]
# variant 2 (0.003)
d2.setdefault(key, []).append(val)
# variant 3 (0.0017)
d3[key].append(val)
# variant 4 (0.002)
if key in d4:
d4[key].append(val)
else:
d4[key] = [val]
la variante 1 est de loin la plus lente car elle copie la liste à chaque fois, la variante 2 est la deuxième plus lente, la variante 3 est la plus rapide mais ne fonctionnera pas si vous avez besoin de Python plus vieux que 2.5, et la variante 4 est légèrement plus lente que la variante 3 .
Je dirais que vous pouvez utiliser la variante 3 si vous le pouvez, avec la variante 4 en option pour les endroits occasionnels où defaultdict
n'est pas un ajustement exact. Évitez les deux de vos variantes d'origine.
Vous voudrez peut-être consulter defaultdict
dans le module collections
. Ce qui suit est équivalent à vos exemples.
from collections import defaultdict
data = [('a', 1), ('b', 1), ('b', 2)]
d = defaultdict(list)
for k, v in data:
d[k].append(v)
Il y a plus ici .
Pour ceux qui ont encore du mal à comprendre ces deux termes, laissez-moi vous dire la différence fondamentale entre les méthodes get () et setdefault () -
Scénario 1
root = {}
root.setdefault('A', [])
print(root)
Scénario 2
root = {}
root.get('A', [])
print(root)
Dans le scénario 1, la sortie sera {'A': []}
tandis que dans le scénario 2, {}
Donc, setdefault()
définit les clés absentes dans le dict tandis que get()
ne vous fournit que la valeur par défaut, mais ne modifie pas le dictionnaire.
Maintenant, venons là où cela sera utile -- Supposons que vous cherchiez un élément dans un dict dont la valeur est une liste et que vous souhaitiez modifier cette liste s’il est trouvé, sinon créez une nouvelle clé avec cette liste.
en utilisant setdefault()
def fn1(dic, key, lst):
dic.setdefault(key, []).extend(lst)
en utilisant get()
def fn2(dic, key, lst):
dic[key] = dic.get(key, []) + (lst) #Explicit assigning happening here
Maintenant examinons les timings -
dic = {}
%%timeit -n 10000 -r 4
fn1(dic, 'A', [1,2,3])
A pris 288 ns
dic = {}
%%timeit -n 10000 -r 4
fn2(dic, 'A', [1,2,3])
A pris 128 s
Il existe donc une très grande différence de calendrier entre ces deux approches.
In [1]: person_dict = {}
In [2]: person_dict['liqi'] = 'LiQi'
In [3]: person_dict.setdefault('liqi', 'Liqi')
Out[3]: 'LiQi'
In [4]: person_dict.setdefault('Kim', 'kim')
Out[4]: 'kim'
In [5]: person_dict
Out[5]: {'Kim': 'kim', 'liqi': 'LiQi'}
In [8]: person_dict.get('Dim', '')
Out[8]: ''
In [5]: person_dict
Out[5]: {'Kim': 'kim', 'liqi': 'LiQi'}
La logique de dict.get
est la suivante:
if key in a_dict:
value = a_dict[key]
else:
value = default_value
Prenons un exemple:
In [72]: a_dict = {'mapping':['dict', 'OrderedDict'], 'array':['list', 'Tuple']}
In [73]: a_dict.get('string', ['str', 'bytes'])
Out[73]: ['str', 'bytes']
In [74]: a_dict.get('array', ['str', 'byets'])
Out[74]: ['list', 'Tuple']
Le méchamisme de setdefault
est:
levels = ['master', 'manager', 'salesman', 'accountant', 'assistant']
#group them by the leading letter
group_by_leading_letter = {}
# the logic expressed by obvious if condition
for level in levels:
leading_letter = level[0]
if leading_letter not in group_by_leading_letter:
group_by_leading_letter[leading_letter] = [level]
else:
group_by_leading_letter[leading_letter].append(Word)
In [80]: group_by_leading_letter
Out[80]: {'a': ['accountant', 'assistant'], 'm': ['master', 'manager'], 's': ['salesman']}
La méthode setdefault dict répond précisément à cet objectif. La précédente boucle for peut être réécrite comme suit:
In [87]: for level in levels:
...: leading = level[0]
...: group_by_leading_letter.setdefault(leading,[]).append(level)
Out[80]: {'a': ['accountant', 'assistant'], 'm': ['master', 'manager'], 's': ['salesman']}
C’est très simple, cela signifie qu’une liste non nulle ajoute un élément ou une liste nulle ajoute un élément.
Le defaultdict
, ce qui rend encore plus facile. Pour en créer un, vous transmettez un type ou une fonction permettant de générer la valeur par défaut pour chaque emplacement du dict:
from collections import defualtdict
group_by_leading_letter = defaultdict(list)
for level in levels:
group_by_leading_letter[level[0]].append(level)