判断生成器中当前元素是否为第一个或最后一个的Pythonic方法是什么?

4

我正在使用一个生成器,有没有Pythonic的方法来确定当前元素是否是生成器的第一个或最后一个元素,因为它们需要特殊处理?

谢谢。

基本上是生成标签,所以我有像这样的项目

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

所以我想在循环中保留最后一个项目?

2
当你说它们需要特别关注时,你的意思是什么? - bluepnume
@bluepnume他想在它们上面运行一个函数或进程,我猜。 - Maxime Lorant
7个回答

5
这是一个类似枚举的生成器,可以跳过一个元素;它会在最后一个元素返回-1。
>>> 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

它无法判断传入的生成器是否“新鲜”,但仍会告诉您何时接近结尾:
>>> 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

一旦迭代器被使用完,它会像平常一样引发 StopIteration 异常。

>>> annotate(used_iter).next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in annotate
StopIteration

现在这个代码如果传入一个没有 next() 方法的对象会报错。为了避免这种情况,你可以在 annotate 函数的开头添加 gen = iter(gen) - senderle

3
我用的方法与其他答案很相似,但是这是我的偏好。也许它也适合您的喜好。
使用下面的函数,我可以编写如下代码:
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

以下是函数定义:

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

首先,使用一个标志来告诉你是否已经处理了任何内容。最后,将下一个值保存在变量中,如果没有更多值,则该值为最后一个。


应该可以创建一个通用的函数来实现这个功能。我很惊讶在itertools中没有类似的东西。 - Mark Ransom

1

好的,关于第一个元素:

for n, item in enumerate(generator()):
  if n == 0:
    # item is first
# out of the loop now: item is last

循环中最后一项没有办法处理吗? - Timmy
即使是生成器,直到为时已晚也可能不知道。 - Lasse V. Karlsen
@Timmy:知道你需要这个信息的原因会非常有帮助,也许在循环中不需要检查最后一个项目就可以实现你想要的功能。 - bluepnume

1

将其转换为序列,例如:

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

如果你需要在循环期间执行此操作:

>>> 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
>>>

显然,这不是解决方案,如果是创建生成器的人,并且需要它保持原样(为了节省内存),但如果你不在意,这比设置标志更符合Pythonic本质(最好是隐式的,因为它依赖于迭代期间最后一个元素的持久性),而enumerate不会让你更接近找到最后一个元素。


谢谢,我不仅需要第一个和最后一个元素,而是需要对它们进行特殊处理。 - Timmy
谢谢,我知道可以这样做,但不喜欢,但可能是最好的选择。 - Timmy
你不必将其转换为列表。你可以创建一个向前查看的生成器。 - senderle
1
@Timmy - 我不想放弃分数 :) 但最佳答案是 @senderle 的(因为它直接针对生成器做了你想要的事情,以一种Pythonic的方式)。 - orokusaki

1

当然,这违反了所有生成器的优点,但如果您的可迭代对象不是很大,您应该使用:

list(gener)[1:-1]

1

如果您担心可能会构建动态大型集合,因此不想将其暂时放入单个数据结构中,则可以尝试另一种方式:

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)

该函数将为原始集合中的每个元素生成一个元组序列。

元组的内容:

  • t[0] = 基于0的索引
  • t[1] = 位标志,如果元素是第一个元素,则存在FLAGMASK_FIRST,如果元素是最后一个元素,则存在FLAGMASK_LAST
  • t[2] = 原始集合中的原始元素

上述代码的示例输出:

 +-- 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

我相信有更好的方法来构建这种东西,但这是我的贡献。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接