10

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

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方法。

这个假设让我想知道如果我再次调用生成器函数会发生什么。结果是它创建了一个新的生成器对象,该对象也产生了一个结果(预期的第二个结果,即 ie ["11"]),然后向我抛出了一个StopIteration回击。所以事实上,把它写成一个生成器函数可以有效地给出相同的结果,就好像我把它写成一个普通函数并returned 列表而不是yielding 它一样。

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

4

6 回答 6

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]
于 2012-08-07T19:24:56.743 回答
7

其他答案很好地解释了这里发生的事情,您需要takewhile多次调用当前生成器不执行的操作。iter()这是使用带有 sentinel 参数的内置函数获得所需行为的一种相当简洁的方法:

from itertools import takewhile

def readParag(fileObj):
    cond = lambda line: line != "\n"
    return iter(lambda: [ln.rstrip() for ln in takewhile(cond, fileObj)], [])
于 2012-08-07T19:35:57.387 回答
6

这正是.takewhile()应该如何表现。当条件为真时,它会从底层迭代返回元素,一旦它为假,它就会永久地切换到迭代完成阶段。

请注意,这是迭代器的行为方式;提高 StopIteration 意味着,停止迭代我,我完成了。

来自“迭代器”的python词汇表

表示数据流的对象。重复调用迭代器的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 方法也是如此。

于 2012-08-07T19:21:22.153 回答
2

If the file contents fit into memory, there is a much easier way to get the groups separated by blank lines:

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

This approach can be made more robust by using re.split() instead of str.split() and by filtering out potential empty groups resulting from four or more consecutive line breaks.

于 2012-08-09T15:58:48.787 回答
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
于 2012-08-07T19:28:44.963 回答
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']]
于 2012-08-07T19:31:07.780 回答