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


119

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

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

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

lineiterator = iter(foo.splitlines())

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


12
반복 할 수 있다는 것을 알고 foo.splitlines()있습니까?
SilentGhost 2010-06-16

"파서에 의해 다시"란 무엇을 의미합니까?
danben 2010-06-16

4
@SilentGhost : 핵심은 문자열을 두 번 반복하지 않는 것입니다. 한 번 반복되고이 splitlines()메서드의 결과를 반복하여 두 번째로 반복됩니다.
Felix Kling

2
splitlines ()가 기본적으로 반복자를 반환하지 않는 특별한 이유가 있습니까? 나는 경향이 일반적으로 iterables에 대해 그렇게하는 것이라고 생각했습니다. 아니면 dict.keys ()와 같은 특정 함수에만 해당됩니까?
Cerno

답변:


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, 순진한 구현이 훨씬 빠르기 때문에 재미도 없습니다. 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기초적이고 가장 간단하고 빠른 것이므로 여기서 멈추고 있습니다 .


또한 (line[:-1] for line in cStringIO.StringIO(foo))꽤 빠릅니다. 순진한 구현만큼 빠르지 만 그렇지는 않습니다.
맷 앤더슨

이 훌륭한 답변에 감사드립니다. 여기서 주된 교훈은 (파이썬을 처음 접했기 때문에) timeit습관 을 사용 하는 것입니다.
비욘 폴 렉스

@Space, yep, time 그것은 당신이 성능에 관심을 가질 때마다 좋습니다. (주의 깊게 사용하십시오. 예를 들어,이 경우에는 list 모든 관련 부분을 실제로 시간을 전화가 !-).
Alex Martelli

6
메모리 소비는 어떻습니까? split()성능을 위해 메모리를 명확하게 교환하고 목록의 구조 외에도 모든 섹션의 사본을 보유합니다.
ivan_pozdeev

3
타이밍 결과를 구현 및 번호 매기기의 반대 순서로 나열했기 때문에 처음에는 귀하의 발언에 정말 혼란 스러웠습니다. = P
jamesdlin

53

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

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

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

import StringIO
s = StringIO.StringIO(myString)
for line in s:
    do_something_with(line)

5
참고 : 파이썬 3 당신은 사용해야합니다 io예를 들어,이에 대한 패키지를 사용하는 io.StringIO대신 StringIO.StringIO. 참조 docs.python.org/3/library/io.html
Attila123

를 사용하면 StringIO고성능 범용 개행 문자 처리를 얻을 수있는 좋은 방법이기도합니다.
martineau 19.11.21

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

2
이 질문은 특정 시나리오에 관한 것이므로 최고 점수 답변과 같은 간단한 벤치 마크를 표시하는 것이 도움이됩니다.
Björn Pollex 2017-06-26

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)하는 것이 더 빠를까요? 아니면 제가 직접 수행해야하는 실험입니까? ;)
Wayne Werner

그것이 교훈의 자신을 측정 마십시오 -과 영업 이익의 예에서와 같이 모두 짧은 라인을 시도해야하고, 긴 것 -!)
알렉스 마르 텔리

짧은 문자열 (<~ 40 자)의 경우 + =가 실제로 더 빠르지 만 최악의 경우 빨리 맞습니다. 긴 문자열의 경우 .join메서드는 실제로 O (N) 복잡성처럼 보입니다. 아직 구체적인 비교를 찾을 수 없었기 때문에 질문을 시작했습니다. 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))
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.