There's a lot of answers here, so I thought it'd be interesting to compare different approaches.
I've also came up with a pretty good solution not listed here and created a PyPI package.
Let's get straight to the point, here's the code I've used for benchmarking:
from timeit import timeit
from itertools import dropwhile
from operator import indexOf
from rindex import rindex
def rindex1(li, value):
return len(li) - 1 - li[::-1].index(value)
def rindex2(li, value):
for i in reversed(range(len(li))):
if li[i] == value:
return i
raise ValueError("{} is not in list".format(value))
def rindex3(li, value):
return max(loc for loc, val in enumerate(li) if val == value)
def rindex4(li, value):
for r_idx, elt in enumerate(reversed(li)):
if elt == value:
return len(li) - 1 - r_idx
def rindex5(li, value):
return next(dropwhile(lambda x: li[x] != value, reversed(range(len(li)))))
def rindex6(li, value):
return dict(map(reversed, enumerate(li)))[value]
def rindex7(seq, value, start=None, stop=None):
"""L.rindex(value, [start, [stop]]) -> integer -- return last index of value.
Raises ValueError if the value is not present."""
start, stop, _ = slice(start, stop).indices(len(seq))
if stop == 0:
# start = 0
raise ValueError("{!r} is not in list".format(value))
else:
stop -= 1
start = None if start == 0 else start - 1
return stop - seq[stop:start:-1].index(value)
# Thanks Kelly Bundy for mentioning this in the comments
# https://stackoverflow.com/a/63834895
def rindex8(li, value):
return len(li) - 1 - indexOf(reversed(li), value)
def my_rindex(li, value, start=0, stop=None, /):
size = len(li)
li.reverse()
try:
return (
size - 1 - li.index(value, 0 if stop is None else size - stop, size - start)
)
finally:
li.reverse()
FUNCTIONS = (rindex, rindex1, rindex2, rindex3, rindex4, rindex5, rindex6, rindex7, rindex8, my_rindex)
FUNCTIONS_WITH_BOUNDARIES = (rindex7, my_rindex, rindex)
def test_correctness():
li = [0, 1, 0, 2]
for f in FUNCTIONS:
assert f(li, 0) == 2
for f in FUNCTIONS_WITH_BOUNDARIES:
assert f(li, 0, 0, 1) == 0
def test_speed():
small = list(range(10))
big = list(range(1_000_000))
tests = (
("small best", small, len(small) - 1, 1_000_000),
("big best", big, len(big) - 1, 100),
("small middle", small, len(small) // 2, 1_000_000),
("big middle", big, len(big) // 2, 100),
("small worst", small, 0, 1_000_000),
("big worst", big, 0, 100),
)
for name, li, value, repeats in tests:
print(format(f" {name} ", "=^22"))
for f in (list.index,) + FUNCTIONS:
print(
f.__name__.ljust(10),
":",
format(
timeit(
"f(li, value)",
globals={
"f": f,
"li": li,
"value": value
if f is not list.index
else len(li) - 1 - value,
},
number=repeats,
),
"9f",
),
)
if __name__ == "__main__":
test_correctness()
test_speed()
And the results:
===== small best =====
index : 0.124899
rindex : 0.168111
rindex1 : 0.711771
rindex2 : 1.042878
rindex3 : 3.031604
rindex4 : 1.018811
rindex5 : 2.309284
rindex6 : 8.428993
rindex7 : 1.536563
rindex8 : 0.714625
my_rindex : 0.751303
====== big best ======
index : 0.000020
rindex : 0.000030
rindex1 : 2.520536
rindex2 : 0.000187
rindex3 : 12.694951
rindex4 : 0.000135
rindex5 : 0.000276
rindex6 : 82.994252
rindex7 : 2.820221
rindex8 : 0.000113
my_rindex : 0.619653
==== small middle ====
index : 0.264816
rindex : 0.325221
rindex1 : 0.875725
rindex2 : 1.440296
rindex3 : 3.100110
rindex4 : 1.424884
rindex5 : 3.330751
rindex6 : 8.430511
rindex7 : 1.686265
rindex8 : 0.888111
my_rindex : 0.888112
===== big middle =====
index : 1.813371
rindex : 1.747949
rindex1 : 4.465189
rindex2 : 5.886711
rindex3 : 12.696632
rindex4 : 6.856758
rindex5 : 13.855785
rindex6 : 84.861101
rindex7 : 4.457431
rindex8 : 2.044351
my_rindex : 2.299312
==== small worst =====
index : 0.452868
rindex : 0.456035
rindex1 : 1.016495
rindex2 : 1.846306
rindex3 : 3.038056
rindex4 : 1.901025
rindex5 : 4.539291
rindex6 : 8.428476
rindex7 : 1.841232
rindex8 : 1.061607
my_rindex : 1.054495
===== big worst ======
index : 3.620210
rindex : 3.083546
rindex1 : 5.824754
rindex2 : 11.764573
rindex3 : 12.696748
rindex4 : 13.481179
rindex5 : 27.432877
rindex6 : 82.762718
rindex7 : 5.880688
rindex8 : 3.685528
my_rindex : 3.725783
Here, the "best" case means that the searched value is rightmost, "middle" - in the middle, "worst" - leftmost.
Technically, the worst case would be absence of the searched value, but handling of exceptions may mess up the measurements.
rindex
from my rindex
package is superior (even beats the built-in index
in some cases) because it's written in Cython. Use it if you need the best performance.
Comparison of pure Python functions:
We can also exclude rindex3
, rindex5
and rindex6
from the competition: they do not have advantages.
rindex2
and rindex4
are almost the same, but 4 is a little faster for "good" cases, and for the "bad" ones it's the opposite.
They're also excluded because rindex8
is faster in every case.
rindex1
is probably the best one for small lists (though, rindex8
and my_rindex
are only a bit slower).
For other cases rindex8
is the fastest (even comparable to the built-in index
in the "big worst" case).
my_rindex
is almost as fast, but slows down dramatically in the "big best" case.
It supports boundaries, though.
However, if you use boundaries that effectively reduce a big list to a small list, rindex7
will be your friend.