Lặp lại các dòng của một chuỗi


119

Tôi có một chuỗi nhiều dòng được định nghĩa như sau:

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

Chuỗi này chúng tôi đã sử dụng làm đầu vào thử nghiệm cho trình phân tích cú pháp mà tôi đang viết. Hàm phân tích cú pháp nhận một- fileđối tượng làm đầu vào và lặp lại nó. Nó cũng gọi next()phương thức trực tiếp để bỏ qua các dòng, vì vậy tôi thực sự cần một trình lặp làm đầu vào, không phải một trình lặp. Tôi cần một trình lặp lặp qua các dòng riêng lẻ của chuỗi đó giống như một- fileđối tượng trên các dòng của tệp văn bản. Tất nhiên tôi có thể làm như thế này:

lineiterator = iter(foo.splitlines())

Có cách nào trực tiếp hơn để làm điều này không? Trong trường hợp này, chuỗi phải được duyệt qua một lần cho quá trình phân tách và sau đó được trình phân tích cú pháp duyệt lại một lần nữa. Nó không quan trọng trong trường hợp thử nghiệm của tôi, vì chuỗi ở đó rất ngắn, tôi chỉ hỏi vì tò mò. Python có rất nhiều tích hợp hữu ích và hiệu quả cho những thứ như vậy, nhưng tôi không thể tìm thấy gì phù hợp với nhu cầu này.


12
bạn biết rằng bạn có thể lặp lại foo.splitlines()đúng không?
SilentGhost

Ý bạn là gì khi "lại bởi trình phân tích cú pháp"?
danben

4
@SilentGhost: Tôi nghĩ điểm mấu chốt là không lặp lại chuỗi hai lần. Một lần nó được lặp lại splitlines()và lần thứ hai bằng cách lặp lại kết quả của phương thức này.
Felix Kling

2
Có lý do cụ thể nào tại sao splitlines () không trả về một trình lặp theo mặc định không? Tôi nghĩ rằng xu hướng nói chung là làm điều đó cho các mục nhiều lần. Hay điều này chỉ đúng với các hàm cụ thể như dict.keys ()?
Cerno

Câu trả lời:


144

Đây là ba khả năng:

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

Chạy điều này như là kịch bản chính xác nhận ba chức năng là tương đương. Với timeit(và một * 100cho foođể có được chuỗi đáng kể để đo chính xác hơn):

$ 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

Lưu ý rằng chúng ta cần lệnh list()gọi để đảm bảo các trình vòng lặp được duyệt qua chứ không chỉ được xây dựng.

IOW, việc triển khai ngây thơ nhanh hơn rất nhiều, thậm chí không buồn cười: nhanh hơn 6 lần so với nỗ lực của tôi với findcác cuộc gọi, nhanh hơn 4 lần so với cách tiếp cận cấp thấp hơn.

Bài học cần lưu lại: đo lường luôn là một điều tốt (nhưng phải chính xác); các phương thức chuỗi như splitlinesđược thực hiện theo những cách rất nhanh; việc đặt các chuỗi lại với nhau bằng cách lập trình ở mức rất thấp (đặc biệt là bằng các vòng lặp của +=các phần rất nhỏ) có thể khá chậm.

Chỉnh sửa : đã thêm đề xuất của @ Jacob, được sửa đổi một chút để cho kết quả tương tự như các đề xuất khác (các khoảng trống ở cuối trên một dòng được giữ lại), tức là:

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

Đo lường mang lại:

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

không hoàn toàn tốt như .findcách tiếp cận dựa trên - vẫn cần lưu ý vì nó có thể ít bị lỗi nhỏ hơn (bất kỳ vòng lặp nào mà bạn thấy các lần xuất hiện +1 và -1, như của tôi f3ở trên, sẽ tự động kích hoạt từng sự nghi ngờ - và nhiều vòng lặp thiếu những tinh chỉnh như vậy cũng nên có - mặc dù tôi tin rằng mã của tôi cũng đúng vì tôi có thể kiểm tra đầu ra của nó với các chức năng khác ').

Nhưng cách tiếp cận dựa trên phân tách vẫn còn nguyên tắc.

Một bên: phong cách có thể tốt hơn f4sẽ là:

from cStringIO import StringIO

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

ít nhất, nó ít dài dòng hơn một chút. \nThật không may, nhu cầu loại bỏ dấu vết sẽ cấm việc thay thế whilevòng lặp rõ ràng và nhanh hơn bằng return iter(stri)( iterphần mà nó bị thừa trong các phiên bản Python hiện đại, tôi tin rằng kể từ phiên bản 2.3 hoặc 2.4, nhưng nó cũng vô hại). Cũng có thể đáng thử:

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

hoặc các biến thể của chúng - nhưng tôi đang dừng ở đây vì nó là một bài tập lý thuyết khá nhiều strip, dựa trên một bài tập dựa trên, đơn giản nhất và nhanh nhất.


Ngoài ra, (line[:-1] for line in cStringIO.StringIO(foo))là khá nhanh; gần như nhanh bằng cách thực hiện ngây thơ, nhưng không hoàn toàn.
Matt Anderson

Cảm ơn bạn vì câu trả lời tuyệt vời này. Tôi đoán bài học chính ở đây (vì tôi mới làm quen với python) là tạo timeitthói quen sử dụng .
Björn Pollex

@Space, vâng, thời gian là tốt, bất cứ lúc nào bạn quan tâm đến hiệu suất (hãy sử dụng nó cẩn thận, ví dụ: trong trường hợp này, hãy xem lưu ý của tôi về việc cần listgọi để thực sự đếm thời gian cho tất cả các phần liên quan! -).
Alex Martelli

6
Còn về tiêu thụ bộ nhớ? split()rõ ràng giao dịch bộ nhớ cho hiệu suất, giữ một bản sao của tất cả các phần ngoài cấu trúc của danh sách.
ivan_pozdeev

3
Tôi thực sự bối rối với nhận xét của bạn lúc đầu vì bạn đã liệt kê các kết quả thời gian theo thứ tự ngược lại với việc thực hiện và đánh số của chúng. = P
jamesdlin 4/1017

53

Tôi không chắc ý của bạn khi nói "then again by the parser". Sau khi quá trình tách đã được thực hiện xong, không có sự chuyển tải nào của chuỗi nữa , chỉ có sự chuyển tải của danh sách các chuỗi được phân tách. Đây có lẽ sẽ là cách nhanh nhất để thực hiện điều này, miễn là kích thước chuỗi của bạn không hoàn toàn lớn. Thực tế là python sử dụng chuỗi bất biến có nghĩa là bạn phải luôn tạo một chuỗi mới, vì vậy điều này dù sao cũng phải được thực hiện tại một số thời điểm.

Nếu chuỗi của bạn rất lớn, điều bất lợi là trong việc sử dụng bộ nhớ: bạn sẽ có chuỗi gốc và danh sách các chuỗi được chia nhỏ trong bộ nhớ cùng một lúc, nhân đôi bộ nhớ cần thiết. Phương pháp tiếp cận trình lặp có thể giúp bạn tiết kiệm điều này, xây dựng một chuỗi khi cần thiết, mặc dù nó vẫn trả tiền phạt "chia nhỏ". Tuy nhiên, nếu chuỗi của bạn lớn đến mức đó, bạn thường muốn tránh ngay cả chuỗi không cắm vào bộ nhớ. Sẽ tốt hơn nếu chỉ đọc chuỗi từ một tệp, điều này đã cho phép bạn lặp qua nó dưới dạng dòng.

Tuy nhiên, nếu bạn đã có một chuỗi lớn trong bộ nhớ, một cách tiếp cận sẽ là sử dụng StringIO, trình bày giao diện giống tệp với một chuỗi, bao gồm cho phép lặp lại theo dòng (nội bộ sử dụng .find để tìm dòng mới tiếp theo). Sau đó, bạn nhận được:

import StringIO
s = StringIO.StringIO(myString)
for line in s:
    do_something_with(line)

5
Lưu ý: đối với python 3, bạn phải sử dụng iogói cho việc này, ví dụ: sử dụng io.StringIOthay vì StringIO.StringIO. Xem docs.python.org/3/library/io.html
Attila123

Sử dụng StringIOcũng là một cách tốt để xử lý dòng mới phổ quát hiệu suất cao.
martineau

3

Nếu tôi đọc Modules/cStringIO.cđúng, điều này sẽ khá hiệu quả (mặc dù hơi dài dòng):

from cStringIO import StringIO

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

3

Tìm kiếm dựa trên regex đôi khi nhanh hơn so với cách tiếp cận của trình tạo:

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

2
Câu hỏi này là về một tình huống cụ thể, vì vậy sẽ hữu ích nếu hiển thị một điểm chuẩn đơn giản, giống như câu trả lời cho điểm cao nhất đã làm.
Björn Pollex

1

Tôi cho rằng bạn có thể tự cuộn:

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

Tôi không chắc việc triển khai này hiệu quả như thế nào, nhưng điều đó sẽ chỉ lặp lại qua chuỗi của bạn một lần.

Mmm, máy phát điện.

Biên tập:

Tất nhiên bạn cũng sẽ muốn thêm bất kỳ loại hành động phân tích cú pháp nào mà bạn muốn thực hiện, nhưng điều đó khá đơn giản.


Khá kém hiệu quả đối với các dòng dài ( +=phần có O(N squared)hiệu suất trong trường hợp xấu nhất , mặc dù một số thủ thuật triển khai cố gắng giảm mức đó khi khả thi).
Alex Martelli

Yeah - Tôi vừa mới học về điều đó gần đây. Sẽ nhanh hơn nếu thêm vào một danh sách các ký tự và sau đó '' .join (ký tự) chúng? Hay đó là một thử nghiệm tôi nên tự thực hiện? ;)
Wayne Werner

xin đừng đo lường chính mình, đó là bài học - và chắc chắn để thử cả hai dòng ngắn như trong ví dụ của OP, và những người lâu -!)
Alex Martelli

Đối với các chuỗi ngắn (<~ 40 ký tự), + = thực sự nhanh hơn, nhưng nhanh chóng chạm vào trường hợp xấu nhất. Đối với các chuỗi dài hơn, .joinphương pháp thực sự trông giống như độ phức tạp O (N). Vì tôi không thể tìm thấy sự so sánh đặc biệt được thực hiện trên SO nêu ra, tôi bắt đầu một câu hỏi stackoverflow.com/questions/3055477/... (mà đáng ngạc nhiên nhận được nhiều câu trả lời hơn là chỉ riêng tôi!)
Wayne Werner

0

Bạn có thể lặp lại "một tệp", tệp này tạo ra các dòng, bao gồm cả ký tự dòng mới ở cuối. Để tạo một "tệp ảo" từ một chuỗi, bạn có thể sử dụng StringIO:

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

for line in io.StringIO(foo):
    print(repr(line))
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.