วนซ้ำบนบรรทัดของสตริง


119

ฉันมีสตริงหลายบรรทัดที่กำหนดไว้ดังนี้:

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

สตริงนี้เราใช้เป็นอินพุตทดสอบสำหรับตัวแยกวิเคราะห์ที่ฉันกำลังเขียน parser-function รับ a file-object เป็นอินพุตและวนซ้ำทับมัน นอกจากนี้ยังเรียกnext()วิธีการนี้โดยตรงเพื่อข้ามบรรทัดดังนั้นฉันจึงต้องการตัววนซ้ำเป็นอินพุตไม่ใช่ทำซ้ำได้ ฉันต้องการตัววนซ้ำที่วนซ้ำในแต่ละบรรทัดของสตริงนั้นเช่น - fileวัตถุจะอยู่เหนือบรรทัดของไฟล์ข้อความ แน่นอนฉันสามารถทำได้เช่นนี้:

lineiterator = iter(foo.splitlines())

มีวิธีที่ตรงกว่านี้หรือไม่? ในสถานการณ์สมมตินี้สตริงจะต้องข้ามผ่านหนึ่งครั้งสำหรับการแยกและจากนั้นอีกครั้งโดยตัวแยกวิเคราะห์ มันไม่สำคัญในกรณีทดสอบของฉันเนื่องจากสตริงนั้นสั้นมากฉันแค่ถามด้วยความอยากรู้ Python มีบิวท์อินที่มีประโยชน์และมีประสิทธิภาพมากมายสำหรับสิ่งเหล่านี้ แต่ฉันไม่พบสิ่งใดที่เหมาะสมกับความต้องการนี้


12
คุณรู้ว่าคุณสามารถทำซ้ำได้foo.splitlines()ใช่ไหม
SilentGhost

คุณหมายถึงอะไร "อีกครั้งโดยตัวแยกวิเคราะห์"
danben

4
@SilentGhost: ฉันคิดว่าประเด็นคือการไม่ย้ำสตริงซ้ำสองครั้ง เมื่อมีการทำซ้ำโดยsplitlines()และครั้งที่สองโดยทำซ้ำบนผลลัพธ์ของวิธีนี้
Felix Kling

2
มีสาเหตุเฉพาะหรือไม่ที่ทำให้ Splitlines () ไม่ส่งคืนตัววนซ้ำตามค่าเริ่มต้น? ฉันคิดว่าแนวโน้มโดยทั่วไปจะทำเช่นนั้นสำหรับการวนซ้ำ หรือเป็นจริงเฉพาะสำหรับฟังก์ชันเฉพาะเช่น 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วิธีการตาม - แต่ก็ควรคำนึงถึงเพราะอาจมีแนวโน้มที่จะเกิดข้อบกพร่องเล็ก ๆ น้อย ๆ (ลูปใด ๆ ที่คุณเห็นการเกิดขึ้นของ +1 และ -1 เช่นเดียวกับf3ข้างต้นของฉันควรทำโดยอัตโนมัติ ทำให้เกิดความสงสัยแบบทีละรายการ - และควรมีลูปจำนวนมากที่ขาดการปรับแต่งดังกล่าวและควรมี - แม้ว่าฉันจะเชื่อว่ารหัสของฉันถูกต้องเนื่องจากฉันสามารถตรวจสอบเอาต์พุตด้วยฟังก์ชันอื่น ๆ ได้ ')

แต่แนวทางแบบแยกส่วนยังคงเป็นกฎเกณฑ์

กัน: สไตล์ที่ดีกว่าอาจ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))ค่อนข้างเร็ว เกือบจะเร็วพอ ๆ กับการใช้งานแบบไร้เดียงสา แต่ก็ไม่มาก
Matt Anderson

ขอบคุณสำหรับคำตอบที่ดีนี้ ฉันเดาว่าบทเรียนหลักที่นี่ (เนื่องจากฉันยังใหม่กับ python) คือการใช้timeitนิสัย
Björn Pollex

@Space ใช่เวลาเป็นสิ่งที่ดีทุกครั้งที่คุณสนใจเกี่ยวกับประสิทธิภาพ (อย่าลืมใช้อย่างระมัดระวังเช่นในกรณีนี้ให้ดูบันทึกของฉันเกี่ยวกับการต้องlistโทรเพื่อกำหนดเวลาในส่วนที่เกี่ยวข้องทั้งหมด! -)
Alex Martelli

6
แล้วการใช้หน่วยความจำล่ะ? split()ทำการค้าหน่วยความจำอย่างชัดเจนเพื่อประสิทธิภาพโดยถือสำเนาของส่วนทั้งหมดนอกเหนือจากโครงสร้างของรายการ
ivan_pozdeev

3
ฉันรู้สึกสับสนกับคำพูดของคุณในตอนแรกเนื่องจากคุณระบุผลการจับเวลาในลำดับที่ตรงกันข้ามกับการใช้งานและการกำหนดหมายเลข = P
jamesdlin

53

ฉันไม่แน่ใจว่าคุณหมายถึงอะไร "แล้วอีกครั้งโดยโปรแกรมแยกวิเคราะห์" หลังจากทำการแยกเสร็จแล้วจะไม่มีการข้ามผ่านของสตริงอีกต่อไปมีเพียงการข้ามผ่านของรายการสตริงที่แยกเท่านั้น นี่อาจเป็นวิธีที่เร็วที่สุดในการทำสิ่งนี้ตราบใดที่ขนาดของสตริงของคุณไม่ใหญ่มาก ความจริงที่ว่า python ใช้สตริงที่ไม่เปลี่ยนรูปหมายความว่าคุณต้องสร้างสตริงใหม่อยู่เสมอดังนั้นจึงต้องดำเนินการในบางจุดอยู่ดี

หากสตริงของคุณมีขนาดใหญ่มากข้อเสียคือการใช้หน่วยความจำ: คุณจะมีสตริงดั้งเดิมและรายการสตริงแยกในหน่วยความจำในเวลาเดียวกันทำให้หน่วยความจำที่ต้องการเพิ่มขึ้นเป็นสองเท่า วิธีการวนซ้ำสามารถช่วยคุณประหยัดได้โดยสร้างสตริงได้ตามต้องการแม้ว่าจะยังคงจ่ายค่าปรับ "การแยก" ก็ตาม แต่ถ้าสายของคุณคือการที่มีขนาดใหญ่โดยทั่วไปคุณต้องการหลีกเลี่ยงแม้unsplitสตริงการอยู่ในหน่วยความจำ มันจะดีกว่าแค่อ่านสตริงจากไฟล์ซึ่งช่วยให้คุณวนซ้ำเป็นบรรทัดได้อยู่แล้ว

อย่างไรก็ตามหากคุณมีสตริงขนาดใหญ่ในหน่วยความจำอยู่แล้ววิธีหนึ่งคือการใช้ StringIO ซึ่งนำเสนออินเทอร์เฟซแบบไฟล์ไปยังสตริงรวมถึงการอนุญาตให้ทำซ้ำทีละบรรทัด (ใช้ภายใน. find เพื่อค้นหาบรรทัดใหม่ถัดไป) จากนั้นคุณจะได้รับ:

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

การค้นหาตาม Regex บางครั้งเร็วกว่าวิธีการสร้าง:

RRR = re.compile(r'(.*)\n')
def f4(arg):
    return (i.group(1) for i in RRR.finditer(arg))

2
คำถามนี้เกี่ยวกับสถานการณ์เฉพาะดังนั้นการแสดงเกณฑ์มาตรฐานอย่างง่ายเช่นคำตอบที่ได้คะแนนสูงสุดจะเป็นประโยชน์
Björn Pollex

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 (ตัวอักษร) พวกเขา? หรือนั่นเป็นการทดลองที่ฉันควรทำเอง? ;)
Wayne Werner

โปรดวัดตัวเองเป็นคำแนะนำ - และอย่าลืมลองทั้งเส้นสั้น ๆ เช่นในตัวอย่างของ OP และเส้นยาว! -)
Alex Martelli

สำหรับสตริงสั้น ๆ (<~ 40 ตัวอักษร) + = นั้นเร็วกว่าจริง ๆ แต่พบกรณีที่เลวร้ายที่สุดอย่างรวดเร็ว สำหรับสตริงที่ยาวขึ้น.joinเมธอดดูเหมือนความซับซ้อน O (N) เนื่องจากฉันไม่พบการเปรียบเทียบโดยเฉพาะใน SO ฉันจึงเริ่มคำถาม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.