Trova l'indice di un dict all'interno di un elenco, facendo corrispondere il valore del dict


131

Ho un elenco di dadi:

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

Come posso trovare in modo efficiente la posizione dell'indice [0], [1] o [2] facendo corrispondere su name = 'Tom'?

Se questo fosse un elenco unidimensionale, potrei fare list.index () ma non sono sicuro di come procedere cercando i valori dei dadi all'interno dell'elenco.


6
"list" è il costruttore dell'elenco, è meglio scegliere un altro nome per un elenco (anche in un esempio). E quale dovrebbe essere la risposta se non viene trovato alcun elemento? sollevare un'eccezione? ritorno Nessuno?
tokland

7
Se ne avrai bisogno molto, usa una struttura dati più appropriata (forse { 'Jason': {'id': '1234'}, 'Tom': {'id': '1245'}, ...}?)

3
@delnan Perché questa è una ricetta per il disastro! Semmai, dovrebbe essere {'1234': {'name': 'Jason'}, ...}. Non che ciò aiuterebbe questo caso d'uso.
OJFord,

Risposte:


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

Se devi recuperare ripetutamente dal nome, dovresti indicizzarli per nome (usando un dizionario), in questo modo ottenere operazioni sarebbe O (1) volta. Un'idea:

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'}

2
IMHO questo non è così leggibile o Pythonic è la risposta di @Emile. Poiché l'intenzione non è davvero quella di creare un generatore (e l'utilizzo next()per questo mi sembra strano), l'obiettivo è solo quello di ottenere l'indice. Inoltre, questo genera StopIteration, mentre il lst.index()metodo Python genera ValueError.
Ben Hoyt,

@benhoyt: Non mi piace neanche l'eccezione StopIteration, ma mentre puoi cambiare il valore predefinito di next (), l'eccezione che genera è fissa. La pitonicità è in qualche modo soggettiva, quindi non la contesterò, probabilmente un for-loop è più pitonico. D'altra parte, alcune persone aliases next () per first (), e questo suona decisamente meglio: first (indice per (index, d) in ...).
tokland,

first()suona meglio. Puoi sempre provare / tranne StopIteration e aumentare ValueError in modo che il chiamante abbia coerenza. In alternativa, imposta next()il valore predefinito su -1.
Ben Hoyt,

1
@ gdw2: ottengo SyntaxError: Generator expression must be parenthesized if not sole argumentquando lo faccio.
avoliva,

2
@avoliva aggiunge una parentesi attorno come seguenext((index for (index, d) in enumerate(lst) if d["name"] == "Tom"), None)
HussienK

45

Una semplice versione leggibile è

def find(lst, key, value):
    for i, dic in enumerate(lst):
        if dic[key] == value:
            return i
    return -1

8
Questo sembra il più leggibile e Pythonic. Imita anche il comportamento di str.find()bene. Puoi anche chiamarlo index()e aumentare un ValueErrorinvece di restituire -1 se ciò fosse preferibile.
Ben Hoyt,

6
D'accordo - restituendo -1 quando non viene trovata alcuna corrispondenza, otterrai sempre l'ultimo dict nell'elenco, che probabilmente non è quello che desideri. Meglio restituire None e verificare l'esistenza di una partita nel codice chiamante.
shacker,

9

Non sarà efficiente, in quanto è necessario percorrere l'elenco controllando ogni elemento in esso contenuto (O (n)). Se vuoi l'efficienza, puoi usare dict of dicts . Sulla domanda, ecco un modo possibile per trovarlo (anche se, se si desidera attenersi a questa struttura di dati, in realtà è più efficiente utilizzare un generatore come ha scritto Brent Newey nei commenti; vedere anche la risposta di 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

1
Puoi ottenere l'efficienza che desideri utilizzando un generatore. Vedi la risposta di tokland.
Brent Newey,

2
@Brent Newey: il generatore non cambia il fatto che devi attraversare l'intero elenco, facendo la ricerca O (n) come afferma un etere ... A seconda di quanto tempo è lungo l'elenco, la differenza tra l'utilizzo di un generatore rispetto all'uso un ciclo for o qualsiasi altra cosa sia instabile, dove potrebbe non essere la differenza tra l'utilizzo di un dict e l'utilizzo di un elenco
Dirk

@Brent: hai ragione, ma può battere una ricerca O (1) in un dizionario, inoltre se l'elemento cercato è alla fine dell'elenco?
aeter,

1
@Dirk La chiamata successiva () sul generatore si interrompe quando viene trovata una corrispondenza, quindi non deve attraversare l'intero elenco.
Brent Newey,

@aeter Fai un punto giusto. Mi riferivo alla possibilità di fermarmi quando viene trovata una partita.
Brent Newey,

2

Ecco una funzione che trova la posizione dell'indice del dizionario se esiste.

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

2

Sembra più logico utilizzare una combinazione filtro / indice:

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

E se pensi che potrebbero esserci più corrispondenze:

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

2

La risposta offerta da @faham è una bella riga, ma non restituisce l'indice al dizionario che contiene il valore. Invece restituisce il dizionario stesso. Ecco un modo semplice per ottenere: Un elenco di indici uno o più se ce ne sono più di uno o un elenco vuoto se non ce ne sono:

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()]

Produzione:

>>> [1]

Quello che mi piace di questo approccio è che con una semplice modifica puoi ottenere un elenco di entrambi gli indici e i dizionari come tuple. Questo è il problema di cui avevo bisogno per risolvere e trovare queste risposte. Di seguito, ho aggiunto un valore duplicato in un dizionario diverso per mostrare come funziona:

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()]

Produzione:

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

Questa soluzione trova tutti i dizionari contenenti "Tom" in uno dei loro valori.


1

Una fodera !?

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

0

Per un dato iterabile, more_itertools.locategenera posizioni di elementi che soddisfano un predicato.

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_itertoolsè una libreria di terze parti che implementa ricette itertools tra gli altri strumenti utili.


0
def search(itemID,list):
     return[i for i in list if i.itemID==itemID]
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.