【问题标题】:itertools.takewhile within a generator function - why is it evaluated once only?生成器函数中的 itertools.takewhile - 为什么它只评估一次?
【发布时间】:2012-08-04 20:36:01
【问题描述】:

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

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 扔回给我。所以实际上,将其编写为生成器函数可以有效地得到与我将其编写为普通函数并returned 列表而不是yielding 它一样的结果。

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

【问题讨论】:

    标签: python generator itertools


    【解决方案1】:

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

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

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

    【讨论】:

      【解决方案2】:

      对于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]
      

      【讨论】:

      • 使用bool 代替lambdayield 结果而不是将它们附加到列表中——否则很好! =)
      • +1 出于嫉妒。我将它的一个版本编写为 genexp,但没有考虑通过剥离线传递 groupby,所以我在两个地方有.strip(),我不喜欢它的外观。你赢了这一轮!
      • @DSM,您介意发布您想出的生成器表达式进行比较吗?
      • @RikPoggi:我认为g 可以用于list(g),并且在单行中的open 函数调用之后缺少右括号(即:[g for k,g in groupby((line.strip() for line in open("myfile.txt")), bool) if k])。否则,你已经回答了我的问题!谢谢!
      • @Westcroft_to_Apse:您的示例输出显示了列表列表。如果您的实际情况不同(例如:您可能只需要一个耗时的迭代器),请更改您需要的内容。缺少的) 是一个错字,已修复。
      【解决方案3】:

      这正是.takewhile() 的行为方式。当条件为真时,它会从底层的可迭代对象中返回元素,一旦它为假,它永久切换到迭代完成阶段。

      请注意,这是迭代器的行为方式;提高 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 方法也是如此。

      【讨论】:

      • 这个。关于这一点,一个可能的解决方法是每次检测到换行符时再次调用takewhile()
      • 谢谢,Martijn - 这很有帮助。你知道是否有一个等同于.takewhile不会永久切换到“迭代完成”阶段,这样我就可以让我的单线工作如我所愿?还是我应该坚持使用我原来的生成器函数并感谢它完成了工作?
      • 使用groupby(),就像 Rik Poggi 的回答一样。
      • @JAB:实际上,我使用了不同的方法。
      • @JAB Rik Poggi 的回答很棒,但除非我遗漏了什么(完全有可能!)它看起来太复杂而无法重写为生成器表达式,所以我仍然想知道是否像我的单线可以工作吗?
      【解决方案4】:

      其他答案很好地解释了这里发生了什么,您需要多次调用takewhile,而您当前的生成器没有这样做。这是使用带有哨兵参数的内置 iter() 函数获得所需行为的一种相当简洁的方法:

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

      【讨论】:

      • 非常感谢!我选择的答案稍微简洁一些(使用groupby 而不是takewhile),但我很感激您的演示,让takewhile 在这种情况下工作的最佳方法是使用@987654328 @函数而不是生成器。
      【解决方案5】:

      你可以多次调用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']]
      

      【讨论】:

      • while group 也可以。
      【解决方案6】:

      这是takewhile 的记录行为。 while 条件为真。如果以后条件再次变为真,它不会再次启动。

      简单的解决方法是让你的函数只在循环中调用 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
      

      【讨论】:

      • 这是我提问的时候没看懂的!谢谢。
      猜你喜欢
      • 2018-06-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-03-18
      • 1970-01-01
      相关资源
      最近更新 更多