web-dev-qa-db-fra.com

Itérer sur les lignes d'une chaîne

J'ai une chaîne multi-lignes définie comme ceci:

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

Nous avons utilisé cette chaîne comme entrée de test pour un analyseur que j'écris. La fonction parser reçoit un objet file- en entrée et itère dessus. Elle appelle également la méthode next() directement pour sauter des lignes. J'ai donc vraiment besoin d'un itérateur, et non d'un itératif. J'ai besoin d'un itérateur qui itère sur les lignes individuelles de cette chaîne, comme le ferait un objet file- sur les lignes d'un fichier texte. Je pourrais bien sûr le faire comme ça:

lineiterator = iter(foo.splitlines())

Y a-t-il un moyen plus direct de faire cela? Dans ce scénario, la chaîne doit être parcourue une fois pour le fractionnement, puis à nouveau par l'analyseur. Cela n’a aucune importance dans mon cas de test, car la chaîne est très courte là-bas, je demande simplement par curiosité. Python possède de nombreuses fonctions intégrées utiles, mais je n’ai rien trouvé qui réponde à ce besoin.

104
Björn Pollex

Voici trois possibilités:

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 __== '__main__':
  for f in f1, f2, f3:
    print list(f())

En l'exécutant comme script principal, vous confirmez que les trois fonctions sont équivalentes. Avec timeit (et un * 100 Pour foo pour obtenir des chaînes substantielles pour une mesure plus précise):

$ 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

Notez que nous avons besoin de l’appel list() pour nous assurer que les itérateurs sont parcourus et pas seulement construits.

IOW, la mise en œuvre naïve est tellement plus rapide qu'elle n'est même pas drôle: 6 fois plus rapide que ma tentative d'appel avec find, ce qui est 4 fois plus rapide qu'une approche de niveau inférieur.

Leçons à retenir: la mesure est toujours une bonne chose (mais doit être précise); les méthodes de chaîne telles que splitlines sont mises en œuvre de manière très rapide; assembler des chaînes en programmant à un niveau très bas (en particulier par des boucles de += de très petites pièces) peut être assez lent.

Edit: ajout de la proposition de Jacob, légèrement modifiée pour donner les mêmes résultats que les autres (les espaces en fin de ligne sont conservés), c'est-à-dire:

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

Mesurer donne:

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

pas tout à fait aussi bon que l’approche basée sur .find - cela n’empêche que cela vaut la peine d’être gardé à l’esprit car il est peut-être moins sujet aux petits bogues non liés (une boucle où vous voyez des occurrences de +1 et -1, comme mon f3 ci-dessus, devrait automatiquement déclencher des suspicions - de même que de nombreuses boucles qui manquent de telles modifications et devraient les avoir - bien que je crois que mon code est également correct puisque j'ai pu vérifier sa sortie avec d'autres fonctions ').

Mais l'approche basée sur la scission règne toujours.

Un côté: peut-être un meilleur style pour f4 Serait:

from cStringIO import StringIO

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

au moins, c'est un peu moins bavard. La nécessité de supprimer les \n Suivants interdit malheureusement le remplacement plus clair et plus rapide de la boucle while par return iter(stri) (la partie iter est redondante dans les versions modernes). versions de Python, je crois depuis 2.3 ou 2.4, mais c’est aussi inoffensif). Cela vaut peut-être la peine d'essayer aussi:

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

ou leurs variantes - mais je m'arrête là car il s'agit plutôt d'un exercice théorique autour du strip, le plus simple et le plus rapide.

127
Alex Martelli

Je ne suis pas sûr de ce que vous entendez par "encore une fois par l'analyseur". Une fois la scission effectuée, il n'y a plus de parcours de la chaîne , il ne s'agit que d'un parcours de la liste des chaînes scindées. Ce sera probablement le moyen le plus rapide d'y parvenir, à condition que la taille de votre chaîne ne soit pas absolument énorme. Le fait que python utilise des chaînes immuables signifie que vous devez toujours créer une nouvelle chaîne, vous devez donc le faire. à un moment donné quand même.

Si votre chaîne est très longue, l'inconvénient réside dans l'utilisation de la mémoire: vous aurez en même temps la chaîne d'origine et une liste des chaînes scindées en mémoire, ce qui double la quantité de mémoire requise. Une approche itérateur peut vous éviter cela, en construisant une chaîne selon vos besoins, même si elle paie toujours la pénalité de "division". Cependant, si votre chaîne est aussi grosse, vous voulez généralement éviter que même la chaîne non coupée soit en mémoire. Il serait préférable de lire la chaîne d'un fichier, ce qui vous permet déjà de la parcourir sous forme de lignes.

Cependant, si vous avez déjà une énorme chaîne en mémoire, une approche serait d'utiliser StringIO, qui présente une interface de type fichier avec une chaîne, autorisant notamment une itération par ligne (en interne, utilisez .find pour trouver la nouvelle ligne suivante). Vous obtenez alors:

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

La recherche par regex est parfois plus rapide que l’approche du générateur:

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

Si je lis Modules/cStringIO.c correctement, cela devrait être assez efficace (bien qu'un peu verbeux):

from cStringIO import StringIO

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

Je suppose que vous pourriez rouler le vôtre:

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

Je ne suis pas sûr de l'efficacité de cette implémentation, mais cela ne fera qu'une itération sur votre chaîne.

Mmm, générateurs.

Modifier:

Bien sûr, vous voudrez également ajouter le type d'actions d'analyse que vous souhaitez entreprendre, mais c'est assez simple.

1
Wayne Werner