将生成器拆分为多个块,而无需预先遍历


问题内容

(这个问题是关系到这一个这一个,但这些都是预先行走发电机,而这正是我想避免)

我想将生成器拆分为多个块。要求是:

  • 不要填充数据块:如果剩余元素的数量小于数据块大小,则最后一个数据块必须较小。
  • 不要事先遍历生成器:计算元素是昂贵的,并且只能由使用函数来完成,而不是由分块器来完成
  • 这当然意味着:不要在内存中累积(无列表)

我尝试了以下代码:

def head(iterable, max=10):
    for cnt, el in enumerate(iterable):
        yield el
        if cnt >= max:
            break

def chunks(iterable, size=10):
    i = iter(iterable)
    while True:
        yield head(i, size)

# Sample generator: the real data is much more complex, and expensive to compute
els = xrange(7)

for n, chunk in enumerate(chunks(els, 3)):
    for el in chunk:
        print 'Chunk %3d, value %d' % (n, el)

这以某种方式起作用:

Chunk   0, value 0
Chunk   0, value 1
Chunk   0, value 2
Chunk   1, value 3
Chunk   1, value 4
Chunk   1, value 5
Chunk   2, value 6
^CTraceback (most recent call last):
  File "xxxx.py", line 15, in <module>
    for el in chunk:
  File "xxxx.py", line 2, in head
    for cnt, el in enumerate(iterable):
KeyboardInterrupt

Buuuut …它永远不会停止(我必须按下^Cwhile True。每当生成器被耗尽时,我都想停止该循环,但是我不知道如何检测到这种情况。我试图提出一个异常:

class NoMoreData(Exception):
    pass

def head(iterable, max=10):
    for cnt, el in enumerate(iterable):
        yield el
        if cnt >= max:
            break
    if cnt == 0 : raise NoMoreData()

def chunks(iterable, size=10):
    i = iter(iterable)
    while True:
        try:
            yield head(i, size)
        except NoMoreData:
            break

# Sample generator: the real data is much more complex, and expensive to compute    
els = xrange(7)

for n, chunk in enumerate(chunks(els, 2)):
    for el in chunk:
        print 'Chunk %3d, value %d' % (n, el)

但是然后仅在使用者的上下文中引发异常,这不是我想要的(我想保持使用者代码的清洁)

Chunk   0, value 0
Chunk   0, value 1
Chunk   0, value 2
Chunk   1, value 3
Chunk   1, value 4
Chunk   1, value 5
Chunk   2, value 6
Traceback (most recent call last):
  File "xxxx.py", line 22, in <module>
    for el in chunk:
  File "xxxx.py", line 9, in head
    if cnt == 0 : raise NoMoreData
__main__.NoMoreData()

如何在chunks不走动的情况下检测发电机是否在功能中耗尽?


问题答案:

一种方法是先查看第一个元素(如果有),然后创建并返回实际的生成器。

def head(iterable, max=10):
    first = next(iterable)      # raise exception when depleted
    def head_inner():
        yield first             # yield the extracted first element
        for cnt, el in enumerate(iterable):
            yield el
            if cnt + 1 >= max:  # cnt + 1 to include first
                break
    return head_inner()

只需在chunk生成器中使用它,并StopIteration像处理自定义异常一样捕获异常即可。


更新:
这是另一个版本,itertools.islice用于替换大部分head功能和一个for循环。这个简单for的事实,循环做
同样的事情 为笨重的while-try-next-except-break原代码构造,所以结果是 很多 的可读性。

def chunks(iterable, size=10):
    iterator = iter(iterable)
    for first in iterator:    # stops when iterator is depleted
        def chunk():          # construct generator for next chunk
            yield first       # yield element from for loop
            for more in islice(iterator, size - 1):
                yield more    # yield more elements from the iterator
        yield chunk()         # in outer generator, yield next chunk

使用itertools.chain替换内部生成器,我们可以得到比这更短的代码:

def chunks(iterable, size=10):
    iterator = iter(iterable)
    for first in iterator:
        yield chain([first], islice(iterator, size - 1))