Trouver l'index d'un dict dans une liste, en faisant correspondre la valeur du dict

131

J'ai une liste de dictés:

list = [{'id':'1234','name':'Jason'},
        {'id':'2345','name':'Tom'},
        {'id':'3456','name':'Art'}]

Comment puis-je trouver efficacement la position d'index [0], [1] ou [2] en faisant correspondre le nom = 'Tom'?

S'il s'agissait d'une liste unidimensionnelle, je pourrais faire list.index () mais je ne sais pas comment procéder en recherchant les valeurs des dictats dans la liste.

piéger
la source
6
"list" est le constructeur de liste, il vaut mieux choisir un autre nom pour une liste (même dans un exemple). Et quelle devrait être la réponse si aucun élément n'est trouvé? soulever une exception? retourner Aucun?
tokland
7
Si vous en avez beaucoup besoin, utilisez une structure de données plus appropriée (peut { 'Jason': {'id': '1234'}, 'Tom': {'id': '1245'}, ...}- être ?)
3
@delnan Parce que c'est la recette du désastre! Si quoi que ce soit, ça devrait l'être {'1234': {'name': 'Jason'}, ...}. Non pas que cela aiderait ce cas d'utilisation.
OJFord

Réponses:

145
tom_index = next((index for (index, d) in enumerate(lst) if d["name"] == "Tom"), None)
# 1

Si vous avez besoin de récupérer à plusieurs reprises le nom, vous devez les indexer par nom (en utilisant un dictionnaire), de cette façon, les opérations d' obtention seraient le temps O (1). Une idée:

def build_dict(seq, key):
    return dict((d[key], dict(d, index=index)) for (index, d) in enumerate(seq))

info_by_name = build_dict(lst, key="name")
tom_info = info_by_name.get("Tom")
# {'index': 1, 'id': '2345', 'name': 'Tom'}
tokland
la source
2
IMHO ce n'est pas aussi lisible ou Pythonic est la réponse de @ Emile. Parce que l'intention n'est pas vraiment de créer un générateur (et utiliser next()pour cela me semble bizarre), le but est juste d'obtenir l'index. En outre, cela déclenche StopIteration, tandis que la lst.index()méthode Python déclenche ValueError.
Ben Hoyt
@benhoyt: Je n'aime pas non plus l'exception StopIteration, mais bien que vous puissiez changer la valeur par défaut de next (), l'exception qu'elle déclenche est corrigée. La pythonicité est quelque peu subjective donc je ne la contesterai pas, probablement une boucle for est plus pythonique. D'un autre côté, certaines personnes alias next () pour first (), et cela sonne certainement mieux: first (index for (index, d) in ...).
tokland
first()ça sonne mieux. Vous pouvez toujours essayer / sauf StopIteration et lever ValueError pour que l'appelant soit cohérent. Vous pouvez également définir next()la valeur par défaut de -1.
Ben Hoyt
1
@ gdw2: je reçois SyntaxError: Generator expression must be parenthesized if not sole argumenten faisant ça.
avoliva le
2
@avoliva ajouter une parenthèse autour du suivant comme suitnext((index for (index, d) in enumerate(lst) if d["name"] == "Tom"), None)
HussienK
45

Une version lisible simple est

def find(lst, key, value):
    for i, dic in enumerate(lst):
        if dic[key] == value:
            return i
    return -1
Emile
la source
8
Cela semble le plus lisible et le plus pythonique. Il imite également le comportement de str.find()bien. Vous pouvez également l'appeler index()et augmenter a ValueErrorau lieu de renvoyer -1 si cela était préférable.
Ben Hoyt
6
D'accord - en renvoyant -1 lorsqu'aucune correspondance n'est trouvée, vous obtiendrez toujours le dernier dict de la liste, ce qui n'est probablement pas ce que vous voulez. Mieux vaut retourner None et vérifier l'existence d'une correspondance dans le code appelant.
shacker
9

Ce ne sera pas efficace, car vous devez parcourir la liste en vérifiant tous les éléments qu'elle contient (O (n)). Si vous voulez de l'efficacité, vous pouvez utiliser des dictées . Sur la question, voici une façon possible de la trouver (cependant, si vous voulez vous en tenir à cette structure de données, il est en fait plus efficace d' utiliser un générateur comme Brent Newey l'a écrit dans les commentaires; voir aussi la réponse de tokland):

>>> L = [{'id':'1234','name':'Jason'},
...         {'id':'2345','name':'Tom'},
...         {'id':'3456','name':'Art'}]
>>> [i for i,_ in enumerate(L) if _['name'] == 'Tom'][0]
1
Aeter
la source
1
Vous pouvez gagner l'efficacité que vous désirez en utilisant un générateur. Voir la réponse de tokland.
Brent Newey
2
@Brent Newey: Le générateur ne change pas le fait que vous devez parcourir la liste entière, faisant la recherche O (n) comme une réclamation d'aeter ... En fonction de la longueur de cette liste, la différence entre l'utilisation d'un générateur et l'utilisation une boucle for ou ce qui pourrait être négligeable, alors que la différence entre l'utilisation d'un dict et l'utilisation d'une liste pourrait ne pas l'être
Dirk
@Brent: Vous avez raison, mais peut-il battre une recherche O (1) dans un dictionnaire, de plus si l'élément recherché est à la fin de la liste?
aeter
1
@Dirk L'appel next () sur le générateur s'arrête lorsqu'une correspondance est trouvée, il n'est donc pas nécessaire de parcourir toute la liste.
Brent Newey
@aeter Vous faites valoir un bon point. Je parlais de pouvoir s'arrêter lorsqu'une correspondance est trouvée.
Brent Newey
2

Voici une fonction qui trouve la position d'index du dictionnaire si elle existe.

dicts = [{'id':'1234','name':'Jason'},
         {'id':'2345','name':'Tom'},
         {'id':'3456','name':'Art'}]

def find_index(dicts, key, value):
    class Null: pass
    for i, d in enumerate(dicts):
        if d.get(key, Null) == value:
            return i
    else:
        raise ValueError('no dict with the key and value combination found')

print find_index(dicts, 'name', 'Tom')
# 1
find_index(dicts, 'name', 'Ensnare')
# ValueError: no dict with the key and value combination found
Martineau
la source
2

Il semble plus logique d'utiliser un combo filtre / index:

names=[{}, {'name': 'Tom'},{'name': 'Tony'}]
names.index(filter(lambda n: n.get('name') == 'Tom', names)[0])
1

Et si vous pensez qu'il pourrait y avoir plusieurs correspondances:

[names.index(n) for item in filter(lambda n: n.get('name') == 'Tom', names)]
[1]
Michael saumon
la source
2

La réponse offerte par @faham est une belle ligne unique, mais elle ne renvoie pas l'index du dictionnaire contenant la valeur. Au lieu de cela, il renvoie le dictionnaire lui-même. Voici un moyen simple d'obtenir: Une liste d'index un ou plusieurs s'il y en a plus d'un, ou une liste vide s'il n'y en a pas:

list = [{'id':'1234','name':'Jason'},
        {'id':'2345','name':'Tom'},
        {'id':'3456','name':'Art'}]

[i for i, d in enumerate(list) if 'Tom' in d.values()]

Production:

>>> [1]

Ce que j'aime dans cette approche, c'est qu'avec une simple modification, vous pouvez obtenir une liste des index et des dictionnaires sous forme de tuples. C'est le problème que j'avais besoin de résoudre et j'ai trouvé ces réponses. Dans ce qui suit, j'ai ajouté une valeur en double dans un dictionnaire différent pour montrer comment cela fonctionne:

list = [{'id':'1234','name':'Jason'},
        {'id':'2345','name':'Tom'},
        {'id':'3456','name':'Art'},
        {'id':'4567','name':'Tom'}]

[(i, d) for i, d in enumerate(list) if 'Tom' in d.values()]

Production:

>>> [(1, {'id': '2345', 'name': 'Tom'}), (3, {'id': '4567', 'name': 'Tom'})]

Cette solution trouve tous les dictionnaires contenant «Tom» dans l'une de leurs valeurs.

carrément
la source
1

Bon mot!?

elm = ([i for i in mylist if i['name'] == 'Tom'] or [None])[0]
faham
la source
0

Pour un itérable donné, more_itertools.locatedonne les positions des éléments qui satisfont un prédicat.

import more_itertools as mit


iterable = [
    {"id": "1234", "name": "Jason"},
    {"id": "2345", "name": "Tom"},
    {"id": "3456", "name": "Art"}
]

list(mit.locate(iterable, pred=lambda d: d["name"] == "Tom"))
# [1]

more_itertoolsest une bibliothèque tierce qui implémente les recettes itertools parmi d'autres outils utiles.

pylang
la source
0
def search(itemID,list):
     return[i for i in list if i.itemID==itemID]
Rohan Kumara
la source