Итерации по строкам строки

У меня есть многострочная строка, определенная следующим образом:

foo = """
this is 
a multi-line string.
"""

Эта строка, которую мы использовали в качестве тестового ввода для синтаксического анализатора, который я пишу. Функция-синтаксический анализатор получает объект file -объект в качестве входных данных и выполняет итерацию по нему. Он также вызывает метод next(), чтобы пропустить строки, поэтому мне действительно нужен итератор в качестве входных данных, а не итерируемый. Мне нужен итератор, который выполняет итерации по отдельным строкам этой строки, как file -объект, по строкам текстового файла. Я мог бы, конечно, сделать это вот так:

lineiterator = iter(foo.splitlines())

Есть ли более прямой способ сделать это? В этом сценарии строка должна пройти один раз для разделения, а затем снова парсером. В моем тестовом случае это не имеет значения, так как строка очень короткая, я просто спрашиваю из любопытства. У Python есть так много полезных и эффективных встроенных модулей для таких вещей, но я не мог найти ничего, что подходит для этой потребности.

Ответ 1

Вот три возможности:

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())

Выполнение этого в качестве основного script подтверждает, что три функции эквивалентны. С timeit (и a * 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, реализованы очень быстрыми способами; помещая строки вместе, программируя на очень низком уровне (особенно петлями += очень маленьких частей), может быть довольно медленным.

Изменить: добавлено предложение @Jacob, слегка измененное, чтобы дать те же результаты, что и другие (сохраняются пробелы в строке), т.е.

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 запрещает более ясную и быструю замену цикла while return iter(stri) (часть iter, которая избыточна в современных версиях Python, я считаю, начиная с 2.3 или 2.4, но она также безобидные). Возможно, стоит попробовать, а также:

    return itertools.imap(lambda s: s.strip('\n'), stri)

или их вариации - но я останавливаюсь здесь, так как это довольно теоретическое упражнение на основе strip, простейшего и самого быстрого, одного.

Ответ 2

Я не уверен, что вы подразумеваете под "затем снова парсером". После того, как расщепление было выполнено, нет дальнейшего обхода строки, только обход списка разделенных строк. Вероятно, это будет самый быстрый способ выполнить это, если размер вашей строки не будет абсолютно огромным. Тот факт, что python использует неизменяемые строки, означает, что вы всегда должны создавать новую строку, поэтому это должно быть сделано в какой-то момент.

Если ваша строка очень большая, недостатком является использование памяти: вы будете иметь исходную строку и список разделенных строк в памяти одновременно, удваивая требуемую память. Итераторный подход может сэкономить вам это, строя строку по мере необходимости, хотя она по-прежнему платит штраф за "разделение". Однако, если ваша строка такая большая, вы обычно хотите избежать даже строки unsplit, находящейся в памяти. Было бы лучше просто прочитать строку из файла, которая уже позволяет вам перебирать ее в виде строк.

Однако, если у вас уже есть огромная строка в памяти, один подход будет состоять в использовании StringIO, который представляет собой файловый интерфейс для строки, включая разрешение итерации по строке (внутреннее использование .find для поиска следующей новой строки), Затем вы получаете:

import StringIO
s = StringIO.StringIO(myString)
for line in s:
    do_something_with(line)

Ответ 3

Если я правильно прочитал 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

Ответ 4

Поиск на основе Regex иногда быстрее, чем подход генератора:

RRR = re.compile(r'(.*)\n')
def f4(arg):
    return (i.group(1) for i in RRR.finditer(arg))

Ответ 5

Я полагаю, вы могли бы свернуть самостоятельно:

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

Я не уверен, насколько эффективна эта реализация, но это будет выполняться только по вашей строке один раз.

Mmm, генераторы.

Edit:

Конечно, вы также захотите добавить любые типы действий синтаксического анализа, которые вы хотите предпринять, но это довольно просто.