Ітерація над рядками рядка


119

У мене багаторядковий рядок визначений так:

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

Цей рядок, який ми використовували як тестовий вхід для аналізатора, про який я пишу. Функція парсера отримує file-об'єкт як вхід і повторює його. Він також викликає next()метод безпосередньо для пропуску рядків, тому мені дуже потрібен ітератор як вхідний, а не ітерабельний. Мені потрібен ітератор, який повторює окремі рядки цього рядка, як file-об'єкт, над рядками текстового файлу. Я, звичайно, могла зробити це так:

lineiterator = iter(foo.splitlines())

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


12
ти знаєш, що можеш перебирати foo.splitlines()правильно?
SilentGhost

Що ви маєте на увазі під "знову за допомогою аналізатора"?
danben

4
@SilentGhost: Я думаю, що справа в тому, щоб не повторювати рядок двічі. Один раз його splitlines()повторюють і повторюють шляхом повторення результату цього методу.
Фелікс Клінг

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* 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заснована на найпростішій та найшвидшій.


Також, (line[:-1] for line in cStringIO.StringIO(foo)) досить швидко; майже так само швидко, як і наївна реалізація, але не зовсім.
Метт Андерсон

Дякую за цю чудову відповідь. Я думаю, головний урок тут (так як я новачок у пітоні) - це зробити timeitзвичку.
Бьорн Поллекс

@ Простір, так, timeit хороший, будь-який час, коли ви піклуєтесь про продуктивність (обов'язково використовуйте його обережно, наприклад, у цьому випадку див. Мою записку про необхідність listдзвінка, щоб фактично виконати всі відповідні частини! -).
Алекс Мартеллі

6
А як щодо споживання пам'яті? split()чітко торгує пам'ять для продуктивності, зберігаючи копію всіх розділів на додаток до структур списку.
ivan_pozdeev

3
Спочатку мене дуже збентежили ваші зауваження, оскільки ви перерахували результати часу в протилежному порядку їх впровадження та нумерації. = Р
джемсдлін

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також є хорошим способом отримати високоефективну універсальну обробку нового рядка.
мартіно

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

Пошук на основі Regex іноді швидше, ніж підхід генератора:

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

2
Це питання стосується конкретного сценарію, тому було б корисно показати простий орієнтир, як це було зроблено у відповіді на найкращу оцінку.
Бьорн Поллекс

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

Для коротких рядків (<~ 40 знаків) + = насправді швидше, але найгірший випадок швидко. Для довших рядків .joinметод насправді виглядає як O (N) складність. Оскільки я ще не міг знайти конкретного порівняння, зробленого на SO, я почав запитання stackoverflow.com/questions/3055477/… (на диво отримав більше відповідей, ніж просто мій власний!)
Wayne Werner

0

Ви можете повторювати "файл", який створює рядки, включаючи символ останнього рядка. Щоб зробити "віртуальний файл" з рядка, ви можете використовувати StringIO:

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

for line in io.StringIO(foo):
    print(repr(line))
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.