Iterar sobre as linhas de uma sequência

119

Eu tenho uma seqüência de várias linhas definida assim:

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

Essa string que usamos como entrada de teste para um analisador que estou escrevendo. A função analisadora recebe um fileobjeto-como entrada e itera sobre ele. Ele também chama o next()método diretamente para pular linhas, então eu realmente preciso de um iterador como entrada, não de um iterável. Eu preciso de um iterador que itere sobre as linhas individuais dessa sequência, como um fileobjeto-faria sobre as linhas de um arquivo de texto. É claro que eu poderia fazer assim:

lineiterator = iter(foo.splitlines())

Existe uma maneira mais direta de fazer isso? Nesse cenário, a seqüência de caracteres deve ser percorrida uma vez para a divisão e, em seguida, novamente pelo analisador. Não importa no meu caso de teste, já que a string é muito curta lá, estou apenas perguntando por curiosidade. O Python possui tantos recursos úteis e eficientes para esse tipo de coisa, mas não consegui encontrar nada que atenda a essa necessidade.

Björn Pollex
fonte
12
você está ciente de que pode iterar foo.splitlines()certo?
SilentGhost
O que você quer dizer com "novamente pelo analisador"?
danben
4
@ SilentGhost: Eu acho que o ponto é não repetir a string duas vezes. Uma vez iterado splitlines()e uma segunda vez, iterando sobre o resultado desse método.
Felix Kling
2
Existe uma razão específica para que splitlines () não retorne um iterador por padrão? Eu pensei que a tendência era fazer isso geralmente para iterables. Ou isso é verdade apenas para funções específicas como dict.keys ()?
Cerno 31/01

Respostas:

144

Aqui estão três possibilidades:

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

A execução como o script principal confirma que as três funções são equivalentes. Com timeit(e a * 100para fooobter seqüências substanciais para uma medição mais 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

Observe que precisamos da list()chamada para garantir que os iteradores sejam percorridos, não apenas construídos.

IOW, a implementação ingênua é muito mais rápida e nem engraçada: 6 vezes mais rápida do que minha tentativa de fazer findchamadas, que por sua vez é 4 vezes mais rápida que uma abordagem de nível inferior.

Lições a reter: a medição é sempre uma coisa boa (mas deve ser precisa); métodos de string como splitlinessão implementados de maneira muito rápida; juntar as strings programando em um nível muito baixo (especialmente por loops de +=peças muito pequenas) pode ser bastante lento.

Edit : adicionou a proposta de Jacob, ligeiramente modificada para dar os mesmos resultados que os outros (os espaços em branco em uma linha são mantidos), ou seja:

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

A medição fornece:

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

não é tão bom quanto a .findabordagem baseada - ainda assim, lembre-se, pois pode ser menos propenso a pequenos erros pontuais (qualquer loop em que você vê ocorrências de +1 e -1, como o meu f3acima, deve automaticamente desencadear suspeitas de um por um - e o mesmo acontece com muitos loops que não possuem esses ajustes e devem tê-los - embora eu acredite que meu código também esteja correto, pois fui capaz de verificar sua saída com outras funções ').

Mas a abordagem baseada em divisão ainda domina.

Um aparte: possivelmente um estilo melhor f4seria:

from cStringIO import StringIO

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

pelo menos, é um pouco menos detalhado. \nInfelizmente, a necessidade de remover os trailing s proíbe a substituição mais clara e rápida do whileloop return iter(stri)(a iterparte da qual é redundante nas versões modernas do Python, acredito que desde 2.3 ou 2.4, mas também é inócua). Talvez valha a pena tentar também:

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

ou variações - mas estou parando aqui, já que é praticamente um exercício teórico stripbaseado no mais simples, e rápido.

Alex Martelli
fonte
Além disso, (line[:-1] for line in cStringIO.StringIO(foo))é muito rápido; quase tão rápido quanto a implementação ingênua, mas não exatamente.
Matt Anderson
Obrigado por esta ótima resposta. Eu acho que a principal lição aqui (como eu sou novo no python) é usar o timeithábito.
Björn Pollex
@ Espaço, sim, tempo; é bom, sempre que você se preocupa com o desempenho (certifique-se de usá-lo com cuidado; por exemplo, neste caso, veja minha nota sobre a necessidade de uma listchamada para realmente marcar todas as partes relevantes! -).
Alex Martelli
6
E o consumo de memória? split()troca claramente a memória pelo desempenho, mantendo uma cópia de todas as seções, além das estruturas da lista.
ivan_pozdeev
3
Fiquei realmente confuso com as suas observações a princípio, porque você listou os resultados do cronograma na ordem oposta à sua implementação e numeração. = P
jamesdlin 4/17
53

Não sei ao certo o que você quer dizer com "novamente pelo analisador". Após a divisão, não há mais a travessia da string , apenas a travessia da lista de strings divididas. Essa provavelmente será a maneira mais rápida de conseguir isso, desde que o tamanho da sua string não seja absolutamente enorme. O fato de o python usar seqüências imutáveis ​​significa que você deve sempre criar uma nova sequência, portanto, isso deve ser feito em algum momento.

Se a sua string for muito grande, a desvantagem está no uso da memória: você terá a string original e uma lista de strings divididas na memória ao mesmo tempo, duplicando a memória necessária. Uma abordagem do iterador pode economizar isso, criando uma cadeia conforme necessário, embora ainda pague a penalidade de "divisão". No entanto, se sua string for muito grande, você geralmente deseja evitar que a string não dividida esteja na memória. Seria melhor apenas ler a string de um arquivo, o que já permite que você itere como linhas.

No entanto, se você já tem uma cadeia enorme de memória, uma abordagem seria usar o StringIO, que apresenta uma interface semelhante a arquivo para uma cadeia, incluindo a possibilidade de iterar por linha (usando internamente .find para encontrar a próxima nova linha). Você então obtém:

import StringIO
s = StringIO.StringIO(myString)
for line in s:
    do_something_with(line)
Brian
fonte
5
Nota: para python 3, você deve usar o iopacote para isso, por exemplo, use em io.StringIOvez de StringIO.StringIO. Veja docs.python.org/3/library/io.html
Attila123
O uso StringIOtambém é uma boa maneira de obter manipulação universal de nova linha de alto desempenho.
21419 martineau
3

Se eu leio Modules/cStringIO.ccorretamente, isso deve ser bastante eficiente (embora um tanto detalhado):

from cStringIO import StringIO

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

Às vezes, a pesquisa baseada em regex é mais rápida que a abordagem de gerador:

RRR = re.compile(r'(.*)\n')
def f4(arg):
    return (i.group(1) for i in RRR.finditer(arg))
socketpair
fonte
2
Esta pergunta é sobre um cenário específico, portanto, seria útil mostrar uma referência simples, como a resposta da melhor pontuação.
Björn Pollex
1

Suponho que você possa fazer o seu próprio:

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

Não tenho certeza de quão eficiente é essa implementação, mas isso só irá iterar sua string uma vez.

Mmm, geradores.

Editar:

É claro que você também desejará adicionar qualquer tipo de ação de análise que queira executar, mas isso é bastante simples.

Wayne Werner
fonte
Bastante ineficiente para longas filas (a +=parte tem O(N squared)desempenho de pior caso , embora vários truques de implementação tentem diminuir isso quando possível).
precisa
Sim - eu acabei de aprender sobre isso recentemente. Seria mais rápido anexar a uma lista de caracteres e depois ''. Juntar (caracteres)? Ou é um experimento que eu deveria me empreender? ;)
Wayne Werner
por favor, medir-se, é instrutiva - e não deixe de experimentar ambas as linhas curtas, como no exemplo do OP, e as longas -!)
Alex Martelli
Para cadeias curtas (<~ 40 caracteres), o + = é realmente mais rápido, mas atinge o pior caso rapidamente. Para seqüências mais longas, o .joinmétodo realmente se parece com a complexidade O (N). Desde que eu não poderia encontrar a comparação especial feita em mais Então, eu comecei uma pergunta stackoverflow.com/questions/3055477/... (que surpreendentemente recebeu mais respostas do que apenas o meu próprio!)
Wayne Werner
0

Você pode iterar sobre "um arquivo", que produz linhas, incluindo o caractere de nova linha à direita. Para criar um "arquivo virtual" de uma string, você pode usar StringIO:

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

for line in io.StringIO(foo):
    print(repr(line))
Tomasz Gandor
fonte