What is the most pythonic way to have inverse enumerate of a list?
Asked Answered
B

2

16

The first thing that comes to mind is:

>>> l = list('abcdef')
>>> for i in range(len(l)-1, -1, -1):
...   item = l[i]
...   print(i, item)
...
5 f
4 e
3 d
2 c
1 b
0 a

I tried using the following:

>>> l
['a', 'b', 'c', 'd', 'e', 'f']
>>> for i,ch in reversed(enumerate(l)):
...   print(i,ch)
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'enumerate' object is not reversible

but apparently 'enumerate' object is not reversible. I can trick it with:

>>> for i,ch in reversed(list(enumerate(l))):
...   print(i,ch)
...
5 f
4 e
3 d
2 c
1 b
0 a

but that doesn't feel right - it's a bit cumbersome. Is there a better way to do this? Maybe some inverse_enumerate hidden is a lib like collections or itertools?

Brainchild answered 15/2, 2018 at 12:54 Comment(5)
The first thing that came to mind is probably the best option here.Sybarite
You can enumerate from 1, and negate the index, because -1 == 5, -2 == 4, and so on, with python indexing. If not, you can still use len(string) - i and get the value you're looking for.Chancellor
list(enumerate(s))[::-1]Know
This is what I did: reversed(list(enumerate(array))) - You have to convert the generator to a list before reversing it.Excise
sorted( enumerate( l, reverse=True ) )Dairen
C
5

That might not be the most pythonic approach, but you could reuse the code provided by the documentation on enumerate to re-implement your own reversed enumerate.

The doc provides the following code:

def enumerate(sequence, start=0):
    n = start
    for elem in sequence:
        yield n, elem
        n += 1

Here is a proposal for a renumerate function, that yields elements of a sequence in the reversed order:

def renumerate(sequence, start=None):
    n = start
    if start is None:
        n = len(sequence) - 1
    for elem in sequence[::-1]:
        yield n, elem
        n -= 1

Unfortunately, this will not work with generators, since it requires knowing the length of the sequence.

Convey answered 15/2, 2018 at 12:59 Comment(4)
Whats wrong with sorted( enumerate(), reverse=True )?Dairen
@Dairen that sorts, which is not what is wanted here.Squeamish
@AdamBarnes this sorts based on the first item of the tuples yielded by enumerate so it's fine - however the sorting might be inefficientConvey
My mistake, I misunderstood completely.Squeamish
D
2

I'm not sure about the definition of pythonic but you can do the following (in increasing order of complexity).

TLDR: Take method 3.

For comparison, I also list the maximum memory footprints (M) using Gnu time as well as the computation times (T) using the jupyters built-in function %timeit -n 4 -r 4.

The reference size

/usr/bin/time -v python3 -c 'list( int(1e6) * "abcdef" )'

results in 61.4 MB.


Method 1

As suggested earlier:

reversed( list( enumerate(l) ) )

enumerate returns a generator, and generators are not invertible.

So list internally creates a new list by iterating over the entire generator (until a StopIteration exception is thrown) and reversed creates a generator along that list in reverse order.

This is simple and probably "pythonic", but creates significant memory overhead (during copying and due to the copying itself).

M T (ms per loop [mean ± std. dev. of 4 runs, 4 loops each])
673.6 MB 1.03 s ± 61.3 ms

Method 2

As written in my comment, you can also use

sorted( enumerate(), reverse=True )

which is slighly more clear as the above and does pretty much the same. sorted also internally create a copy with the associated memory-overhead but additionally a sorting operation. As sorting a sorted list is rather fast, the run-time overhead is tiny.

So you are trading readability for runtime here.

M T (ms per loop [mean ± std. dev. of 4 runs, 4 loops each])
673.6 MB 1.28 s ± 30.2 ms

Method 3

For what you ask, as far as I know, there is no built-in generator or way to chain generators. But you can easily write one yourself, and that is what I recommend:

def enumerate_reversed(l):
    return zip(range(len(l)-1, -1, -1), reversed(l))

Or as one-liner:

enumerate_reversed = lambda l: zip(range(len(l)-1, -1, -1), reversed(l))

reversed, range and zip all return generators. This way you get an inverted enumerate without creating any intermediate copies.

M T (ms per loop [mean ± std. dev. of 4 runs, 4 loops each]) T (lambda)
61.8 MB 1.04 s ± 26.7 ms 1.04 s ± 30.6 ms per loop

Appendix:

Commands used:

Method Command
1 /usr/bin/time -f "%M" python3 -c 'reversed(list(enumerate(list(int(1e6)*"abcdef"))))'
2 /usr/bin/time -f "%M" python3 -c 'sorted(enumerate(list(int(1e6)*"abcdef")),reverse=True)'
3 /usr/bin/time -f "%M" python3 -c 'f = lambda l: zip(range(len(l)-1, -1, -1), reversed(l)); f(list(int(1e6)*"abcdef"))'
Dairen answered 12/2 at 11:52 Comment(3)
to long, didnt read: Its used to guide lazy readers to the core informationMangan
@mkrieger1 Sorry for the created confusion. I adapted the answer accordingly.Dairen
Method 3 is what I would recommend. Note the that generator expression isn't necessary; lambda l: zip(range(...), reversed(l)) is equivalent.Hoagy

© 2022 - 2024 — McMap. All rights reserved.