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


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.