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

119

J'ai une chaîne multiligne définie comme ceci:

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

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

lineiterator = iter(foo.splitlines())

Existe-t-il une manière plus directe de procéder? Dans ce scénario, la chaîne doit traverser une fois pour le fractionnement, puis à nouveau par l'analyseur. Cela n'a pas d'importance dans mon cas de test, puisque la chaîne est très courte là-bas, je demande juste par curiosité. Python a tellement de composants intégrés utiles et efficaces pour ce genre de choses, mais je n'ai rien trouvé qui réponde à ce besoin.

Björn Pollex
la source
12
vous savez que vous pouvez itérer, foo.splitlines()non?
SilentGhost
Que voulez-vous dire par "encore une fois par l'analyseur"?
danben
4
@SilentGhost: Je pense que le but est de ne pas itérer la chaîne deux fois. Une fois qu'il est itéré par splitlines()et une seconde fois en itérant sur le résultat de cette méthode.
Felix Kling
2
Y a-t-il une raison particulière pour laquelle splitlines () ne renvoie pas d'itérateur par défaut? Je pensais que la tendance était généralement de le faire pour les itérables. Ou est-ce seulement vrai pour des fonctions spécifiques comme dict.keys ()?
Cerno

Réponses:

144

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

L'exécution de ceci comme le script principal confirme que les trois fonctions sont équivalentes. Avec timeit(et un * 100for foopour 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' list()appel pour nous assurer que les itérateurs sont parcourus, pas seulement construits.

IOW, l'implémentation naïve est tellement plus rapide qu'elle n'est même pas drôle: 6 fois plus rapide que ma tentative avec des findappels, qui à son tour 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 comme splitlinessont implémentées de manière très rapide; assembler des chaînes en programmant à un niveau très bas (en particulier par des boucles +=de très petits morceaux) 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 blancs de fin sur une ligne sont conservés), soit:

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 mesure 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' .findapproche basée - encore, à garder à l'esprit car il pourrait être moins sujet à de petits bugs ponctuels (toute boucle où vous voyez des occurrences de +1 et -1, comme celle f3ci - dessus, devrait automatiquement déclencher des soupçons ponctuels - et il en va de même pour de nombreuses boucles qui n'ont pas de tels réglages et devraient les avoir - bien que je pense 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 division est toujours d'actualité.

Un aparté: peut-être un meilleur style pour f4serait:

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 verbeux. La nécessité de supprimer les trailing \ns interdit malheureusement le remplacement plus clair et plus rapide de la whileboucle par return iter(stri)(la iterpartie dont est redondante dans les versions modernes 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 des variations de celui-ci - mais je m'arrête ici car c'est à peu près un exercice théorique pour le stripplus simple et le plus rapide.

Alex Martelli
la source
En outre, (line[:-1] for line in cStringIO.StringIO(foo))c'est assez rapide; presque aussi rapide que l'implémentation naïve, mais pas tout à fait.
Matt Anderson
Merci pour cette excellente réponse. Je suppose que la leçon principale ici (car je suis nouveau en python) est de faire en utilisant timeitune habitude.
Björn Pollex
@Space, oui, le temps est bon, chaque fois que vous vous souciez de la performance (assurez-vous de l'utiliser avec précaution, par exemple, dans ce cas, consultez ma note sur la nécessité d'un listappel pour chronométrer toutes les parties pertinentes! -).
Alex Martelli
6
Qu'en est-il de la consommation de mémoire? split()troque clairement la mémoire pour la performance, tenant une copie de toutes les sections en plus des structures de la liste.
ivan_pozdeev
3
J'ai été vraiment déconcerté par vos remarques au début, car vous avez énuméré les résultats du calendrier dans l'ordre inverse de leur mise en œuvre et de leur numérotation. = P
jamesdlin
53

Je ne sais pas ce que vous entendez par "puis encore par l'analyseur". Une fois le fractionnement effectué, il n'y a plus de parcours de la chaîne , seulement un parcours de la liste des chaînes fractionnées. Ce sera probablement le moyen le plus rapide d'accomplir cela, tant que la taille de votre chaîne n'est pas absolument énorme. Le fait que python utilise des chaînes immuables signifie que vous devez toujours créer une nouvelle chaîne, donc cela doit être fait à un moment donné de toute façon.

Si votre chaîne est très volumineuse, l'inconvénient réside dans l'utilisation de la mémoire: vous aurez la chaîne d'origine et une liste de chaînes fractionnées en mémoire en même temps, doublant la mémoire requise. Une approche itératrice peut vous épargner cela, en construisant une chaîne selon vos besoins, même si elle paie toujours la pénalité de "fractionnement". Cependant, si votre chaîne est aussi grande, vous souhaitez généralement éviter même la chaîne non divisée en mémoire. Il serait préférable de simplement lire la chaîne à partir d'un fichier, ce qui vous permet déjà de l'itérer 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 à une chaîne, y compris en permettant l'itération par ligne (en utilisant .find en interne pour trouver la prochaine nouvelle ligne). Vous obtenez alors:

import StringIO
s = StringIO.StringIO(myString)
for line in s:
    do_something_with(line)
Brian
la source
5
Remarque: pour python 3, vous devez utiliser le iopackage pour cela, par exemple utiliser à la io.StringIOplace de StringIO.StringIO. Voir docs.python.org/3/library/io.html
Attila123
L'utilisation StringIOest également un bon moyen d'obtenir une gestion de nouvelle ligne universelle haute performance.
martineau
3

Si je lis Modules/cStringIO.ccorrectement, 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
Jacob Oscarson
la source
3

La recherche basée sur 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))
socketpair
la source
2
Cette question concerne un scénario spécifique, il serait donc utile de montrer un point de référence simple, comme l'a fait la réponse la plus performante.
Björn Pollex
1

Je suppose que vous pouvez 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 répétera qu'une seule fois sur votre chaîne.

Mmm, générateurs.

Éditer:

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

Wayne Werner
la source
Assez inefficace pour les longues lignes (la +=partie a les pires O(N squared)performances, bien que plusieurs astuces d'implémentation essaient de réduire cela lorsque cela est possible).
Alex Martelli
Ouais - je viens d'apprendre ça récemment. Serait-il plus rapide de les ajouter à une liste de caractères, puis de les joindre (caractères)? Ou est-ce une expérience que je devrais entreprendre moi-même? ;)
Wayne Werner
veuillez vous mesurer, c'est instructif - et assurez-vous d'essayer à la fois les lignes courtes comme dans l'exemple de l'OP, et les longues! -)
Alex Martelli
Pour les chaînes courtes (<~ 40 caractères), le + = est en fait plus rapide, mais atteint le pire des cas rapidement. Pour les chaînes plus longues, la .joinméthode ressemble en fait à une complexité O (N). Comme je n'ai pas encore trouvé la comparaison particulière faite sur SO, j'ai commencé une question stackoverflow.com/questions/3055477/… (qui a étonnamment reçu plus de réponses que les miennes!)
Wayne Werner
0

Vous pouvez parcourir "un fichier", qui produit des lignes, y compris le caractère de nouvelle ligne de fin. Pour créer un "fichier virtuel" à partir d'une chaîne, vous pouvez utiliser StringIO:

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

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