如何为Python迭代器编写分页程序?

10

我正在寻找一种“分页”Python迭代器的方法。也就是说,我想用另一个迭代器将给定的迭代器 iterpage_size 包装起来,使得它返回来自迭代器 iter 的项目作为一系列“页面”。每个页面本身都是一个带有最多 page_size 个迭代项的迭代器。

我查看了 itertools , 最接近的东西是 itertools.islice。在某些方面,我想要的正好与itertools.chain相反--我不想将一系列迭代器连接成一个迭代器,而是想将一个迭代器拆分成一系列更小的迭代器。我希望在 itertools 中可以找到一个分页函数,但找不到。

以下是我设计的分页类和演示。

class pager(object):
    """
    takes the iterable iter and page_size to create an iterator that "pages through" iter.  That is, pager returns a series of page iterators,
    each returning up to page_size items from iter.
    """
    def __init__(self,iter, page_size):
        self.iter = iter
        self.page_size = page_size
    def __iter__(self):
        return self
    def next(self):
        # if self.iter has not been exhausted, return the next slice
        # I'm using a technique from 
        # https://dev59.com/x3M_5IYBdhLWcg3wslfs
        # to check for iterator completion by cloning self.iter into 3 copies:
        # 1) self.iter gets advanced to the next page
        # 2) peek is used to check on whether self.iter is done
        # 3) iter_for_return is to create an independent page of the iterator to be used by caller of pager
        self.iter, peek, iter_for_return = itertools.tee(self.iter, 3)
        try:
            next_v = next(peek)
        except StopIteration: # catch the exception and then raise it
            raise StopIteration
        else:
            # consume the page from the iterator so that the next page is up in the next iteration
            # is there a better way to do this?
            # 
            for i in itertools.islice(self.iter,self.page_size): pass
            return itertools.islice(iter_for_return,self.page_size)



iterator_size = 10
page_size = 3

my_pager = pager(xrange(iterator_size),page_size)

# skip a page, then print out rest, and then show the first page
page1 = my_pager.next()

for page in my_pager:
    for i in page:
        print i
    print "----"

print "skipped first page: " , list(page1)   

我正在寻求一些反馈,有以下问题:

  1. 是否已经在 itertools 中有一个我忽略的分页器?
  2. 对 self.iter 进行三次克隆似乎很笨拙。有一个克隆是为了检查 self.iter 是否还有更多项目。我决定采用Alex Martelli建议的技术(知道他写过包装技术)。第二个克隆是为了使返回的页面与内部迭代器(self.iter)独立。是否有方法可以避免克隆3次?
  3. 除了捕获并再次引发它之外,是否有更好的处理 StopIteration 异常的方法?我倾向于根本不捕获它,让它冒泡。

谢谢! -Raymond


1
相关:https://dev59.com/lnVC5IYBdhLWcg3wYQAp https://dev59.com/VHRC5IYBdhLWcg3wCMc6 https://dev59.com/AnM_5IYBdhLWcg3wiDyA https://dev59.com/SHRA5IYBdhLWcg3w_C8w - jfs
6个回答

8

看一下来自itertools recipesgrouper()

from itertools import zip_longest

def grouper(iterable, n, fillvalue=None):
    "Collect data into fixed-length chunks or blocks"
    # grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx"
    args = [iter(iterable)] * n
    return zip_longest(*args, fillvalue=fillvalue)

感谢指出这些配方。我可以看到使用石斑鱼是高效的,并且可以使配方的行为与我的Pager完全相同。我仍然很好奇Pager是否有很大的价值,或者我应该放弃它,采用类似石斑鱼的方法。 - Raymond Yee

4
为什么你不使用这个?
def grouper( page_size, iterable ):
    page= []
    for item in iterable:
        page.append( item )
        if len(page) == page_size:
            yield page
            page= []
    yield page

每个页面本身都是一个迭代器,最多有page_size个项目。每个页面都是一个简单的项目列表,可迭代。您可以使用yield iter(page)来产生迭代器而不是对象,但我不认为这会改善任何东西。它在末尾抛出一个标准的StopIteration。还想要什么?

感谢您回答我的问题并提供了一种很好的思考方式,即如何仅通过迭代器进行循环。我认为有一个小错误--您是否意味着将项目附加到页面上--就像这样:def grouper(page_size,iterable): page= [] for item in iterable: if len(page) == page_size: yield page page= [] else: page.append(item) yield page - Raymond Yee
@raymondyee:实际上,有更好的方法。你的版本存在一个大问题。试一下,你会发现它跳过了一个项目。 - S.Lott
@S.Lott -- 是的,我把我的page.append(item)放错了位置。感谢您的纠正。我仍在学习itertools何时有帮助,何时不需要。有什么指导可以提供吗? - Raymond Yee
@raymondyee:没有建议。我不经常使用iterools。生成器函数非常简单。 - S.Lott

3
我会这样做:
def pager(iterable, page_size):
    args = [iter(iterable)] * page_size
    fillvalue = object()
    for group in izip_longest(fillvalue=fillvalue, *args):
        yield (elem for elem in group if elem is not fillvalue)

这样,None 就可以成为迭代器输出的合法值。只有单个对象 fillvalue 被过滤掉,它不能成为可迭代对象的元素。


谢谢,Matt。你让我意识到我既没有允许None成为迭代器中的合法值,也没有考虑填充值。 - Raymond Yee

0
基于指向itertools配方的grouper()指针,我构想了以下适应Pager的grouper()版本。我希望过滤掉任何None结果,并且希望返回一个迭代器而不是元组(尽管我怀疑这样转换可能没有什么优势)。
# based on http://docs.python.org/library/itertools.html#recipes
def grouper2(n, iterable, fillvalue=None):
    args = [iter(iterable)] * n
    for item in izip_longest(fillvalue=fillvalue, *args):
        yield iter(filter(None,item))

我欢迎反馈意见,关于如何改进这段代码。


0

more_itertools.chunked 将会完全满足你的需求:

>>> import more_itertools
>>> list(chunked([1, 2, 3, 4, 5, 6], 3))
[[1, 2, 3], [4, 5, 6]]

如果您想要进行分块操作而不创建临时列表,可以使用more_itertools.ichunked

该库还有许多其他很好的选项,可用于高效地分组、窗口化、切片等操作。


0
def group_by(iterable, size):
    """Group an iterable into lists that don't exceed the size given.

    >>> group_by([1,2,3,4,5], 2)
    [[1, 2], [3, 4], [5]]

    """
    sublist = []

    for index, item in enumerate(iterable):
        if index > 0 and index % size == 0:
            yield sublist
            sublist = []

        sublist.append(item)

    if sublist:
        yield sublist

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