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.
foo.splitlines()
prawda?splitlines()
i drugi raz przez iterację po wyniku tej metody.Odpowiedzi:
Oto trzy możliwości:
Uruchomienie tego jako głównego skryptu potwierdza, że te trzy funkcje są równoważne. Z
timeit
(a* 100
dlafoo
uzyskać znaczne ciągi dla bardziej precyzyjnego pomiaru):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
find
połą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,
splitlines
są 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 .:
Pomiar daje:
nie tak dobre, jak
.find
podejś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 jakf3
powyż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
f4
byłoby:przynajmniej jest trochę mniej rozwlekły. Konieczność
\n
usunięcia końcowych s niestety zabrania jaśniejszej i szybszej zamianywhile
pętli nareturn iter(stri)
(taiter
część 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:lub ich odmiany - ale zatrzymuję się tutaj, ponieważ jest to właściwie ćwiczenie teoretyczne
strip
oparte na podstawowym, najprostszym i najszybszym.źródło
(line[:-1] for line in cStringIO.StringIO(foo))
jest dość szybki; prawie tak szybko, jak naiwne wdrożenie, ale nie do końca.timeit
nawyku.list
wezwania, aby faktycznie zmierzyć czas wszystkich istotnych części! -).split()
wyraźnie zamienia pamięć na wydajność, przechowując kopię wszystkich sekcji oprócz struktury listy.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:
źródło
io
pakietu, np. Użyjio.StringIO
zamiastStringIO.StringIO
. Zobacz docs.python.org/3/library/io.htmlStringIO
jest również dobrym sposobem na uzyskanie wysokiej wydajności, uniwersalnej obsługi nowej linii.Jeśli dobrze przeczytałem
Modules/cStringIO.c
, powinno to być dość wydajne (choć nieco rozwlekłe):źródło
Wyszukiwanie oparte na regex jest czasami szybsze niż podejście generatora:
źródło
Przypuszczam, że możesz toczyć własne:
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.
źródło
+=
część ma najgorsząO(N squared)
wydajność, chociaż kilka sztuczek implementacyjnych próbuje ją obniżyć, gdy jest to możliwe)..join
metoda 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!)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
:źródło