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

У меня есть многострочная строка, определенная так:

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

по крайней мере, это немного менее многословно. Нужно лишить трейлинг \ns, к сожалению, запрещает более четкую и быструю замену 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

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

Ммм, генераторы.

Редактировать:

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

Другие вопросы по тегам