文字列の行を反復する


119

私はこのように定義された複数行の文字列を持っています:

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

私が書いているパーサーのテスト入力として使用したこの文字列。parser-functionはfile-objectを入力として受け取り、それを反復処理します。また、next()メソッドを直接呼び出して行をスキップするので、反復可能ではなく、入力としてイテレータが本当に必要です。file-objectがテキストファイルの行を繰り返すように、その文字列の個々の行を繰り返すイテレータが必要です。もちろん、次のようにすることもできます。

lineiterator = iter(foo.splitlines())

これを行うより直接的な方法はありますか?このシナリオでは、文字列は分割のために1回、次にパーサーによってもう一度トラバースする必要があります。テストケースでは問題ではありません。文字列が非常に短いため、好奇心から質問しています。Pythonにはそのようなもののための非常に多くの便利で効率的なビルトインがありますが、このニーズに合うものは何も見つかりませんでした。


12
あなたはあなたがfoo.splitlines()正しく反復できることを知っていますか?
SilentGhost

「もう一度パーサー」とはどういう意味ですか?
danben

4
@SilentGhost:ポイントは文字列を2回繰り返さないことだと思います。splitlines()このメソッドの結果を反復することにより、1回および2回目に反復されます。
Felix Kling

2
splitlines()がデフォルトでイテレータを返さない特別な理由はありますか?一般的にはイテラブルに対してそれを行う傾向にあると思いました。それとも、dict.keys()などの特定の関数にのみ当てはまりますか?
セルノ

回答:


144

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ためのfor 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の発生が見られるループは自動的に発生するはずです)が発生しにくいので、覚えておく価値があります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残念ながら、末尾の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))かなり高速です。素朴な実装とほぼ同じ速さですが、完全ではありません。
マットアンダーソン

この素晴らしい答えをありがとう。ここでの主なレッスン(私はpythonを初めて使用timeitするため)は、習慣を使用して行うことだと思います。
ビョルンポレックス

@Space、うん、timeitは良いです。パフォーマンスを気にするときはいつでも(必ずこの例を使用してください。たとえば、この場合、list実際にすべての関連パーツの時間を測定するために呼び出しが必要であるというメモを参照してください!)。
Alex Martelli

6
メモリ消費についてはどうですか?split()リストの構造に加えてすべてのセクションのコピーを保持して、パフォーマンスとメモリを明確に交換します。
ivan_pozdeev 2014

3
タイミングの結果を実装と番号付けの逆の順序でリストしたため、最初はあなたの発言に本当に混乱しました。= P
jamesdlin 2017年

53

あなたが「それから再びパーサーによって」とはどういう意味かわかりません。分割が完了すると、文字列のそれ以上の走査はなくなり、分割された文字列リストの走査のみが行われます。文字列のサイズが絶対に大きくない限り、これはおそらくこれを実現する最も速い方法です。Pythonが不変の文字列を使用するという事実は、常に新しい文字列を作成する必要があることを意味します。そのため、いずれにしてもこれを行う必要があります。

文字列が非常に大きい場合、欠点はメモリ使用量にあります。元の文字列と分割された文字列のリストが同時にメモリにあり、必要なメモリが2倍になります。イテレータアプローチはこれを節約し、必要に応じて文字列を作成しますが、それでも「分割」ペナルティは発生します。ただし、文字列がこれほど大きい場合は、通常、分割されていない文字列でさえメモリに存在しないようにします。ファイルから文字列を読み取るだけの方がよいでしょう。ファイルから文字列を繰り返し処理できます。

ただし、メモリに巨大な文字列が既にある場合、1つのアプローチは、文字列へのファイルのようなインターフェイスを提供するStringIOを使用することです。あなたはそれから得ます:

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ことも、高性能なユニバーサル改行処理を実現するための良い方法です。
martineau

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
この質問は特定のシナリオに関するものであるため、トップスコアの回答が行ったように、単純なベンチマークを示すと役立ちます。
ビョルンポレックス2017年

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

この実装がどれほど効率的であるかはわかりませんが、これは文字列に対して1回だけ繰り返されます。

うーん、発電機。

編集:

もちろん、どのような種類の構文解析アクションを追加することもできますが、それは非常に簡単です。


長い行の+=場合はかなり非効率的です(一部のワーストケースのO(N squared)パフォーマンスがありますが、いくつかの実装トリックは、可能であればそれを下げようとします)。
Alex Martelli

ええ-私は最近それについて学んでいます。charのリストに追加してから '' .join(chars)を実行する方が高速でしょうか?それとも私が自分で行うべき実験ですか?;)
ウェインヴェルナー

参考にしてください。OPの例のように短い行と長い行の両方を試してください!-)
Alex Martelli

短い文字列(<〜40文字)の場合、+ =は実際には高速ですが、最悪の場合はすぐにヒットします。より長い文字列の場合、.joinメソッドは実際にはO(N)の複雑さのように見えます。私は特定の比較はSOまだで作ら見つけることができませんでしたので、私は質問を始めたstackoverflow.com/questions/3055477/...(!意外にもちょうど私自身よりも多くの回答を受けていること)
ウェイン・ヴェルナー

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.