Python itertools.takewhile在生成器函数中-为什么只对其求值一次?
我有这样一个文本文件:Python itertools.takewhile在生成器函数中-为什么只对其求值一次?,python,generator,itertools,Python,Generator,Itertools,我有这样一个文本文件: 11 2 3 4 11 111 使用Python2.7,我想将其转换为一个行列表,其中换行符分割内部列表中的项目,空行分割外部列表中的项目。像这样: [["11","2","3","4"],["11"],["111"]] 为此,我编写了一个生成器函数,一旦传递了一个打开的文件对象,就会一次生成一个内部列表: def readParag(fileObj): currentParag = [] for line in fileObj:
11
2
3
4
11
111
使用Python2.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!=“\n”,fileObj)
,从而产生另一个列表。但是没有:我得到了一个StopIteration
。因此我推测,take while
表达式在生成器对象创建时只被计算一次,而不是每次调用生成的生成器对象的next
方法时
这个假设使我想知道如果我再次调用生成器函数会发生什么。结果是,它创建了一个新的生成器对象,该对象还生成了一个结果(预期的第二个结果,即[“11”]
),然后向我抛出停止迭代。因此,事实上,将其作为生成器函数有效地给出了相同的结果,就像我将其作为普通函数编写一样,return
ed列表,而不是yield
ing列表
我想我可以通过创建自己的类来解决这个问题,而不是使用生成器(如John Millikin对的回答)。但关键是我希望写一些比我原来的生成器函数(甚至可能是生成器表达式)更简洁的东西。有人能告诉我我做错了什么,以及如何纠正吗?这正是.takewhile()
应该做的。当条件为true时,它将从底层iterable返回元素,一旦条件为false,它将永久性地切换到迭代完成阶段
请注意,这是迭代器的行为方式;提高StopIteration意味着,停止对我进行迭代,我就完成了
从:
表示数据流的对象。重复调用迭代器的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方法也是如此。您所尝试的是一项完美的工作:
这将提供:
>>> 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]
这是takewhile
的记录行为。当条件为真时,它需要时间。如果该条件稍后再次变为真,则不会再次启动
简单的解决方法是让函数在循环中只调用takewhile,在takewhile没有更多返回时停止(即,在文件末尾):
您可以多次呼叫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']]
其他答案很好地解释了这里发生的事情,您需要多次调用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)], [])
如果文件内容适合内存,有一种更简单的方法可以用空行分隔组:
with open("filename") as f:
groups = [group.split() for group in f.read().split("\n\n")]
通过使用re.split()
而不是str.split()
以及过滤掉四个或更多连续换行导致的潜在空组,可以使这种方法更加稳健。。注意,一个可能的修复方法是每次检测到换行时再次调用takewhile()
。谢谢,Martijn-这很有帮助。你知道有没有一个等价的.takewhile
不会永久性地切换到“迭代完成”阶段,这样我就可以让我的一行程序按照我的意愿工作了吗?或者我应该继续使用我原来的生成器函数并感谢它完成了任务吗?使用groupby()
,就像Rik Poggi的答案一样。@JAB:实际上我使用了另一种方法。@JAB Rik Poggi的答案很棒,但除非我遗漏了什么(完全可能!),否则它看起来太复杂,无法作为生成器表达式重新编写,因此,我仍然在想,是否可以使用类似于我的一行程序的方法?使用bool
而不是lambda
,并且生成结果而不是将结果附加到列表中——否则很好!)+1.出于嫉妒。我写了一个版本作为genexp,但没有想到通过剥离行传递groupby,所以我在两个地方使用了.strip()
,我不喜欢它的外观。你赢了这一轮@DSM,您介意发布您提出的生成器表达式以进行比较吗?@RikPoggi:我认为g
适用于list(g)
,并且在一行中的open
函数调用后缺少一个右括号(即:[g代表k,g代表groupby((line.strip())代表line in open(“myfile.txt”)),bool)如果k]
)。否则的话,你就有了答案
from itertools import takewhile
def readParag(fileObj):
cond = lambda line: line != "\n"
return iter(lambda: [ln.rstrip() for ln in takewhile(cond, fileObj)], [])
with open("filename") as f:
groups = [group.split() for group in f.read().split("\n\n")]