我有一个这样的文本文件:
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的回答)。但关键是我希望写一些比我的原始生成器函数(甚至可能是生成器表达式)更简洁。有人能告诉我我做错了什么,以及如何做到对不对?
答案 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
。
您可以将takewhile
与tee
合并,看看下一批中是否还有其他结果:
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']]