Итерация по строкам строки - PullRequest
102 голосов
/ 16 июня 2010

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

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

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

lineiterator = iter(foo.splitlines())

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

Ответы [ 5 ]

125 голосов
/ 16 июня 2010

Вот три варианта:

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(), чтобы гарантировать, что итераторы пройдены, а не тольковстроенный.

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

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

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

47 голосов
/ 16 июня 2010

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

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

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

import StringIO
s = StringIO.StringIO(myString)
for line in s:
    do_something_with(line)
3 голосов
/ 26 июня 2017

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

RRR = re.compile(r'(.*)\n')
def f4(arg):
    return (i.group(1) for i in RRR.finditer(arg))
3 голосов
/ 16 июня 2010

Если я правильно прочитал 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
1 голос
/ 16 июня 2010

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

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

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

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

Edit:

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...