Итерация по строкам строки
У меня есть многострочная строка, определенная так:
foo = """
this is
a multi-line string.
"""
Эту строку мы использовали в качестве тестового ввода для анализатора, который я пишу. Парсер-функция получает file
-объект в качестве ввода и перебирает его. Это также называет next()
метод, чтобы пропустить строки, поэтому мне действительно нужен итератор, а не итерация. Мне нужен итератор, который перебирает отдельные строки этой строки, как file
-объект был бы над строками текстового файла. Конечно, я мог бы сделать это так:
lineiterator = iter(foo.splitlines())
Есть ли более прямой способ сделать это? В этом сценарии строка должна быть пройдена один раз для разделения, а затем снова парсером. Это не имеет значения в моем тестовом случае, так как строка там очень короткая, я просто спрашиваю из любопытства. Python имеет так много полезных и эффективных встроенных программ для таких вещей, но я не смог найти ничего, что бы соответствовало этой потребности.
5 ответов
Вот три варианта:
foo = """
this is
a multi-line string.
"""
def f1(foo=foo): return iter(foo.splitlines())
def f2(foo=foo):
retval = ''
for char in foo:
retval += char if not char == '\n' else ''
if char == '\n':
yield retval
retval = ''
if retval:
yield retval
def f3(foo=foo):
prevnl = -1
while True:
nextnl = foo.find('\n', prevnl + 1)
if nextnl < 0: break
yield foo[prevnl + 1:nextnl]
prevnl = nextnl
if __name__ == '__main__':
for f in f1, f2, f3:
print list(f())
Запуск этого в качестве основного сценария подтверждает, что три функции эквивалентны. С timeit
(и * 100
за foo
получить существенные строки для более точного измерения):
$ python -mtimeit -s'import asp' 'list(asp.f3())'
1000 loops, best of 3: 370 usec per loop
$ python -mtimeit -s'import asp' 'list(asp.f2())'
1000 loops, best of 3: 1.36 msec per loop
$ python -mtimeit -s'import asp' 'list(asp.f1())'
10000 loops, best of 3: 61.5 usec per loop
Обратите внимание, что нам нужно list()
вызов, чтобы убедиться, что итераторы пройдены, а не только построены.
IOW, наивная реализация намного быстрее, это даже не смешно: в 6 раз быстрее, чем моя попытка find
звонки, что, в свою очередь, в 4 раза быстрее, чем подход нижнего уровня.
Уроки для сохранения: измерение всегда хорошо (но должно быть точным); строковые методы, такие как splitlines
реализованы очень быстрыми способами; соединяя строки, программируя на очень низком уровне (особенно в циклах +=
очень маленькие кусочки) может быть довольно медленным.
Изменить: добавлено предложение @ Джейкоба, слегка измененное, чтобы дать те же результаты, что и другие (конечные пробелы в строке сохраняются), то есть:
from cStringIO import StringIO
def f4(foo=foo):
stri = StringIO(foo)
while True:
nl = stri.readline()
if nl != '':
yield nl.strip('\n')
else:
raise StopIteration
Измерение дает:
$ python -mtimeit -s'import asp' 'list(asp.f4())'
1000 loops, best of 3: 406 usec per loop
не так хорошо, как .find
основанный подход - все же, стоит помнить, потому что он может быть менее подвержен небольшим ошибкам (один цикл, где вы видите вхождения +1 и -1, как мой f3
выше, должно автоматически вызывать подозрения "один за другим" - как и многие циклы, в которых отсутствуют такие настройки и должны иметь их - хотя я считаю, что мой код также прав, так как я смог проверить его вывод с помощью других функций ").
Но основанный на разделении подход все еще правил.
В стороне: возможно, лучший стиль для f4
было бы:
from cStringIO import StringIO
def f4(foo=foo):
stri = StringIO(foo)
while True:
nl = stri.readline()
if nl == '': break
yield nl.strip('\n')
по крайней мере, это немного менее многословно. Нужно лишить трейлинг \n
s, к сожалению, запрещает более четкую и быструю замену while
цикл с return iter(stri)
(iter
часть чего избыточна в современных версиях Python, я считаю, начиная с 2.3 или 2.4, но это также безобидно). Возможно стоит попробовать, также:
return itertools.imap(lambda s: s.strip('\n'), stri)
или их вариации - но я остановлюсь здесь, так как это в значительной степени теоретическое упражнение по strip
основанный, самый простой и быстрый, один.
Я не уверен, что вы подразумеваете под "потом снова под парсером". После того, как разделение выполнено, дальнейший обход строки не выполняется, только обход списка разделенных строк. Вероятно, это будет самый быстрый способ сделать это, если размер вашей строки не очень велик. Тот факт, что python использует неизменяемые строки, означает, что вы всегда должны создавать новую строку, так что в любом случае это должно быть сделано в какой-то момент.
Если ваша строка очень велика, недостаток заключается в использовании памяти: у вас будет оригинальная строка и список разделенных строк в памяти одновременно, что удваивает требуемую память. Итераторский подход может спасти вас, создавая строку по мере необходимости, хотя он все равно платит штраф за "расщепление". Однако, если ваша строка настолько велика, вы, как правило, хотите избегать даже неразделенной строки, находящейся в памяти. Было бы лучше просто прочитать строку из файла, который уже позволяет вам перебирать ее как строки.
Однако, если у вас уже есть огромная строка в памяти, одним из подходов будет использование StringIO, который представляет файловый интерфейс для строки, включая возможность итерации по строке (внутренне используя.find для поиска следующей новой строки). Затем вы получите:
import StringIO
s = StringIO.StringIO(myString)
for line in s:
do_something_with(line)
Вы можете перебирать "файл", который создает строки, включая завершающий символ новой строки. Чтобы сделать "виртуальный файл" из строки, вы можете использоватьStringIO
:
import io # for Py2.7 that would be import cStringIO as io
for line in io.StringIO(foo):
print(repr(line))
Если я читаю Modules/cStringIO.c
правильно, это должно быть довольно эффективно (хотя и несколько многословно):
from cStringIO import StringIO
def iterbuf(buf):
stri = StringIO(buf)
while True:
nl = stri.readline()
if nl != '':
yield nl.strip()
else:
raise StopIteration
Поиск на основе регулярных выражений иногда быстрее, чем генераторный подход:
RRR = re.compile(r'(.*)\n')
def f4(arg):
return (i.group(1) for i in RRR.finditer(arg))
Я полагаю, вы можете свернуть свой собственный:
def parse(string):
retval = ''
for char in string:
retval += char if not char == '\n' else ''
if char == '\n':
yield retval
retval = ''
if retval:
yield retval
Я не уверен, насколько эффективна эта реализация, но она будет перебирать вашу строку только один раз.
Ммм, генераторы.
Редактировать:
Конечно, вы также захотите добавить любой тип парсера, который хотите выполнить, но это довольно просто.