Itertools.takewhile в функции генератора - почему он оценивается только один раз?
У меня есть текстовый файл:
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
.
Я думаю, я мог бы решить эту проблему, создав свой собственный класс вместо генератора (как в ответ Джона Милликина на этот вопрос). Но дело в том, что я надеялся написать что-то более сжатое, чем моя исходная функция генератора (возможно, даже выражение генератора). Может кто-нибудь сказать мне, что я делаю неправильно, и как правильно это сделать?
Ответы
Ответ 1
То, что вы пытаетесь сделать, - отличная работа для 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]
Ответ 2
Другие ответы хорошо объясняют, что здесь происходит, вам нужно называть 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)], [])
Ответ 3
Именно так должно вести себя .takewhile()
. Пока условие истинно, оно вернет элементы из базового итеративного файла, и как только оно станет ложным, он будет автоматически переключиться на этап, выполняемый итерацией.
Обратите внимание, что так итераторы должны вести себя; воскрешение StopIteration означает именно это, перестаньте перебирать меня, я закончил.
Из глоссария python на "итераторе" :
Объект, представляющий поток данных. Повторные вызовы метода итераторов 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, указанного в другом ответе.
Ответ 4
Если содержимое файла вписывается в память, гораздо проще получить группы, разделенные пустыми строками:
with open("filename") as f:
groups = [group.split() for group in f.read().split("\n\n")]
Этот подход можно сделать более надежным, используя re.split()
вместо str.split()
и путем фильтрации потенциальных пустых групп в результате четырех или более последовательных разрывов строк.
Ответ 5
Это документированное поведение 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
Ответ 6
Вы можете вызвать 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']]