Currently python provides enumerate(Iterable)
for forward enumeration. The API returns a generator
of tuples of index and item.
However, there lacks a builtin function to enumerate from the back of a Sequence
(Iterable plus len and getitem). Currently the most popular solution on StackOverflow is reversed(list(enumurate(...)))
(reference to this post). This consumes the entire generator and stores them as a list before it can be reversed.
I propose providing an additional builtin function enumerate_back
or add an optional flag enumerate(..., reversed=True)
to support reversed enumeration of Sequence
objects.
P.S. For the flag version, the input type should be narrowed from
Iterable[T]
toSequence[T]
.
Here is a demo python implementation of enumerate_back
:
from typing import TypeVar, Sequence, Generator, Any
T = TypeVar("T")
def enumerate_back(seq: Sequence[T]) -> Generator[tuple[int, T], Any, None]:
if not hasattr(seq, "__len__") or not hasattr(seq, "__getitem__"):
raise TypeError(f"{seq} does not support sequencing")
indexes = range(len(seq) - 1, -1, -1)
items = reversed(seq)
yield from zip(indexes, items)
Given that __getitem__()
may incur reading from file or even a remote fetch, the proposed API will be more memory efficient than the solution from the aforementioned SO post which consumes everything before reversing.
My own use case (ROS2)
import rclpy
from rclpy.node import Node
from collections import deque
class TaskScheduler(Node):
def __init__(self, maxlen: int = 10):
self.task_queue = deque[tuple[float, callable]](maxlen=maxlen)
# Irrelevant code omitted ...
def schedule_task(self, ddl: float, task: callable):
while len(self.task_queue) >= self.task_queue.maxlen:
# No timeout (wait forever) - we need at least one task to be executed
rclpy.spin_once(self)
# Insert the task into task queue in ascending order
# Searching from back because the new task is likely to be the last
for idx, (ts, _) in enumerate_back(self.task_queue):
if ts < ddl:
self.task_queue.insert(idx + 1, (ddl, task))
break
else:
self.task_queue.appendleft((ddl, task))
EDIT: inspired by an earlier GitHub issue shared by @Stefan2 (thanks!), I came up with another alternative approach of implementing this feature. IMO this will be the most ideal one among all proposed solutions:
from typing import Iterable
import builtins
class enumerate(builtins.enumerate):
# EDIT2: make `isinstance(..., collections.abc.Reversible)` happy.
def __new__(cls, obj, **kwargs):
if hasattr(obj, "__len__") and hasattr(obj, "__reversed__"):
return cls.reversible(obj, **kwargs)
else:
return builtins.enumerate(obj, **kwargs)
class reversible(builtins.enumerate):
def __init__(self, iterable: Iterable, start: int=0):
self.iterable = iterable
self.start = start
def __reversed__(self):
indexes = range(len(self.iterable) - 1, self.start - 1, -1)
items = reversed(self.iterable)
return zip(indexes, items)
Usage:
# Works on all objects that supports len() and reversed()
reversed(enumerate(range(10)))
# Throws TypeError for objects that do not support these methods
# (e.g. a Generator)
reversed(enumerate((i for i in range(10)))) # TypeError