0

If I'm looping through Python's iterator, how can I find out if now is the last element? I'm looking for some standard library way of doing this, maybe some function in itertools, is there such? Or some other very short and Pythonic way of doing this.

Of cause I can implement my own helper generator achieving this task:

Try it online!

def is_last(it):
    first = True
    for e in it:
        if not first:
            yield False, prev
        else:
            first = False
        prev = e
    if not first:
        yield True, prev

for last, e in is_last(range(4)):
    print(last, e)

Which outputs:

False 0
False 1
False 2
True 3

Also if I'm looping through simple range I can always compare to upper limit (below is solution for positive step):

Try it online!

start, stop, step = 10, 21, 3
for i in range(start, stop, step):
    last = i + step >= stop
    print(last, i)

Which outputs:

False 10
False 13
False 16
True 19

This above is also obvious and probably easiest solution for the case of simple range, unfortunatelly it needs keeping stop/step in separate variables. But anyway range-case is not very interesting. What about the case of any arbitrary iterator, is there a Pythonic way to solve this case?

Arty
  • 14,883
  • 6
  • 36
  • 69
  • There's no way to know. Some iterators don't have a last element, or they generate elements dynamically so there's no way to tell until you try to get it. – Barmar May 06 '21 at 19:20
  • @Barmar I understand that you have to get it. That's why I provided my own solution helper generator `is_last()` which gets element in advance. But I wanted to know if there is some function for doing this in `itertools` library? – Arty May 06 '21 at 19:21
  • There are forms of infinite iterarators in Python such as `while True: yield something` where there just is no last element. – dawg May 06 '21 at 19:23
  • @dawg After looking at his `is_last` iterator, I can see that he's OK with it always returning `false` in such cases. – Barmar May 06 '21 at 19:24
  • @dawg I don't want to get last element. I just want to know if current element is last or not. If it is infinite iterator then a function should just always return False for the question of `is last?`. – Arty May 06 '21 at 19:25
  • Actually I solved it already through my `is_last()` generator. I just wanted to know if there is such function already in `itertools` or other standard libraries. – Arty May 06 '21 at 19:26
  • I don't think there's anything in itertools that does this. Note that your solution is always reading the original iterator one step ahead of the caller. This could potentially be a problem with some iterators, e.g. reading a file. – Barmar May 06 '21 at 19:27
  • E.g. if you use `for last, line in is_last(file)` and then do `file.seek(0)`, you'll read the line after where you seek. – Barmar May 06 '21 at 19:28
  • @Barmar There is no way to tell in advance that it is last element, so caller of my function `is_last()` should be aware that it may cause problems of read-ahead. – Arty May 06 '21 at 19:28
  • Which is why there's no standard library for this, it doesn't generalize well. – Barmar May 06 '21 at 19:29
  • @Barmar Understood! Thanks a lot for your quick response. – Arty May 06 '21 at 19:29

1 Answers1

0

I have used something like this before. It's not a great solution because it's easy to misuse, but it could work.

def peek(itr: Iterator[T]) -> Tuple[Optional[T], Optional[Iterator[T]]]:
    """
    Return a pair of (first element of iterator, new iterator).  If iterator is exhausted, return (None, None).
    https://stackoverflow.com/a/664239/3282436

    NOTE: Because checking for values consumes part of the supplied iterator, that iterator is invalidated.  To continue
          iterating, you *must* use the iterator returned from peek.

    >>> itr = iter(range(3))
    >>> if recipes.peek(itr)[0] == 0:  # WRONG, itr will be missing first element
    >>>     # ...

    >>> itr = iter(range(3))
    >>> first, itr = recipes.peek(itr)  # Correct
    >>> if first == 0:
    >>>     # ...
    """
    try:
        first = next(itr)
    except StopIteration:
        return None, None
    return first, itertools.chain([first], itr)
0x5453
  • 12,753
  • 1
  • 32
  • 61