Iteruj po liniach łańcucha


119

Mam ciąg wieloliniowy zdefiniowany w ten sposób:

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

Ten ciąg użyliśmy jako wejście testowe dla parsera, który piszę. Funkcja parsera otrzymuje file-obiekt jako dane wejściowe i wykonuje iterację po nim. Wywołuje również next()metodę bezpośrednio, aby pominąć wiersze, więc naprawdę potrzebuję iteratora jako wejścia, a nie iteracji. Potrzebuję iteratora, który iteruje po poszczególnych wierszach tego ciągu, tak jak file-object po wierszach pliku tekstowego. Mógłbym oczywiście zrobić to tak:

lineiterator = iter(foo.splitlines())

Czy można to zrobić w bardziej bezpośredni sposób? W tym scenariuszu ciąg musi przejść raz w celu podziału, a następnie ponownie przez parser. W moim przypadku testowym nie ma to znaczenia, ponieważ sznurek jest tam bardzo krótki, pytam tylko z ciekawości. Python ma tak wiele przydatnych i wydajnych wbudowanych funkcji do takich rzeczy, ale nie mogłem znaleźć niczego, co by odpowiadało tej potrzebie.


12
jesteś świadomy, że możesz iterować, foo.splitlines()prawda?
— SilentGhost

Co masz na myśli, mówiąc „ponownie przez parser”?
— danben

4
@SilentGhost: Myślę, że chodzi o to, aby nie powtarzać ciągu dwukrotnie. Raz jest iterowany splitlines()i drugi raz przez iterację po wyniku tej metody.
— Felix Kling

2
Czy jest jakiś szczególny powód, dla którego splitlines () domyślnie nie zwraca iteratora? Myślałem, że trend polega na tym, aby ogólnie robić to w przypadku iterowalnych. A może dotyczy to tylko określonych funkcji, takich jak dict.keys ()?
— Cerno

Odpowiedzi:


144

Oto trzy możliwości:

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

Uruchomienie tego jako głównego skryptu potwierdza, że ​​te trzy funkcje są równoważne. Z timeit(a * 100dla foouzyskać znaczne ciągi dla bardziej precyzyjnego pomiaru):

$ 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

Zauważ, że potrzebujemy list()wywołania, aby upewnić się, że iteratory są przetwarzane, a nie tylko budowane.

IOW, naiwna implementacja jest o wiele szybsza, nawet nie jest zabawna: 6 razy szybsza niż moja próba z findpołączeniami, która z kolei jest 4 razy szybsza niż podejście niższego poziomu.

Lekcje do zapamiętania: pomiar jest zawsze dobry (ale musi być dokładny); metody łańcuchowe, takie jak, splitlinessą implementowane bardzo szybko; składanie łańcuchów razem przez programowanie na bardzo niskim poziomie (szczególnie przez pętle +=bardzo małych elementów) może być dość powolne.

Edycja : dodano propozycję @ Jacoba, nieznacznie zmodyfikowaną, aby uzyskać takie same wyniki, jak pozostałe (zachowane są końcowe spacje w linii), tj .:

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

Pomiar daje:

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

nie tak dobre, jak .findpodejście oparte - nadal warto o tym pamiętać, ponieważ może być mniej podatne na małe błędy, które nie są po jednym (każda pętla, w której widzisz wystąpienia +1 i -1, tak jak f3powyżej, powinna automatycznie wywołują podejrzenia - podobnie jak wiele pętli, które nie mają takich poprawek i powinny je mieć - chociaż uważam, że mój kod jest również poprawny, ponieważ mogłem sprawdzić jego wyjście za pomocą innych funkcji '').

Jednak podejście oparte na podziale nadal obowiązuje.

Na marginesie: prawdopodobnie lepszym stylem f4byłoby:

from cStringIO import StringIO

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

przynajmniej jest trochę mniej rozwlekły. Konieczność \nusunięcia końcowych s niestety zabrania jaśniejszej i szybszej zamiany whilepętli na return iter(stri)(ta iterczęść jest zbędna we współczesnych wersjach Pythona, myślę, że od 2.3 lub 2.4, ale jest również nieszkodliwa). Może warto spróbować, także:

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

lub ich odmiany - ale zatrzymuję się tutaj, ponieważ jest to właściwie ćwiczenie teoretyczne stripoparte na podstawowym, najprostszym i najszybszym.


Ponadto (line[:-1] for line in cStringIO.StringIO(foo))jest dość szybki; prawie tak szybko, jak naiwne wdrożenie, ale nie do końca.
— Matt Anderson

Dziękuję za tę świetną odpowiedź. Myślę, że główną lekcją tutaj (ponieważ jestem nowy w Pythonie) jest zrobienie timeitnawyku.
— Björn Pollex

@Space, tak, czas jest dobry, za każdym razem, gdy zależy Ci na wydajności (pamiętaj, aby używać go ostrożnie, np. W tym przypadku zobacz moją notatkę o konieczności listwezwania, aby faktycznie zmierzyć czas wszystkich istotnych części! -).
— Alex Martelli

6
A co z zużyciem pamięci? split()wyraźnie zamienia pamięć na wydajność, przechowując kopię wszystkich sekcji oprócz struktury listy.
— ivan_pozdeev

3
Na początku byłem naprawdę zdezorientowany Twoimi uwagami, ponieważ wymieniłeś wyniki synchronizacji w kolejności odwrotnej do ich implementacji i numeracji. = P
— jamesdlin

53

Nie jestem pewien, co masz na myśli, mówiąc „potem znowu przez parser”. Po dokonaniu podziału nie ma dalszego przechodzenia przez ciąg , a jedynie przechodzenie przez listę podzielonych ciągów. Prawdopodobnie będzie to najszybszy sposób na osiągnięcie tego celu, o ile rozmiar twojego sznurka nie jest absolutnie duży. Fakt, że Python używa niezmiennych ciągów oznacza, że zawsze musisz utworzyć nowy ciąg, więc i tak trzeba to zrobić w pewnym momencie.

Jeśli twój łańcuch jest bardzo duży, wadą jest użycie pamięci: będziesz mieć oryginalny ciąg i listę podzielonych ciągów w pamięci w tym samym czasie, podwajając wymaganą pamięć. Podejście iteracyjne może zaoszczędzić ci tego, budując ciąg w razie potrzeby, chociaż nadal płaci karę za „dzielenie”. Jednakże, jeśli twój ciąg jest tak duży, generalnie chcesz uniknąć nawet niepodzielonego ciągu w pamięci. Byłoby lepiej po prostu odczytać ciąg z pliku, który już pozwala na iterację po nim jako linie.

Jeśli jednak masz już w pamięci ogromny ciąg, jednym podejściem byłoby użycie StringIO, które przedstawia interfejs podobny do pliku dla ciągu, w tym umożliwia iterację po linii (wewnętrznie używając .find do znalezienia następnej nowej linii). Otrzymasz wtedy:

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

5
Uwaga: w przypadku Pythona 3 musisz do tego użyć iopakietu, np. Użyj io.StringIOzamiast StringIO.StringIO. Zobacz docs.python.org/3/library/io.html
— Attila123,

Używanie StringIOjest również dobrym sposobem na uzyskanie wysokiej wydajności, uniwersalnej obsługi nowej linii.
— martineau

3

Jeśli dobrze przeczytałem Modules/cStringIO.c, powinno to być dość wydajne (choć nieco rozwlekłe):

from cStringIO import StringIO

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

3

Wyszukiwanie oparte na regex jest czasami szybsze niż podejście generatora:

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

2
To pytanie dotyczy konkretnego scenariusza, dlatego pomocne byłoby przedstawienie prostego testu porównawczego, takiego jak odpowiedź, która uzyskała najwyższy wynik.
— Björn Pollex

1

Przypuszczam, że możesz toczyć własne:

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

Nie jestem pewien, jak wydajna jest ta implementacja, ale spowoduje to powtórzenie ciągu tylko raz.

Mmm, generatory.

Edytować:

Oczywiście będziesz chciał również dodać dowolne typy analizowania, które chcesz wykonać, ale to całkiem proste.


Dość nieefektywne w przypadku długich linii ( +=część ma najgorszą O(N squared)wydajność, chociaż kilka sztuczek implementacyjnych próbuje ją obniżyć, gdy jest to możliwe).
— Alex Martelli

Tak - niedawno się o tym dowiedziałem. Czy byłoby szybciej dołączyć do listy znaków, a następnie „.join (znaki) je”? A może jest to eksperyment, który sam powinienem wykonać? ;)
— Wayne Werner

proszę się zmierzyć, to pouczające - i koniecznie wypróbuj obie krótkie linie, jak na przykładzie OP, i długie! -)
— Alex Martelli

W przypadku krótkich ciągów znaków (<~ 40 znaków) + = jest faktycznie szybsze, ale szybko trafia w najgorszy przypadek. W przypadku dłuższych łańcuchów .joinmetoda faktycznie wygląda jak złożoność O (N). Ponieważ nie mogłem jeszcze znaleźć konkretnego porównania dokonanego na SO, zacząłem pytanie stackoverflow.com/questions/3055477/… (które zaskakująco otrzymało więcej odpowiedzi niż tylko moje własne!)
— Wayne Werner

0

Możesz iterować po „pliku”, co powoduje powstanie wierszy, łącznie z końcowym znakiem nowej linii. Aby utworzyć „plik wirtualny” z ciągu znaków, możesz użyć StringIO:

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

for line in io.StringIO(foo):
    print(repr(line))
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.