Itera sulle righe di una stringa


119

Ho una stringa multilinea definita in questo modo:

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

Questa stringa è stata utilizzata come input di test per un parser che sto scrivendo. La funzione parser riceve un oggetto filecome input e scorre su di esso. Chiama anche il next()metodo direttamente per saltare le righe, quindi ho davvero bisogno di un iteratore come input, non di un iterabile. Ho bisogno di un iteratore che itera sulle singole righe di quella stringa come filefarebbe un oggetto sulle righe di un file di testo. Ovviamente potrei farlo in questo modo:

lineiterator = iter(foo.splitlines())

C'è un modo più diretto per farlo? In questo scenario la stringa deve essere attraversata una volta per la suddivisione e poi di nuovo dal parser. Non importa nel mio caso di prova, poiché la stringa è molto corta lì, lo chiedo solo per curiosità. Python ha così tanti integrati utili ed efficienti per queste cose, ma non sono riuscito a trovare nulla che si adatti a questa esigenza.


12
sei consapevole che puoi iterare, foo.splitlines()giusto?
SilentGhost

Cosa intendi con "di nuovo dal parser"?
danben

4
@ SilentGhost: Penso che il punto sia non iterare la stringa due volte. Una volta iterato da splitlines()e una seconda volta ripetendo il risultato di questo metodo.
Felix Kling,

2
C'è un motivo particolare per cui splitlines () non restituisce un iteratore per impostazione predefinita? Pensavo che la tendenza fosse quella di farlo generalmente per gli iterabili. O è vero solo per funzioni specifiche come dict.keys ()?
Cerno

Risposte:


144

Ecco tre possibilità:

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

L'esecuzione di questo come lo script principale conferma che le tre funzioni sono equivalenti. Con timeit(e un * 100per fooottenere stringhe sostanziali per una misurazione più precisa):

$ 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

Nota che abbiamo bisogno della list()chiamata per garantire che gli iteratori siano attraversati, non solo compilati.

IOW, l'implementazione ingenua è molto più veloce che non è nemmeno divertente: 6 volte più veloce del mio tentativo con le findchiamate, che a sua volta è 4 volte più veloce di un approccio di livello inferiore.

Lezioni da mantenere: la misurazione è sempre una buona cosa (ma deve essere accurata); metodi stringa come splitlinessono implementati in modi molto veloci; mettere insieme le stringhe programmando a un livello molto basso (specialmente per loop di +=pezzi molto piccoli) può essere piuttosto lento.

Modifica : aggiunta la proposta di @ Jacob, leggermente modificata per dare gli stessi risultati degli altri (vengono mantenuti gli spazi finali su una riga), ovvero:

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

La misurazione dà:

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

non abbastanza buono come l' .findapproccio basato - comunque, vale la pena tenerlo a mente perché potrebbe essere meno incline a piccoli bug off-by-one (qualsiasi ciclo in cui vedi le occorrenze di +1 e -1, come il mio f3sopra, dovrebbe automaticamente innescare sospetti off-by-one - e così dovrebbero essere molti loop privi di tali modifiche e dovrebbero averli - anche se credo che anche il mio codice sia corretto poiché sono stato in grado di controllare il suo output con altre funzioni ').

Ma l'approccio basato sulla divisione continua a dominare.

Una nota a parte: forse lo stile migliore per f4sarebbe:

from cStringIO import StringIO

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

almeno, è un po 'meno prolisso. La necessità di eliminare le \ns finali purtroppo proibisce la sostituzione più chiara e veloce del whileciclo con return iter(stri)(la iterparte di cui è ridondante nelle versioni moderne di Python, credo dalla 2.3 o 2.4, ma è anche innocua). Forse vale la pena provare, anche:

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

o variazioni di ciò - ma mi fermo qui poiché è praticamente un esercizio teorico rispetto a quello stripbasato, più semplice e veloce.


Inoltre, (line[:-1] for line in cStringIO.StringIO(foo))è abbastanza veloce; quasi veloce quanto l'implementazione ingenua, ma non del tutto.
Matt Anderson

Grazie per questa ottima risposta. Immagino che la lezione principale qui (dato che sono nuovo in Python) sia usare timeitun'abitudine.
Björn Pollex

@Spazio, sì, il tempo è buono, ogni volta che ti interessa le prestazioni (assicurati di usarlo con attenzione, ad esempio in questo caso vedi la mia nota sulla necessità di una listchiamata per cronometrare effettivamente tutte le parti rilevanti! -).
Alex Martelli

6
E il consumo di memoria? split()scambia chiaramente la memoria con la performance, conservando una copia di tutte le sezioni oltre alle strutture dell'elenco.
ivan_pozdeev

3
All'inizio ero davvero confuso dalle tue osservazioni perché hai elencato i risultati temporali nell'ordine opposto rispetto alla loro implementazione e numerazione. = P
jamesdlin

53

Non sono sicuro di cosa intendi per "poi di nuovo dal parser". Dopo che la divisione è stata eseguita, non c'è più attraversamento della stringa , solo un attraversamento dell'elenco delle stringhe divise. Questo sarà probabilmente il modo più veloce per farlo, a patto che la dimensione della tua stringa non sia assolutamente enorme. Il fatto che python usi stringhe immutabili significa che devi sempre creare una nuova stringa, quindi questo deve essere fatto comunque a un certo punto.

Se la tua stringa è molto grande, lo svantaggio è nell'utilizzo della memoria: avrai la stringa originale e un elenco di stringhe divise in memoria allo stesso tempo, raddoppiando la memoria richiesta. Un approccio iteratore può salvarti questo, costruendo una stringa secondo necessità, sebbene continui a pagare la penalità di "divisione". Tuttavia, se la tua stringa è così grande, in genere vuoi evitare che anche la stringa non divisa sia in memoria. Sarebbe meglio leggere semplicemente la stringa da un file, che già ti permette di iterarlo come linee.

Tuttavia, se hai già una stringa enorme in memoria, un approccio potrebbe essere quello di utilizzare StringIO, che presenta un'interfaccia simile a un file a una stringa, incluso consentire l'iterazione per riga (internamente usando .find per trovare la nuova riga successiva). Quindi ottieni:

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

5
Nota: per python 3 devi usare il iopacchetto per questo, ad esempio usa io.StringIOinvece di StringIO.StringIO. Vedi docs.python.org/3/library/io.html
Attila123

L'utilizzo StringIOè anche un buon modo per ottenere una gestione del newline universale ad alte prestazioni.
martineau

3

Se leggo Modules/cStringIO.ccorrettamente, dovrebbe essere abbastanza efficiente (anche se un po 'prolisso):

from cStringIO import StringIO

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

3

La ricerca basata su espressioni regolari è talvolta più veloce dell'approccio del generatore:

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

2
Questa domanda riguarda uno scenario specifico, quindi sarebbe utile mostrare un semplice benchmark, come ha fatto la risposta con il punteggio più alto.
Björn Pollex

1

Suppongo che potresti rotolare il tuo:

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

Non sono sicuro di quanto sia efficiente questa implementazione, ma itererà solo una volta sulla stringa.

Mmm, generatori.

Modificare:

Ovviamente vorrai anche aggiungere qualsiasi tipo di azione di analisi che desideri intraprendere, ma è piuttosto semplice.


Abbastanza inefficiente per le lunghe code (la +=parte ha O(N squared)prestazioni nel caso peggiore , anche se diversi trucchi di implementazione cercano di ridurla quando possibile).
Alex Martelli

Sì, l'ho imparato di recente. Sarebbe più veloce aggiungere a un elenco di caratteri e poi ''. Unirli (caratteri)? O è un esperimento che dovrei intraprendere io stesso? ;)
Wayne Werner

per favore misurati, è istruttivo - e assicurati di provare sia le linee brevi come nell'esempio dell'OP, sia quelle lunghe! -)
Alex Martelli

Per stringhe brevi (<~ 40 caratteri) il + = è effettivamente più veloce, ma colpisce rapidamente il caso peggiore. Per stringhe più lunghe, il .joinmetodo sembra effettivamente complessità O (N). Dal momento che non sono ancora riuscito a trovare il particolare confronto effettuato su SO, ho avviato una domanda stackoverflow.com/questions/3055477/… (che sorprendentemente ha ricevuto più risposte della mia!)
Wayne Werner,

0

Puoi iterare su "un file", che produce righe, incluso il carattere di fine riga. Per creare un "file virtuale" da una stringa, puoi utilizzare StringIO:

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

for line in io.StringIO(foo):
    print(repr(line))
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.