1. ホーム
  2. python

[解決済み] 文字列の行を反復処理する

2022-06-28 04:43:17

質問

次のような複数行の文字列が定義されています。

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

この文字列は、私が書いているパーサーのテスト入力として使われます。パーサー関数は file -オブジェクトを入力として受け取り、それに対して反復処理を行います。また、この関数は next() メソッドを直接呼び出して行をスキップさせるので、入力として本当に必要なのは反復子ではなくイテレータです。 その文字列の個々の行をイテレートするイテレータが必要なのです。 file -オブジェクトがテキストファイルの行を越えるように。もちろん、私はこのようにすることができます。

lineiterator = iter(foo.splitlines())

これを行うより直接的な方法はありますか?このシナリオでは、文字列は分割のために一度トラバースされ、その後パーサーによって再度トラバースされる必要があります。私のテストケースでは、文字列は非常に短いので、それは重要ではありません、私は単に好奇心で尋ねています。Pythonはそのようなもののために非常に多くの便利で効率的なビルトインを持っていますが、私はこの必要性に合ったものを見つけることができませんでした。

どのように解決するのですか?

以下の3つの可能性があります。

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

これをメインスクリプトとして実行すると、3つの関数が等価であることが確認できます。とは 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 の呼び出しによる私の試みよりも 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 ベースのアプローチよりも優れているわけではありませんが、それでも、小さな1つ違いバグ (私の f3 のような +1 と -1 が現れるループは自動的に off-by-one 疑惑を引き起こすはずで、そのような微調整がなく、それを持つべき多くのループもそうです -- 私は他の関数でその出力を確認できたので私のコードも正しいと信じていますが)。

しかし、分割ベースのアプローチはまだルールです。

余談ですが、おそらく 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 をベースとした、最もシンプルで高速なものについては、かなり理論的なエクササイズになるので、ここで止めておきます。