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.