itertools.takewhile在生成器函数中,为什么只计算一次?

2024-10-01 15:44:32 发布

您现在位置:Python中文网/ 问答频道 /正文

我有一个这样的文本文件:

11
2
3
4

11

111

使用Python2.7,我想把它转换成一个行列表,其中换行符划分内部列表中的项,空行分隔外部列表中的项。是这样的:

^{pr2}$

为此,我编写了一个生成器函数,该函数将在传递打开的文件对象后一次生成一个内部列表:

def readParag(fileObj):
    currentParag = []
    for line in fileObj:
        stripped = line.rstrip()
    if len(stripped) > 0: currentParag.append(stripped)
    elif len(currentParag) > 0:
        yield currentParag
        currentParag = []

这很好,我可以从列表理解中调用它,产生期望的结果。然而,我后来想到,我也许可以使用itertools.takewhile来更简洁地完成同样的事情(为了将生成器函数重写为生成器表达式,但我们暂时不讨论它)。我试过了:

from itertools import takewhile    
def readParag(fileObj):
    yield [ln.rstrip() for ln in takewhile(lambda line: line != "\n", fileObj)]

在这种情况下,生成的生成器只生成一个结果(预期的第一个结果,即["11","2","3","4"])。我希望再次调用它的next方法将导致它再次对文件的其余部分求值{},从而导致它生成另一个列表。但是没有:我得到了一个StopIteration。所以我推测take while表达式只在生成器对象创建时计算一次,而不是每次调用结果生成器对象的next方法。在

这个假设让我想知道如果我再次调用生成器函数会发生什么。结果是它创建了一个新的generator对象,在向我抛出一个StopIteration之前,它还生成了一个单独的结果(预期的第二个结果,即["11"])。所以事实上,把它作为一个生成器函数来写,会得到与我把它写成普通函数一样的结果,并且return而不是yield对列表进行处理。在

我想我可以通过创建自己的类来代替生成器来解决这个问题(如johnmillikin对this question的回答)。但重点是我希望能写出比我原来的生成器函数(甚至可能是生成器表达式)更简洁的东西。有人能告诉我我做错了什么,以及如何改正吗?在


Tags: 文件对象函数in列表for表达式def
3条回答

这正是.takewhile()的行为方式。当条件为真时,它将从底层iterable返回元素,一旦条件为false,它将永远切换到迭代完成阶段。在

注意,这就是迭代器的行为方式;提升StopIteration意味着,停止在我身上迭代,我完成了。在

python glossary on "iterator"

An object representing a stream of data. Repeated calls to the iterator’s next() method return successive items in the stream. When no more data are available a StopIteration exception is raised instead. At this point, the iterator object is exhausted and any further calls to its next() method just raise StopIteration again.

您可以将takewhile与{}结合起来,看看下一批中是否还有更多结果:

import itertools

def readParag(filename):
    with open(filename) as f:
        while True:
            paras = itertools.takewhile(lambda l: l.strip(), f)
            test, paras = itertools.tee(paras)
            test.next()  # raises StopIteration when the file is done
            yield (l.strip() for l in paras)

这就产生了生成器,所以每个生成的项本身就是一个生成器。您确实需要使用这些生成器中的所有元素才能继续工作;对于另一个答案中列出的groupby方法也是如此。在

其他答案很好地解释了这里发生了什么,您需要多次调用takewhile,而您当前的生成器没有这样做。下面是一个相当简洁的方法,可以使用带有sentinel参数的内置^{}函数来获得所需的行为:

from itertools import takewhile

def readParag(fileObj):
    cond = lambda line: line != "\n"
    return iter(lambda: [ln.rstrip() for ln in takewhile(cond, fileObj)], [])

您要做的是^{}的完美工作:

from itertools import groupby

def read_parag(filename):
    with open(filename) as f:
        for k,g in groupby((line.strip() for line in f), bool):
            if k:
                yield list(g)

这将提供:

^{pr2}$

或者在一行中:

[list(g) for k,g in groupby((line.strip() for line in open('myfile.txt')), bool) if k]

相关问题 更多 >

    热门问题