itertools.takewhile在生成器函数中 - 为什么它只被评估一次?

时间:2012-08-07 19:15:51

标签: python generator itertools

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

11
2
3
4

11

111

使用Python 2.7,我想把它变成一个行列表列表,其中换行符分隔内部列表中的项目,空行划分外部列表中的项目。像这样:

[["11","2","3","4"],["11"],["111"]]

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

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方法会使它在文件的其余部分再次评估takewhile(lambda line: line != "\n", fileObj),从而导致它产生另一个列表。但不是:我得到了StopIteration。所以我猜测take while表达式只在生成生成器对象时被评估过一次,而不是每次都调用生成的生成器对象的next方法。

这个假设让我想知道如果再次调用生成器函数会发生什么。结果是它创建了一个新的生成器对象,在向我抛出["11"]之前也产生了单个结果(预期的第二个结果,即StopIteration)。所以事实上,将其作为生成器函数有效地写出来会产生相同的结果,就好像我将它写成普通函数并return编辑列表而不是yield

我想我可以通过创建自己的类来代替生成器来解决这个问题(如John Millikin对this question的回答)。但关键是我希望写一些我的原始生成器函数(甚至可能是生成器表达式)更简洁。有人能告诉我我做错了什么,以及如何做到对不对?

6 个答案:

答案 0 :(得分:26)

你要做的是groupby的完美工作:

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)

将给出:

>>> list(read_parag('myfile.txt')
[['11', '2', '3', '4'], ['11'], ['111']]

或者在一行中:

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

答案 1 :(得分:7)

其他答案很好地解释了这里发生的事情,你需要多次调用takewhile当前发电机不能做的事情。下面是使用带有sentinel参数的内置iter()函数获得所需行为的相当简洁的方法:

from itertools import takewhile

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

答案 2 :(得分:6)

这正是.takewhile()的行为方式。虽然条件为真,但它将从底层迭代中返回元素,并且只要它为假,它就会 permamently 切换到迭代完成阶段。

请注意,这是迭代器必须表现的方式;提高StopIteration意味着,停止迭代我,我已经完成了。

来自python glossary on "iterator"

  

表示数据流的对象。重复调用迭代器的next()方法返回流中的连续项。当没有更多数据可用时,会引发StopIteration异常。此时,迭代器对象已用完,对next()方法的任何进一步调用只会再次引发StopIteration

您可以将takewhiletee合并,看看下一批中是否还有其他结果:

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方法也是如此。

答案 3 :(得分:2)

如果文件内容适合内存,有一种更简单的方法可以用空行分隔组:

with open("filename") as f:
    groups = [group.split() for group in f.read().split("\n\n")]

使用re.split()代替str.split()并过滤掉四个或更多连续换行产生的潜在空组,可以使这种方法更加健壮。

答案 4 :(得分:1)

这是takewhile的记录行为。当条件为真时,它需要 。如果条件稍后再次变为真,它就不会重新启动。

简单的解决方法是让你的函数在循环中调用takewhile,在takewhile停止时不再返回(即,在文件的末尾):

def readParag(fileObj):
    while True:      
        nextList = [ln.rstrip() for ln in takewhile(lambda line: line != "\n", fileObj)]
        if not nextList:
            break
        yield nextList

答案 5 :(得分:0)

您可以多次拨打takewhile:

>>> def readParagGenerator(fileObj):
...     group = [ln.rstrip() for ln in takewhile(lambda line: line != "\n", fileObj)]
...     while len(group) > 0:
...         yield group
...         group = [ln.rstrip() for ln in takewhile(lambda line: line != "\n", fileObj)]
... 
>>> list(readParagGenerator(StringIO(F)))
[['11', '2', '3', '4'], ['11'], ['111']]