Iterasi di atas garis string

119

Saya memiliki string multi-baris yang didefinisikan seperti ini:

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

String ini kami gunakan sebagai input-tes untuk parser yang saya tulis. Parser-function menerima file-object sebagai input dan mengulanginya. Itu juga memanggil next()metode secara langsung untuk melewati baris, jadi saya benar-benar membutuhkan iterator sebagai input, bukan iterable. Saya memerlukan iterator yang mengulangi baris individu dari string itu seperti file-objek akan melewati baris file teks. Saya tentu saja bisa melakukannya seperti ini:

lineiterator = iter(foo.splitlines())

Apakah ada cara yang lebih langsung untuk melakukan ini? Dalam skenario ini, string harus dilintasi sekali untuk pemisahan, dan kemudian lagi oleh parser. Tidak masalah dalam kasus uji saya, karena stringnya sangat pendek di sana, saya hanya bertanya karena ingin tahu. Python memiliki begitu banyak built-in yang berguna dan efisien untuk hal-hal seperti itu, tetapi saya tidak dapat menemukan apa pun yang sesuai dengan kebutuhan ini.

Björn Pollex
sumber
12
Anda sadar bahwa Anda dapat mengulang, foo.splitlines()kan?
SilentGhost
Apa yang Anda maksud dengan "lagi oleh parser"?
danben
4
@SilentGhost: Saya pikir intinya adalah untuk tidak mengulang string dua kali. Setelah diiterasi oleh splitlines()dan kedua kalinya dengan mengulang hasil dari metode ini.
Felix Kling
2
Apakah ada alasan tertentu mengapa garis terpisah () tidak mengembalikan iterator secara default? Saya pikir tren umumnya melakukannya untuk iterable. Atau apakah ini hanya berlaku untuk fungsi tertentu seperti dict.keys ()?
Cerno

Jawaban:

144

Berikut tiga kemungkinan:

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

Menjalankan ini sebagai skrip utama mengonfirmasi bahwa ketiga fungsi tersebut setara. Dengan timeit(dan * 100untuk foomendapatkan string substansial untuk pengukuran yang lebih tepat):

$ 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

Perhatikan bahwa kita membutuhkan list()panggilan untuk memastikan iterator dilintasi, tidak hanya dibuat.

IOW, implementasi naif jauh lebih cepat bahkan tidak lucu: 6 kali lebih cepat daripada upaya saya dengan findpanggilan, yang pada gilirannya 4 kali lebih cepat daripada pendekatan tingkat yang lebih rendah.

Pelajaran yang perlu dipertahankan: pengukuran selalu merupakan hal yang baik (tetapi harus akurat); metode string seperti splitlinesdiimplementasikan dengan cara yang sangat cepat; menempatkan string bersama-sama dengan memprogram pada tingkat yang sangat rendah (khususnya dengan loop dari +=bagian yang sangat kecil) bisa sangat lambat.

Sunting : menambahkan proposal @ Jacob, sedikit dimodifikasi untuk memberikan hasil yang sama dengan yang lain (tanda kosong pada baris disimpan), yaitu:

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

Pengukuran memberi:

$ python -mtimeit -s'import asp' 'list(asp.f4())'
1000 loops, best of 3: 406 usec per loop

tidak sebagus .findpendekatan berbasis - tetap saja, perlu diingat karena mungkin kurang rentan terhadap bug kecil-kecilan (setiap loop di mana Anda melihat kemunculan +1 dan -1, seperti yang saya di f3atas, akan secara otomatis memicu kecurigaan off-by-one - dan seharusnya banyak loop yang tidak memiliki tweak seperti itu dan seharusnya memilikinya - meskipun saya yakin kode saya juga benar karena saya dapat memeriksa outputnya dengan fungsi lain ').

Tetapi pendekatan berbasis terpisah masih berlaku.

Sebuah tambahan: gaya yang mungkin lebih baik untuk f4adalah:

from cStringIO import StringIO

def f4(foo=foo):
    stri = StringIO(foo)
    while True:
        nl = stri.readline()
        if nl == '': break
        yield nl.strip('\n')

setidaknya, itu sedikit kurang bertele-tele. Kebutuhan untuk menghapus jejak \nsayangnya melarang penggantian whileloop yang lebih jelas dan lebih cepat dengan return iter(stri)( iterbagian yang berlebihan dalam versi modern Python, saya percaya sejak 2.3 atau 2.4, tetapi juga tidak berbahaya). Mungkin patut dicoba, juga:

    return itertools.imap(lambda s: s.strip('\n'), stri)

atau variasinya - tetapi saya berhenti di sini karena ini adalah latihan teoretis yang stripberbasis, paling sederhana dan tercepat.

Alex Martelli
sumber
Juga, (line[:-1] for line in cStringIO.StringIO(foo))cukup cepat; hampir secepat penerapan naif, tetapi tidak cukup.
Matt Anderson
Terima kasih atas jawaban yang bagus ini. Saya kira pelajaran utama di sini (karena saya baru mengenal python) adalah membuat timeitkebiasaan.
Björn Pollex
@Space, ya, waktu itu bagus, kapan pun Anda peduli dengan kinerja (pastikan untuk menggunakannya dengan hati-hati, misalnya dalam hal ini lihat catatan saya tentang perlunya listpanggilan untuk benar-benar mengatur waktu semua bagian yang relevan! -).
Alex Martelli
6
Bagaimana dengan konsumsi memori? split()jelas memperdagangkan memori untuk kinerja, memegang salinan dari semua bagian selain struktur daftar.
ivan_pozdeev
3
Saya benar-benar bingung dengan komentar Anda pada awalnya karena Anda mencantumkan hasil pengaturan waktu dalam urutan yang berlawanan dari penerapan dan penomorannya. = P
jamesdlin
53

Saya tidak yakin apa yang Anda maksud dengan "kemudian lagi dengan parser". Setelah pemisahan selesai, tidak ada lagi traversal string , hanya traversal daftar string split. Ini mungkin cara tercepat untuk melakukannya, selama ukuran senar Anda tidak terlalu besar. Fakta bahwa python menggunakan string yang tidak dapat diubah berarti Anda harus selalu membuat string baru, jadi ini harus dilakukan di beberapa titik.

Jika string Anda sangat besar, kerugiannya adalah penggunaan memori: Anda akan memiliki string asli dan daftar string terpisah dalam memori pada saat yang sama, menggandakan memori yang diperlukan. Pendekatan iterator dapat menyelamatkan Anda dari hal ini, membuat string sesuai kebutuhan, meskipun tetap membayar penalti "pemisahan". Namun, jika string Anda sebesar itu, Anda biasanya ingin menghindari bahkan string unsplit berada dalam memori. Akan lebih baik jika Anda membaca string dari file, yang sudah memungkinkan Anda untuk mengulanginya sebagai baris.

Namun jika Anda sudah memiliki string yang sangat besar di memori, salah satu pendekatannya adalah menggunakan StringIO, yang menyajikan antarmuka mirip file ke string, termasuk mengizinkan iterasi berdasarkan baris (secara internal menggunakan .find untuk menemukan baris baru berikutnya). Anda kemudian mendapatkan:

import StringIO
s = StringIO.StringIO(myString)
for line in s:
    do_something_with(line)
Brian
sumber
5
Catatan: untuk python 3 Anda harus menggunakan iopaket untuk ini, misalnya gunakan io.StringIOsebagai pengganti StringIO.StringIO. Lihat docs.python.org/3/library/io.html
Attila123
Menggunakan StringIOjuga merupakan cara yang baik untuk mendapatkan penanganan baris baru universal berkinerja tinggi.
martineau
3

Jika saya membacanya Modules/cStringIO.cdengan benar, ini seharusnya cukup efisien (meskipun agak bertele-tele):

from cStringIO import StringIO

def iterbuf(buf):
    stri = StringIO(buf)
    while True:
        nl = stri.readline()
        if nl != '':
            yield nl.strip()
        else:
            raise StopIteration
Jacob Oscarson
sumber
3

Pencarian berbasis Regex terkadang lebih cepat daripada pendekatan generator:

RRR = re.compile(r'(.*)\n')
def f4(arg):
    return (i.group(1) for i in RRR.finditer(arg))
socketpair
sumber
2
Pertanyaan ini adalah tentang skenario tertentu, jadi akan sangat membantu jika menunjukkan tolok ukur sederhana, seperti jawaban dengan skor tertinggi.
Björn Pollex
1

Saya kira Anda bisa menggulung sendiri:

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

Saya tidak yakin seberapa efisien penerapan ini, tetapi itu hanya akan mengulangi string Anda sekali.

Mmm, generator.

Edit:

Tentu saja Anda juga ingin menambahkan jenis tindakan parsing apa pun yang ingin Anda lakukan, tetapi itu cukup sederhana.

Wayne Werner
sumber
Cukup tidak efisien untuk antrean panjang ( +=bagian memiliki O(N squared)performa terburuk , meskipun beberapa trik penerapan mencoba menurunkannya jika memungkinkan).
Alex Martelli
Ya - Saya baru saja mempelajarinya baru-baru ini. Apakah akan lebih cepat untuk menambahkan ke daftar karakter dan kemudian ".join (karakter) mereka? Ataukah itu eksperimen yang harus saya lakukan sendiri? ;)
Wayne Werner
tolong ukur diri Anda sendiri, itu instruktif - dan pastikan untuk mencoba kedua baris pendek seperti pada contoh OP, dan yang panjang! -)
Alex Martelli
Untuk string pendek (<~ 40 karakter) + = sebenarnya lebih cepat, tetapi mengenai kasus terburuk dengan cepat. Untuk string yang lebih panjang, .joinmetode ini sebenarnya terlihat seperti kompleksitas O (N). Karena saya belum dapat menemukan perbandingan khusus yang dibuat pada SO, saya memulai pertanyaan stackoverflow.com/questions/3055477/… (yang secara mengejutkan menerima lebih banyak jawaban daripada jawaban saya sendiri!)
Wayne Werner
0

Anda dapat mengulang "file", yang menghasilkan baris, termasuk karakter baris baru di belakangnya. Untuk membuat "file virtual" dari string, Anda dapat menggunakan StringIO:

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

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