Итерировать по строкам строки

119

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

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

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

lineiterator = iter(foo.splitlines())

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

Бьорн Поллекс
источник
12
вы знаете, что можете перебирать, foo.splitlines()верно?
SilentGhost
Что вы подразумеваете под "снова парсером"?
danben
4
@SilentGhost: я думаю, дело в том, чтобы не повторять строку дважды. Один раз он повторяется splitlines()и второй раз, повторяя результат этого метода.
Felix Kling
2
Есть ли какая-то конкретная причина, по которой splitlines () по умолчанию не возвращает итератор? Я думал, что тенденция заключается в том, чтобы делать это для итераций. Или это верно только для определенных функций, таких как dict.keys ()?
Черно

Ответы:

144

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

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(и 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основанным, самым простым и быстрым.

Алекс Мартелли
источник
Кроме того, (line[:-1] for line in cStringIO.StringIO(foo))это довольно быстро; почти так же быстро, как наивная реализация, но не совсем.
Мэтт Андерсон
Спасибо за отличный ответ. Думаю, главный урок здесь (поскольку я новичок в Python) - это выработать timeitпривычку.
Björn Pollex
@Space, да, время - это хорошо, каждый раз, когда вы заботитесь о производительности (обязательно используйте его осторожно, например, в этом случае см. Мою заметку о необходимости listвызова для фактического времени всех соответствующих частей! -).
Alex Martelli
6
Что с потреблением памяти? split()явно жертвует памятью на производительность, храня копии всех разделов в дополнение к структурам списка.
ivan_pozdeev
3
Поначалу я был очень смущен вашими замечаниями, потому что вы указали результаты по времени в порядке, обратном их реализации и нумерации. = P
jamesdlin 04
53

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

Если ваша строка очень большая, недостаток заключается в использовании памяти: у вас в памяти одновременно будет исходная строка и список разделенных строк, что удвоит требуемую память. Подход с итератором может спасти вас от этого, создавая строку по мере необходимости, хотя он все равно платит штраф за «разбиение». Однако, если ваша строка настолько велика, вы обычно хотите, чтобы даже неразделенная строка находилась в памяти. Лучше было бы просто прочитать строку из файла, который уже позволяет вам перебирать ее как строки.

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

import StringIO
s = StringIO.StringIO(myString)
for line in s:
    do_something_with(line)
Брайан
источник
5
Примечание: для python 3 вы должны использовать ioдля этого пакет, например, используйте io.StringIOвместо StringIO.StringIO. См. Docs.python.org/3/library/io.html
Attila123
Использование StringIO- также хороший способ получить универсальную высокопроизводительную обработку новой строки.
martineau
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
Джейкоб Оскарсон
источник
3

Поиск на основе регулярных выражений иногда быстрее, чем подход генератора:

RRR = re.compile(r'(.*)\n')
def f4(arg):
    return (i.group(1) for i in RRR.finditer(arg))
socketpair
источник
2
Этот вопрос касается конкретного сценария, поэтому было бы полезно показать простой тест, как это сделал лучший ответ.
Björn Pollex
1

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

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

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

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

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

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

Уэйн Вернер
источник
Довольно неэффективен для длинных строк (производительность у этой +=части наихудшая O(N squared), хотя некоторые приемы реализации пытаются ее снизить, когда это возможно).
Alex Martelli
Да, я только недавно узнал об этом. Было бы быстрее добавить к списку символов, а затем '.join (chars) их? Или это эксперимент, который я должен провести сам? ;)
Уэйн Вернер
Пожалуйста, измерьте себя, это поучительно - и обязательно попробуйте как короткие строки, как в примере OP, так и длинные! -)
Alex Martelli
Для коротких строк (<~ 40 символов) + = на самом деле быстрее, но быстро достигает худшего случая. Для более длинных строк .joinметод фактически выглядит как сложность O (N). Поскольку я еще не смог найти конкретное сравнение, сделанное на SO, я начал вопрос stackoverflow.com/questions/3055477/… (на который неожиданно было получено больше ответов, чем только мой собственный!)
Уэйн Вернер
0

Вы можете перебирать «файл», в результате чего получаются строки, включая завершающий символ новой строки. Чтобы создать «виртуальный файл» из строки, вы можете использовать StringIO:

import io  # for Py2.7 that would be import cStringIO as io

for line in io.StringIO(foo):
    print(repr(line))
Томаш Гандор
источник