Znajdź indeks dyktu na liście, dopasowując jego wartość

135

Mam listę dyktand:

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

Jak skutecznie znaleźć pozycję indeksu [0], [1] lub [2], dopasowując do name = „Tom”?

Gdyby to była lista jednowymiarowa, mógłbym zrobić list.index (), ale nie jestem pewien, jak postępować, przeszukując wartości dykt na liście.

usidlić
źródło
6
"lista" to konstruktor listy, lepiej wybierz inną nazwę listy (nawet w przykładzie). A jaka powinna być odpowiedź, jeśli nie zostanie znaleziony żaden element? zgłosić wyjątek? powrót Brak?
tokland
7
Jeśli będziesz tego potrzebować bardzo, użyj bardziej odpowiedniej struktury danych (być może { 'Jason': {'id': '1234'}, 'Tom': {'id': '1245'}, ...}?)
3
@delnan Ponieważ to przepis na katastrofę! Jeśli już, to powinno {'1234': {'name': 'Jason'}, ...}. Nie żeby to pomogło w tym przypadku użycia.
OJFord

Odpowiedzi:

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

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

Jeśli chcesz wielokrotnie pobierać dane z nazwy, powinieneś indeksować je po nazwie (używając słownika), w ten sposób operacje get będą miały czas O (1). Pomysł:

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
źródło
2
IMHO, to nie jest tak czytelne lub Pythonic to odpowiedź @ Emile. Ponieważ celem nie jest tak naprawdę stworzenie generatora (a używanie next()do tego wydaje mi się dziwne), celem jest po prostu uzyskanie indeksu. Ponadto podnosi to StopIteration, podczas gdy lst.index()metoda Pythona podnosi ValueError.
Ben Hoyt,
@benhoyt: Ja też nie podoba mi się wyjątek StopIteration, ale chociaż możesz zmienić domyślną wartość next (), wyjątek, który wywołuje, jest naprawiony. Pytoniczność jest nieco subiektywna, więc nie będę jej kwestionować, prawdopodobnie pętla for jest bardziej pytoniczna. Z drugiej strony, niektórzy używają aliasów next () dla first (), co zdecydowanie brzmi lepiej: first (index for (index, d) in ...).
tokland
first()brzmi lepiej. Zawsze możesz spróbować / z wyjątkiem StopIteration i podnieść ValueError, aby obiekt wywołujący miał spójność. Alternatywnie ustaw next()wartość domyślną na -1.
Ben Hoyt
1
@ gdw2: Rozumiem, SyntaxError: Generator expression must be parenthesized if not sole argumentkiedy to robię.
avoliva
2
@avoliva dodaj nawias wokół następnego, jak poniżejnext((index for (index, d) in enumerate(lst) if d["name"] == "Tom"), None)
HussienK
46

Prosta, czytelna wersja to

def find(lst, key, value):
    for i, dic in enumerate(lst):
        if dic[key] == value:
            return i
    return -1
Emile
źródło
8
Wydaje się, że jest to najbardziej czytelne i Pythonic. str.find()Ładnie naśladuje również zachowanie . Możesz też to index()sprawdzić i podbić ValueErrorzamiast zwracać -1, jeśli wolisz.
Ben Hoyt,
7
Zgoda - zwracając -1, gdy nie zostanie znalezione dopasowanie, zawsze otrzymasz ostatnią dyktę na liście, co prawdopodobnie nie jest tym, czego chcesz. Lepiej jest zwrócić None i sprawdzić, czy istnieje dopasowanie w kodzie wywołującym.
shacker
10

Nie będzie to wydajne, ponieważ musisz przejść się po liście, sprawdzając każdy element na niej (O (n)). Jeśli chcesz wydajności, możesz użyć dyktowania . Jeśli chodzi o pytanie, oto jeden możliwy sposób, aby go znaleźć (jeśli jednak chcesz trzymać się tej struktury danych, w rzeczywistości bardziej wydajne jest użycie generatora, jak napisał Brent Newey w komentarzach; zobacz także odpowiedź toklanda):

>>> 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
źródło
1
Możesz uzyskać pożądaną wydajność, używając generatora. Zobacz odpowiedź tokland.
Brent Newey
2
@Brent Newey: Generator nie zmienia faktu, że musisz przejść całą listę, wykonując wyszukiwanie O (n), jak twierdzi aeter ... W zależności od tego, jak długa jest ta lista, różnica między użyciem generatora a użyciem pętla for lub cokolwiek, co może być pomijalne, czy różnica między używaniem dyktu a używaniem listy może nie
Dirk
@Brent: Masz rację, ale czy może pokonać wyszukiwanie O (1) w słowniku, ponadto, jeśli szukana pozycja znajduje się na końcu listy?
aeter
1
@Dirk Wywołanie next () generatora zatrzymuje się po znalezieniu dopasowania, dlatego nie musi przechodzić przez całą listę.
Brent Newey
@aeter Masz słuszną opinię. Miałem na myśli możliwość zatrzymania się po znalezieniu dopasowania.
Brent Newey
3

Wydaje się najbardziej logiczne użycie kombinacji filtr / indeks:

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

A jeśli uważasz, że może być wiele dopasowań:

[names.index(n) for item in filter(lambda n: n.get('name') == 'Tom', names)]
[1]
michael łosoś
źródło
2

Oto funkcja, która znajduje pozycję indeksu słownika, jeśli istnieje.

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
źródło
2

Odpowiedź oferowana przez @faham jest ładną linijką, ale nie zwraca indeksu do słownika zawierającego wartość. Zamiast tego zwraca sam słownik. Oto prosty sposób, aby uzyskać: Listę indeksów, jeden lub więcej, jeśli jest więcej niż jeden, lub pustą listę, jeśli nie ma:

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

Wynik:

>>> [1]

W tym podejściu podoba mi się to, że dzięki prostej edycji można uzyskać listę zarówno indeksów, jak i słowników jako krotki. To jest problem, który musiałem rozwiązać i znalazłem te odpowiedzi. Poniżej dodałem zduplikowaną wartość w innym słowniku, aby pokazać, jak to działa:

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

Wynik:

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

To rozwiązanie znajduje wszystkie słowniki zawierające słowo „Tom” w dowolnej z ich wartości.

stanely
źródło
1

Jedna wkładka !?

elm = ([i for i in mylist if i['name'] == 'Tom'] or [None])[0]
faham
źródło
1

Potrzebowałem bardziej ogólnego rozwiązania, aby uwzględnić możliwość wielu słowników na liście mających wartość kluczową oraz prostą implementację przy użyciu funkcji rozumienia list:

dict_indices = [i for i, d in enumerate(dict_list) if d[dict_key] == key_value] 
Sebastian Timar
źródło
0

Dla danej iterowalności more_itertools.locatezwraca pozycje pozycji, które spełniają predykat.

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_itertoolsto zewnętrzna biblioteka, która implementuje receptury itertools wśród innych przydatnych narzędzi.

pylang
źródło
0
def search(itemID,list):
     return[i for i in list if i.itemID==itemID]
Rohan Kumara
źródło