4

I am going through a generator, whats the Pythonic way of determining if the current element is the first or last element of a generator, given that they need special care?

thanks

basically generating tags, so i have items like

<div class="first">1</div>
<div>...</div>
<div class="last">n</div>

so i would like to keep the last item in loop?

Timmy
  • 12,468
  • 20
  • 77
  • 107

7 Answers7

5

Here's an enumerate-like generator that skips ahead one; it returns -1 for the last element.

>>> def annotate(gen):
...     prev_i, prev_val = 0, gen.next()
...     for i, val in enumerate(gen, start=1):
...         yield prev_i, prev_val
...         prev_i, prev_val = i, val
...     yield '-1', prev_val
>>> for i, val in annotate(iter(range(4))):
...     print i, val
... 
0 0
1 1
2 2
-1 3

It can't tell whether the generator passed to it is "fresh" or not, but it still tells you when the end is nigh:

>>> used_iter = iter(range(5))
>>> used_iter.next()
0
>>> for i, val in annotate(used_iter):
...     print i, val
... 
0 1
1 2
2 3
-1 4

Once an iterator is used up, it raises StopIteration as usual.

>>> annotate(used_iter).next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in annotate
StopIteration
senderle
  • 145,869
  • 36
  • 209
  • 233
  • As it is, this gives an error when passed an object without a `next()` method. To avoid that, you could add `gen = iter(gen)` at the beginning of `annotate`. – senderle May 12 '11 at 20:00
3

The way that I do this is similar to some of the other answers here - but I do it this way, as a matter of preference. Maybe it will suit your preferences too.

With my function below, I can write code like this:

values = [10, 11, 12, 13]
for i, val, isfirst, islast in enumerate2(values):
  if isfirst:
    print 'BEGIN...', val
  elif islast:
    print val, '... END'
  else:
    print val

Here is the function definition:

def enumerate2(iterable_):
  it = iter(iterable_)
  try:
    e = it.next()
    isfirst = True
    i = 0
    try:
      while True:
        next_e = it.next()
        yield (i, e, isfirst, False)
        i += 1
        isfirst = False
        e = next_e
    except StopIteration:
      yield (i, e, isfirst, True)
  except StopIteration:
    pass
2

For the first, use a flag to tell whether or not you've processed any. For the last, hold the next value in a variable, and if there are no more then that's the last one.

Ignacio Vazquez-Abrams
  • 776,304
  • 153
  • 1,341
  • 1,358
  • It should be possible to create a function that does this generically. I'm surprised nothing exists in itertools. – Mark Ransom May 12 '11 at 19:33
1

Well, as for the first element:

for n, item in enumerate(generator()):
  if n == 0:
    # item is first
# out of the loop now: item is last
bluepnume
  • 16,460
  • 8
  • 38
  • 48
1

Turn it into a sequence, example:

>>> gen = (x for x in range(5))
>>> L = list(gen)
>>> L[0]
0
>>> L[-1]
4
>>>

If you need to do this during the loop:

>>> gen = (x for x in range(5))
>>> L = list(gen)
>>> for idx, item in enumerate(L):
...    if idx == 0:
...        print(u'{item} is first'.format(item=item))
...    if idx == len(L) - 1:
...        print(u'{item} is last'.format(item=item))
...
0 is first
4 is last
>>>

Clearly, this is not the solution, if you are the one who created the generator, and need it to stay that way (for memory savings), but if you don't care, this is more Pythonic per se than setting flags (which is implicit at best, sine it relies on the last element during iteration persisting), and enumerate won't get you any closer to finding the last element.

orokusaki
  • 55,146
  • 59
  • 179
  • 257
  • thx, i dun just need the first and last element, just that processing them specially – Timmy May 12 '11 at 19:28
  • thx, i knew to do it this way, dont like it, but its probbably best – Timmy May 12 '11 at 19:34
  • You don't have to make it into a list. You can create a generator that [looks ahead](http://stackoverflow.com/questions/5983265/pythonic-way-of-determining-if-the-current-element-is-the-first-or-last-element-o/5983613#5983613). – senderle May 12 '11 at 19:51
  • 1
    @Timmy - I hate to give up points :) but the best answer is @senderle's (sine it does exactly what you want directly to a generator, in a Pythonic way). – orokusaki May 12 '11 at 20:17
1

Of course, it violates all generator's advantages, but if your iterable is not large, you should use:

list(gener)[1:-1]
Roman Bodnarchuk
  • 29,461
  • 12
  • 59
  • 75
1

If you're concerned about potentially large collections built dynamically, so that you don't want to temporarily place it into a single data structure, here's a different way:

FLAGMASK_FIRST = 1
FLAGMASK_LAST = 2

def flag_lastfirst(collection):
    first_flag = FLAGMASK_FIRST
    first = True
    index = 0
    for element in collection:
        if not first:
            yield index, first_flag, current
            index += 1
            first_flag = 0
        current = element
        first = False
    if not first:
        yield index, first_flag | FLAGMASK_LAST, current

l = [1, 2, 3, 4]
for k in flag_lastfirst(l):
    print(k)

The function will produce a sequence of tuples, one for each element from the original collection.

The contents of the tuple:

  • t[0] = 0-based index
  • t[1] = bitwise flags, FLAGMASK_FIRST is present if the element is the first element, FLAGMASK_LAST is present if the element is the last element
  • t[2] = The original element from the original collection

Sample output from the code above:

 +-- 0-based index
 v
(0, 1, 1)
(1, 0, 2)
(2, 0, 3)
(3, 2, 4)
    ^  ^
    |  +-- the element from the original collection
    |
    +-- 1 means first, 2 means last,
        3 means both first and last, 0 is everything else

I'm sure there are nicer ways to build this kinda thing, but this is my contribution anyway.

Lasse V. Karlsen
  • 380,855
  • 102
  • 628
  • 825