72

Python's list type has an index() method that takes one parameter and returns the index of the first item in the list matching the parameter. For instance:

>>> some_list = ["apple", "pear", "banana", "grape"]
>>> some_list.index("pear")
1
>>> some_list.index("grape")
3

Is there a graceful (idiomatic) way to extend this to lists of complex objects, like tuples? Ideally, I'd like to be able to do something like this:

>>> tuple_list = [("pineapple", 5), ("cherry", 7), ("kumquat", 3), ("plum", 11)]
>>> some_list.getIndexOfTuple(1, 7)
1
>>> some_list.getIndexOfTuple(0, "kumquat")
2

getIndexOfTuple() is just a hypothetical method that accepts a sub-index and a value, and then returns the index of the list item with the given value at that sub-index. I hope

Is there some way to achieve that general result, using list comprehensions or lambas or something "in-line" like that? I think I could write my own class and method, but I don't want to reinvent the wheel if Python already has a way to do it.

Wolf
  • 9,679
  • 7
  • 62
  • 108
Ryan B. Lynch
  • 2,307
  • 3
  • 21
  • 21

13 Answers13

82

How about this?

>>> tuple_list = [("pineapple", 5), ("cherry", 7), ("kumquat", 3), ("plum", 11)]
>>> [x for x, y in enumerate(tuple_list) if y[1] == 7]
[1]
>>> [x for x, y in enumerate(tuple_list) if y[0] == 'kumquat']
[2]

As pointed out in the comments, this would get all matches. To just get the first one, you can do:

>>> [y[0] for y in tuple_list].index('kumquat')
2

There is a good discussion in the comments as to the speed difference between all the solutions posted. I may be a little biased but I would personally stick to a one-liner as the speed we're talking about is pretty insignificant versus creating functions and importing modules for this problem, but if you are planning on doing this to a very large amount of elements you might want to look at the other answers provided, as they are faster than what I provided.

Paolo Bergantino
  • 480,997
  • 81
  • 517
  • 436
  • Nice solution, but will not produce the desired result: it will not return the index of the first item only, but will iterate over the whole list and return all matches. – van Jun 03 '09 at 20:58
  • 2
    Still creates a new list of size N in memory, which isn't necessary. Also runs in O(n) average case, which can be improved to O(n/2). Yes I know that's still O(n) technically. – Kenan Banks Jun 03 '09 at 21:14
  • The issue van raise is easily resolved by just picking the first result ([0]) from the list of multiple matches. Interestingly, if I run the same speed test as I did in the comments to my answer with a) Paolo's original enumerate comphrehension, b) Paolo's revised comprehension and index, and c) the map/operator/index approach from my answer, option C is the when there's more than one match in tuple_list (ie: more than one "kumquat"). B is next best. A is slowest. This is fun! – Jarret Hardie Jun 03 '09 at 21:19
  • Can you throw Triptych's in that test? :) – Paolo Bergantino Jun 03 '09 at 21:20
  • Sure... and we have a winner. I used Triptych's second (super performant) example that returns as soon as it finds the result as his first example was essentially the same as mine, but with an extra function all. The super-performant version is indeed the fastest. – Jarret Hardie Jun 03 '09 at 21:29
  • 1
    Though I should repeat that this test is obviously a quick one-off, done under conditions that may not mirror someone's production need, and is hardly well-planned, so take it with a truck-load of salt. – Jarret Hardie Jun 03 '09 at 21:30
  • Naturally. Thanks Jarret. I wish I could upvote you more than once. :) – Paolo Bergantino Jun 03 '09 at 21:31
  • This is pretty much exactly what I wanted to do. Thanks! – Ryan B. Lynch Jun 09 '09 at 20:44
29

Those list comprehensions are messy after a while.

I like this Pythonic approach:

from operator import itemgetter

tuple_list = [("pineapple", 5), ("cherry", 7), ("kumquat", 3), ("plum", 11)]

def collect(l, index):
   return map(itemgetter(index), l)

# And now you can write this:
collect(tuple_list,0).index("cherry")   # = 1
collect(tuple_list,1).index("3")        # = 2

If you need your code to be all super performant:

# Stops iterating through the list as soon as it finds the value
def getIndexOfTuple(l, index, value):
    for pos,t in enumerate(l):
        if t[index] == value:
            return pos

    # Matches behavior of list.index
    raise ValueError("list.index(x): x not in list")

getIndexOfTuple(tuple_list, 0, "cherry")   # = 1
Intrastellar Explorer
  • 3,005
  • 9
  • 52
  • 119
Kenan Banks
  • 207,056
  • 34
  • 155
  • 173
  • 2
    +1 as the super performant is indeed the fastest solution posted. I would personally still stick to the one liner as the speed difference at this level is pretty meaningless but it's good to know anyways. – Paolo Bergantino Jun 03 '09 at 21:32
  • Thanks. Normally I would use the collect() version - looks so much nicer. – Kenan Banks Jun 03 '09 at 21:40
11

One possibility is to use the itemgetter function from the operator module:

import operator

f = operator.itemgetter(0)
print map(f, tuple_list).index("cherry") # yields 1

The call to itemgetter returns a function that will do the equivalent of foo[0] for anything passed to it. Using map, you then apply that function to each tuple, extracting the info into a new list, on which you then call index as normal.

map(f, tuple_list)

is equivalent to:

[f(tuple_list[0]), f(tuple_list[1]), ...etc]

which in turn is equivalent to:

[tuple_list[0][0], tuple_list[1][0], tuple_list[2][0]]

which gives:

["pineapple", "cherry", ...etc]
Jarret Hardie
  • 95,172
  • 10
  • 132
  • 126
  • That's neat. I wonder if this or the list comprehension is faster? Either way, +1. – Paolo Bergantino Jun 03 '09 at 20:15
  • The problem with this is that you are iterating twice to get the index. – Nadia Alramli Jun 03 '09 at 20:15
  • 2
    Paolo asks an interesting question... as I think everyone suspects, the list comprehension and enumerate approach is slightly faster... over 100000 runs on my ever-so-scientific test, the enumerate approach was about 10milliseconds faster. – Jarret Hardie Jun 03 '09 at 20:23
  • Cool. Thanks for doing the test. – Paolo Bergantino Jun 03 '09 at 20:24
  • I think Paolo and I should blend answers :-) After he edited his answer, I re-ran the speed tests for cases where there are more than one match in the tuple_list... and the operator approach is fastest... see my comment in Paolo's answer. – Jarret Hardie Jun 03 '09 at 21:20
8

You can do this with a list comprehension and index()

tuple_list = [("pineapple", 5), ("cherry", 7), ("kumquat", 3), ("plum", 11)]
[x[0] for x in tuple_list].index("kumquat")
2
[x[1] for x in tuple_list].index(7)
1
Alasdair
  • 298,606
  • 55
  • 578
  • 516
6

Inspired by this question, I found this quite elegant:

>>> tuple_list = [("pineapple", 5), ("cherry", 7), ("kumquat", 3), ("plum", 11)]
>>> next(i for i, t in enumerate(tuple_list) if t[1] == 7)
1
>>> next(i for i, t in enumerate(tuple_list) if t[0] == "kumquat")
2
Community
  • 1
  • 1
Claudiu
  • 224,032
  • 165
  • 485
  • 680
2

I would place this as a comment to Triptych, but I can't comment yet due to lack of rating:

Using the enumerator method to match on sub-indices in a list of tuples. e.g.

li = [(1,2,3,4), (11,22,33,44), (111,222,333,444), ('a','b','c','d'),
        ('aa','bb','cc','dd'), ('aaa','bbb','ccc','ddd')]

# want pos of item having [22,44] in positions 1 and 3:

def getIndexOfTupleWithIndices(li, indices, vals):

    # if index is a tuple of subindices to match against:
    for pos,k in enumerate(li):
        match = True
        for i in indices:
            if k[i] != vals[i]:
                match = False
                break;
        if (match):
            return pos

    # Matches behavior of list.index
    raise ValueError("list.index(x): x not in list")

idx = [1,3]
vals = [22,44]
print getIndexOfTupleWithIndices(li,idx,vals)    # = 1
idx = [0,1]
vals = ['a','b']
print getIndexOfTupleWithIndices(li,idx,vals)    # = 3
idx = [2,1]
vals = ['cc','bb']
print getIndexOfTupleWithIndices(li,idx,vals)    # = 4
Nisan.H
  • 6,032
  • 2
  • 26
  • 26
1

ok, it might be a mistake in vals(j), the correction is:

def getIndex(li,indices,vals):
for pos,k in enumerate(lista):
    match = True
    for i in indices:
        if k[i] != vals[indices.index(i)]:
            match = False
            break
    if(match):
        return pos
Jon Lin
  • 142,182
  • 29
  • 220
  • 220
1
z = list(zip(*tuple_list))
z[1][z[0].index('persimon')]
o17t H1H' S'k
  • 2,541
  • 5
  • 31
  • 52
0
tuple_list = [("pineapple", 5), ("cherry", 7), ("kumquat", 3), ("plum", 11)]

def eachtuple(tupple, pos1, val):
    for e in tupple:
        if e == val:
            return True

for e in tuple_list:
    if eachtuple(e, 1, 7) is True:
        print tuple_list.index(e)

for e in tuple_list:
    if eachtuple(e, 0, "kumquat") is True:
        print tuple_list.index(e)
Wolf
  • 9,679
  • 7
  • 62
  • 108
0

This is also possible using Lambda expressions:

l = [('rana', 1, 1), ('pato', 1, 1), ('perro', 1, 1)]
map(lambda x:x[0], l).index("pato") # returns 1 
Edit to add examples:
l=[['rana', 1, 1], ['pato', 2, 1], ['perro', 1, 1], ['pato', 2, 2], ['pato', 2, 2]]

extract all items by condition:

filter(lambda x:x[0]=="pato", l) #[['pato', 2, 1], ['pato', 2, 2], ['pato', 2, 2]]

extract all items by condition with index:

>>> filter(lambda x:x[1][0]=="pato", enumerate(l))
[(1, ['pato', 2, 1]), (3, ['pato', 2, 2]), (4, ['pato', 2, 2])]
>>> map(lambda x:x[1],_)
[['pato', 2, 1], ['pato', 2, 2], ['pato', 2, 2]]

Note: The _ variable only works in the interactive interpreter. More generally, one must explicitly assign _, i.e. _=filter(lambda x:x[1][0]=="pato", enumerate(l)).

MRule
  • 529
  • 1
  • 6
  • 18
Wallebot
  • 767
  • 5
  • 8
  • I think this solution (`map(lambda x:x[0], l).index("pato")`) is actually one of the better ones, but I suspect the author does not speak English. Would anyone be willing to re-write this? Alternatively, is it acceptable in this community to wholly rewrite an answer from scratch if its author does not speak English? – MRule Oct 24 '21 at 10:35
0

Python's list.index(x) returns index of the first occurrence of x in the list. So we can pass objects returned by list compression to get their index.

>>> tuple_list = [("pineapple", 5), ("cherry", 7), ("kumquat", 3), ("plum", 11)]
>>> [tuple_list.index(t) for t in tuple_list if t[1] == 7]
[1]
>>> [tuple_list.index(t) for t in tuple_list if t[0] == 'kumquat']
[2]

With the same line, we can also get the list of index in case there are multiple matched elements.

>>> tuple_list = [("pineapple", 5), ("cherry", 7), ("kumquat", 3), ("plum", 11), ("banana", 7)]
>>> [tuple_list.index(t) for t in tuple_list if t[1] == 7]
[1, 4]
Arya McCarthy
  • 8,554
  • 4
  • 34
  • 56
dspmeng
  • 1
  • 1
  • 1
    Hello, and welcome to StackOverflow. Please add some explanation to your answer. – Chait Jun 09 '17 at 02:13
  • 1
    This is [accidentally quadratic](https://accidentallyquadratic.tumblr.com). Instead, you should use `enumerate`: `[idx for idx, t in enumerate(tuple_list) if t[1] == 7]`. – Arya McCarthy Jun 09 '17 at 02:58
0

I guess the following is not the best way to do it (speed and elegance concerns) but well, it could help :

from collections import OrderedDict as od
t = [('pineapple', 5), ('cherry', 7), ('kumquat', 3), ('plum', 11)]
list(od(t).keys()).index('kumquat')
2
list(od(t).values()).index(7)
7
# bonus :
od(t)['kumquat']
3

list of tuples with 2 members can be converted to ordered dict directly, data structures are actually the same, so we can use dict method on the fly.

jerome
  • 183
  • 2
  • 14
0

I came up with a quick and dirty approach using max and lambda.

>>> tuple_list = [("pineapple", 5), ("cherry", 7), ("kumquat", 3), ("plum", 11)]
>>> target = 7
>>> max(range(len(tuple_list)), key=lambda i: tuple_list[i][1] == target)
1

There is a caveat though that if the list does not contain the target, the returned index will be 0, which could be misleading.

>>> target = -1
>>> max(range(len(tuple_list)), key=lambda i: tuple_list[i][1] == target)
0
nightwuffle
  • 311
  • 3
  • 10