• 5
name

A PHP Error was encountered

Severity: Notice

Message: Undefined index: userid

Filename: views/question.php

Line Number: 191

Backtrace:

File: /home/prodcxja/public_html/questions/application/views/question.php
Line: 191
Function: _error_handler

File: /home/prodcxja/public_html/questions/application/controllers/Questions.php
Line: 433
Function: view

File: /home/prodcxja/public_html/questions/index.php
Line: 315
Function: require_once

I have a list of dicts:

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

How can I efficiently find the index position [0],[1], or [2] by matching on name = 'Tom'?

If this were a one-dimensional list I could do list.index() but I'm not sure how to proceed by searching the values of the dicts within the list.

      • 1
    • "list" is the list constructor, you better choose another name for a list (even in a example). And what should be the response if no element is found? raise an exception? return None?
      • 2
    • If you're going to need this a lot, use a more appropriate data structure (perhaps { 'Jason': {'id': '1234'}, 'Tom': {'id': '1245'}, ...}?)
      • 2
    • @delnan Because that's a recipe for disaster! If anything, it should be {'1234': {'name': 'Jason'}, ...}. Not that that would help this use-case.
tom_index = next((index for (index, d) in enumerate(lst) if d["name"] == "Tom"), None)
# 1

If you need to fetch repeatedly from name, you should index them by name (using a dictionary), this way get operations would be O(1) time. An 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'}
  • 145
Reply Report
      • 1
    • IMHO this is not as readable or Pythonic is @Emile's answer. Because the intention is not really to create a generator (and using next() for this seems weird to me), the aim is just to get the index. Also, this raises StopIteration, whereas the Python lst.index() method raises ValueError.
    • @benhoyt: I don't like the StopIteration exception either, but while you can change next()'s default value, the exception it raises is fixed. The pythonicity is somewhat subjective so I won't dispute it, probably a for-loop is more pythonic. On the other hand, some people aliases next() for first(), and that definitely sounds better: first(index for (index, d) in ...).
      • 2
    • first() does sound better. You could always try/except the StopIteration and raise ValueError so the caller has consistency. Alternatively set next()'s default to -1.
      • 2
    • @gdw2: I get SyntaxError: Generator expression must be parenthesized if not sole argument when doing that.

A simple readable version is

def find(lst, key, value):
    for i, dic in enumerate(lst):
        if dic[key] == value:
            return i
    return -1
  • 45
Reply Report
      • 2
    • Agreed - by returning -1 when no match is found, you'll always get the last dict in the list, which is probably not what you want. Better to return None and check for exsistence of a match in the calling code.
      • 2
    • This seems the most readable and Pythonic. It also mimics the behaviour of str.find() nicely. You could also call it index() and raise a ValueError instead of returning -1 if that was preferable.

It won't be efficient, as you need to walk the list checking every item in it (O(n)). If you want efficiency, you can use dict of dicts. On the question, here's one possible way to find it (though, if you want to stick to this data structure, it's actually more efficient to use a generator as Brent Newey has written in the comments; see also tokland's answer):

>>> L = [{'id':'1234','name':'Jason'},
...         {'id':'2345','name':'Tom'},
...         {'id':'3456','name':'Art'}]
>>> [i for i,_ in enumerate(L) if _['name'] == 'Tom'][0]
1
  • 9
Reply Report
    • @Brent Newey: The generator does not change the fact, that you have to traverse the entire list, making the search O(n) as aeter claims... Depending on how long that list is, the difference between using a generator vs using a for loop or whatever might be neglible, wheras the difference between using a dict vs. using a list might not
      • 1
    • @Brent: You are right, but can it beat a O(1) lookup in a dictionary, moreover if the searched item is at the end of the list?
      • 1
    • @Dirk The next() call on the generator stops when a match is found, therefore it does not have to traverse the entire list.

Here's a function that finds the dictionary's index position if it exists.

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
Reply Report

Seems most logical to use a filter/index combo:

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

And if you think there could be multiple matches:

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

Answer offered by @faham is a nice one-liner, but it doesn't return the index to the dictionary containing the value. Instead it returns the dictionary itself. Here is a simple way to get: A list of indexes one or more if there are more than one, or an empty list if there are none:

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

Output:

>>> [1]

What I like about this approach is that with a simple edit you can get a list of both the indexes and the dictionaries as tuples. This is the problem I needed to solve and found these answers. In the following, I added a duplicate value in a different dictionary to show how it works:

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

Output:

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

This solution finds all dictionaries containing 'Tom' in any of their values.

  • 2
Reply Report

For a given iterable, more_itertools.locate yields positions of items that satisfy a predicate.

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 is a third-party library that implements itertools recipes among other useful tools.

  • 0
Reply Report

Trending Tags