[python] 문자열의 줄을 반복합니다.

다음과 같이 정의 된 여러 줄 문자열이 있습니다.

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

이 문자열은 내가 쓰고있는 파서의 테스트 입력으로 사용했습니다. 파서 함수는 file입력 으로 -object를 수신 하고 반복합니다. 또한 next()줄을 건너 뛰기 위해 메서드를 직접 호출 하므로 반복자가 아닌 입력으로 반복기가 필요합니다. file-object가 텍스트 파일의 줄을 넘기는 것처럼 해당 문자열의 개별 줄을 반복하는 반복기가 필요 합니다. 물론 다음과 같이 할 수 있습니다.

lineiterator = iter(foo.splitlines())

더 직접적인 방법이 있습니까? 이 시나리오에서 문자열은 분할을 위해 한 번 통과 한 다음 파서가 다시 통과해야합니다. 내 테스트 케이스에서는 문제가되지 않습니다. 문자열이 매우 짧기 때문에 호기심에서 묻고 있습니다. 파이썬에는 그러한 것들을위한 유용하고 효율적인 내장 기능이 너무 많지만,이 필요에 맞는 것을 찾을 수 없었습니다.



답변

세 가지 가능성이 있습니다.

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, 순진한 구현이 훨씬 빠르기 때문에 재미도 없습니다. find호출 시도보다 6 배 빠르며 하위 수준 접근 방식보다 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기반 접근 방식 만큼 좋지는 않지만 작은 버그에 덜 취약 할 수 있기 때문에 명심할 가치가 있습니다 ( f3위와 같이 +1과 -1이 발생하는 모든 루프 는 자동으로 한 번에 하나씩 의심을 불러 일으키고, 다른 기능으로 출력을 확인할 수 있었기 때문에 내 코드도 옳다고 생각하지만 그러한 조정이 부족하고 가져야하는 많은 루프가 있어야합니다. ‘).

그러나 분할 기반 접근 방식은 여전히 ​​규칙입니다.

제쳐두고 : 아마도 더 나은 스타일 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기초적이고 가장 간단하고 빠른 것이므로 여기서 멈추고 있습니다 .


답변

“파서에 의해 다시”가 무슨 뜻인지 잘 모르겠습니다. 분할이 완료되면 더 이상 문자열을 순회하지 않고 분할 문자열 목록 만 순회합니다 . 문자열의 크기가 절대적으로 크지 않은 한 이것은 실제로 이것을 수행하는 가장 빠른 방법 일 것입니다. 파이썬이 불변의 문자열을 사용한다는 사실 항상 새로운 문자열을 만들어야 한다는 것을 의미 합니다. 그래서 이것은 어쨌든 어느 시점에서 이루어져야합니다.

문자열이 매우 크면 단점은 메모리 사용량에 있습니다. 원래 문자열과 분할 문자열 목록이 동시에 메모리에 있으므로 필요한 메모리가 두 배가됩니다. 반복자 접근 방식을 사용하면 “분할”패널티를 지불하지만 필요에 따라 문자열을 작성하여이를 절약 할 수 있습니다. 그러나 문자열이 그렇게 크면 일반적으로 분할되지 않은 문자열도 메모리에 포함 되지 않도록해야 합니다. 파일에서 문자열을 읽는 것이 더 낫습니다. 이미 줄로 반복 할 수 있습니다.

그러나 메모리에 이미 큰 문자열이있는 경우 한 가지 방법은 StringIO를 사용하는 것입니다.이 방법은 문자열에 파일과 유사한 인터페이스를 제공하는 것입니다.이 인터페이스는 줄 단위로 반복하는 것을 포함합니다 (내부적으로 .find를 사용하여 다음 줄 바꿈 찾기). 그러면 다음을 얻을 수 있습니다.

import StringIO
s = StringIO.StringIO(myString)
for line in s:
    do_something_with(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

이 구현이 얼마나 효율적인지 잘 모르겠지만 문자열을 한 번만 반복합니다.

음, 발전기.

편집하다:

물론 수행하려는 구문 분석 작업 유형을 추가하고 싶을 수도 있지만 매우 간단합니다.


답변

후행 개행 문자를 포함하여 행을 생성하는 “파일”을 반복 할 수 있습니다. 문자열에서 “가상 파일”을 만들려면 다음을 사용할 수 있습니다 StringIO.

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

for line in io.StringIO(foo):
    print(repr(line))


답변