Existe-t-il une version génératrice de `string.split ()` en Python?

113

string.split()renvoie une instance de liste . Existe-t-il une version qui renvoie un générateur à la place? Y a-t-il des raisons de ne pas avoir une version générateur?

Manoj Govindan
la source
3
Cette question pourrait être liée.
Björn Pollex
1
La raison en est qu'il est très difficile de penser à un cas où c'est utile. Pourquoi vous voulez ceci?
Glenn Maynard
10
@Glenn: Récemment, j'ai vu une question sur la division d'une longue chaîne en morceaux de n mots. Une des solutions splitla chaîne et a ensuite renvoyé un générateur travaillant sur le résultat de split. Cela m'a fait réfléchir s'il y avait un moyen splitde retourner un générateur pour commencer.
Manoj Govindan
5
Il y a une discussion pertinente sur le suivi des problèmes Python: bugs.python.org/issue17343
saffsd
@GlennMaynard cela peut être utile pour l'analyse de chaînes / fichiers nus très volumineux, mais n'importe qui peut écrire lui-même très facilement l'analyseur de générateur en utilisant DFA auto-brassé et le rendement
Dmitry Ponyatov

Réponses:

77

Il est très probable que cela re.finditerutilise une surcharge de mémoire assez minime.

def split_iter(string):
    return (x.group(0) for x in re.finditer(r"[A-Za-z']+", string))

Démo:

>>> list( split_iter("A programmer's RegEx test.") )
['A', "programmer's", 'RegEx', 'test']

edit: Je viens de confirmer que cela prend une mémoire constante dans python 3.2.1, en supposant que ma méthodologie de test était correcte. J'ai créé une chaîne de très grande taille (1 Go environ), puis j'ai parcouru l'itérable avec une forboucle (PAS une compréhension de liste, ce qui aurait généré de la mémoire supplémentaire). Cela n'a pas entraîné une augmentation notable de la mémoire (c'est-à-dire que s'il y avait une augmentation de la mémoire, c'était bien moins que la chaîne de 1 Go).

ninjagecko
la source
5
Excellent! J'avais oublié finditer. Si quelqu'un était intéressé à faire quelque chose comme les lignes fractionnées, je suggérerais d'utiliser ce RE: '(. * \ N |. + $)' Str.splitlines coupe la nouvelle ligne d'entraînement (quelque chose que je n'aime pas vraiment ... ); si vous souhaitez répliquer cette partie du comportement, vous pouvez utiliser le regroupement: (m.group (2) ou m.group (3) pour m dans re.finditer ('((. *) \ n | (. +) $) ', s)). PS: Je suppose que les paren externes dans le RE ne sont pas nécessaires; Je me sens juste mal à l'aise d'utiliser | sans paren: P
allyourcode
3
Et les performances? La correspondance doit être plus lente que la recherche ordinaire.
anatoly techtonik
1
Comment réécririez-vous cette fonction split_iter pour qu'elle fonctionne a_string.split("delimiter")?
Moberg
split accepte de toute façon les expressions régulières donc ce n'est pas vraiment plus rapide, si vous voulez utiliser la valeur retournée de manière précédente, regardez ma réponse en bas ...
Veltzer Doron
str.split()n'accepte pas les expressions régulières, c'est ce re.split()que vous pensez ...
alexis
17

La façon la plus efficace pour moi d'en écrire un en utilisant le offsetparamètre de la str.find()méthode. Cela évite une utilisation intensive de la mémoire et la surcharge d'une expression rationnelle lorsqu'elle n'est pas nécessaire.

[modifier 2016-8-2: mis à jour ceci pour prendre en charge facultativement les séparateurs d'expression régulière]

def isplit(source, sep=None, regex=False):
    """
    generator version of str.split()

    :param source:
        source string (unicode or bytes)

    :param sep:
        separator to split on.

    :param regex:
        if True, will treat sep as regular expression.

    :returns:
        generator yielding elements of string.
    """
    if sep is None:
        # mimic default python behavior
        source = source.strip()
        sep = "\\s+"
        if isinstance(source, bytes):
            sep = sep.encode("ascii")
        regex = True
    if regex:
        # version using re.finditer()
        if not hasattr(sep, "finditer"):
            sep = re.compile(sep)
        start = 0
        for m in sep.finditer(source):
            idx = m.start()
            assert idx >= start
            yield source[start:idx]
            start = m.end()
        yield source[start:]
    else:
        # version using str.find(), less overhead than re.finditer()
        sepsize = len(sep)
        start = 0
        while True:
            idx = source.find(sep, start)
            if idx == -1:
                yield source[start:]
                return
            yield source[start:idx]
            start = idx + sepsize

Cela peut être utilisé comme vous le souhaitez ...

>>> print list(isplit("abcb","b"))
['a','c','']

Bien qu'il y ait un peu de recherche de coût dans la chaîne à chaque fois que find () ou slicing est effectué, cela devrait être minime car les chaînes sont représentées comme des tableaux contingents en mémoire.

Eli Collins
la source
10

Il s'agit d'une version génératrice de split()implémentée via re.search()qui n'a pas le problème d'allouer trop de sous-chaînes.

import re

def itersplit(s, sep=None):
    exp = re.compile(r'\s+' if sep is None else re.escape(sep))
    pos = 0
    while True:
        m = exp.search(s, pos)
        if not m:
            if pos < len(s) or sep is not None:
                yield s[pos:]
            break
        if pos < m.start() or sep is not None:
            yield s[pos:m.start()]
        pos = m.end()


sample1 = "Good evening, world!"
sample2 = " Good evening, world! "
sample3 = "brackets][all][][over][here"
sample4 = "][brackets][all][][over][here]["

assert list(itersplit(sample1)) == sample1.split()
assert list(itersplit(sample2)) == sample2.split()
assert list(itersplit(sample3, '][')) == sample3.split('][')
assert list(itersplit(sample4, '][')) == sample4.split('][')

EDIT: Correction de la gestion des espaces blancs environnants si aucun caractère de séparation n'est donné.

Bernd Petersohn
la source
12
pourquoi est-ce mieux que re.finditer?
Erik Kaplun
@ErikKaplun Parce que la logique regex pour les éléments peut être plus complexe que pour leurs séparateurs. Dans mon cas, je voulais traiter chaque ligne individuellement, afin que je puisse signaler si une ligne ne correspond pas.
rovyko le
9

J'ai fait quelques tests de performance sur les différentes méthodes proposées (je ne les répéterai pas ici). Quelques résultats:

  • str.split (par défaut = 0,3461570239996945
  • recherche manuelle (par caractère) (une des réponses de Dave Webb) = 0.8260340550004912
  • re.finditer (réponse de ninjagecko) = 0,698872097000276
  • str.find (une des réponses d'Eli Collins) = 0,7230395330007013
  • itertools.takewhile (Réponse d'Ignacio Vazquez-Abrams) = 2.023023967998597
  • str.split(..., maxsplit=1) récursivité = N / A †

† Les réponses de récursivité ( string.splitavec maxsplit = 1) ne se terminent pas dans un temps raisonnable, étant donné la string.splitvitesse, elles peuvent mieux fonctionner sur des chaînes plus courtes, mais je ne vois pas le cas d'utilisation des chaînes courtes où la mémoire n'est de toute façon pas un problème.

Testé timeitsur:

the_text = "100 " * 9999 + "100"

def test_function( method ):
    def fn( ):
        total = 0

        for x in method( the_text ):
            total += int( x )

        return total

    return fn

Cela soulève une autre question de savoir pourquoi string.splitest tellement plus rapide malgré son utilisation de la mémoire.

cz
la source
2
C'est parce que la mémoire est plus lente que le processeur et dans ce cas, la liste est chargée par blocs où comme tous les autres sont chargés élément par élément. Sur la même note, de nombreux universitaires vous diront que les listes chaînées sont plus rapides et ont moins de complexité, tandis que votre ordinateur sera souvent plus rapide avec des tableaux, qu'il trouve plus faciles à optimiser. Vous ne pouvez pas supposer qu'une option est plus rapide qu'une autre, testez-la! +1 pour les tests.
Benoît P
Le problème se pose dans les étapes suivantes d'une chaîne de traitement. Si vous souhaitez ensuite trouver un morceau spécifique et ignorer le reste lorsque vous le trouvez, vous avez la justification d'utiliser un fractionnement basé sur un générateur au lieu de la solution intégrée.
jgomo3 le
6

Voici ma mise en œuvre, qui est beaucoup, beaucoup plus rapide et plus complète que les autres réponses ici. Il dispose de 4 sous-fonctions distinctes pour différents cas.

Je vais simplement copier la docstring de la str_splitfonction principale :


str_split(s, *delims, empty=None)

Divisez la chaîne spar le reste des arguments, en omettant éventuellement les parties vides (empty argument mot-clé en est responsable). C'est une fonction de générateur.

Lorsqu'un seul délimiteur est fourni, la chaîne est simplement divisée par lui. emptyest alors Truepar défaut.

str_split('[]aaa[][]bb[c', '[]')
    -> '', 'aaa', '', 'bb[c'
str_split('[]aaa[][]bb[c', '[]', empty=False)
    -> 'aaa', 'bb[c'

Lorsque plusieurs délimiteurs sont fournis, la chaîne est divisée par les séquences les plus longues possibles de ces délimiteurs par défaut, ou, si elle emptyest définie sur True, des chaînes vides entre les délimiteurs sont également incluses. Notez que les délimiteurs dans ce cas ne peuvent être que des caractères uniques.

str_split('aaa, bb : c;', ' ', ',', ':', ';')
    -> 'aaa', 'bb', 'c'
str_split('aaa, bb : c;', *' ,:;', empty=True)
    -> 'aaa', '', 'bb', '', '', 'c', ''

Lorsqu'aucun délimiteur n'est fourni, string.whitespace est utilisé, donc l'effet est le même que str.split(), sauf que cette fonction est un générateur.

str_split('aaa\\t  bb c \\n')
    -> 'aaa', 'bb', 'c'

import string

def _str_split_chars(s, delims):
    "Split the string `s` by characters contained in `delims`, including the \
    empty parts between two consecutive delimiters"
    start = 0
    for i, c in enumerate(s):
        if c in delims:
            yield s[start:i]
            start = i+1
    yield s[start:]

def _str_split_chars_ne(s, delims):
    "Split the string `s` by longest possible sequences of characters \
    contained in `delims`"
    start = 0
    in_s = False
    for i, c in enumerate(s):
        if c in delims:
            if in_s:
                yield s[start:i]
                in_s = False
        else:
            if not in_s:
                in_s = True
                start = i
    if in_s:
        yield s[start:]


def _str_split_word(s, delim):
    "Split the string `s` by the string `delim`"
    dlen = len(delim)
    start = 0
    try:
        while True:
            i = s.index(delim, start)
            yield s[start:i]
            start = i+dlen
    except ValueError:
        pass
    yield s[start:]

def _str_split_word_ne(s, delim):
    "Split the string `s` by the string `delim`, not including empty parts \
    between two consecutive delimiters"
    dlen = len(delim)
    start = 0
    try:
        while True:
            i = s.index(delim, start)
            if start!=i:
                yield s[start:i]
            start = i+dlen
    except ValueError:
        pass
    if start<len(s):
        yield s[start:]


def str_split(s, *delims, empty=None):
    """\
Split the string `s` by the rest of the arguments, possibly omitting
empty parts (`empty` keyword argument is responsible for that).
This is a generator function.

When only one delimiter is supplied, the string is simply split by it.
`empty` is then `True` by default.
    str_split('[]aaa[][]bb[c', '[]')
        -> '', 'aaa', '', 'bb[c'
    str_split('[]aaa[][]bb[c', '[]', empty=False)
        -> 'aaa', 'bb[c'

When multiple delimiters are supplied, the string is split by longest
possible sequences of those delimiters by default, or, if `empty` is set to
`True`, empty strings between the delimiters are also included. Note that
the delimiters in this case may only be single characters.
    str_split('aaa, bb : c;', ' ', ',', ':', ';')
        -> 'aaa', 'bb', 'c'
    str_split('aaa, bb : c;', *' ,:;', empty=True)
        -> 'aaa', '', 'bb', '', '', 'c', ''

When no delimiters are supplied, `string.whitespace` is used, so the effect
is the same as `str.split()`, except this function is a generator.
    str_split('aaa\\t  bb c \\n')
        -> 'aaa', 'bb', 'c'
"""
    if len(delims)==1:
        f = _str_split_word if empty is None or empty else _str_split_word_ne
        return f(s, delims[0])
    if len(delims)==0:
        delims = string.whitespace
    delims = set(delims) if len(delims)>=4 else ''.join(delims)
    if any(len(d)>1 for d in delims):
        raise ValueError("Only 1-character multiple delimiters are supported")
    f = _str_split_chars if empty else _str_split_chars_ne
    return f(s, delims)

Cette fonction fonctionne dans Python 3, et un correctif simple, mais assez laid, peut être appliqué pour le faire fonctionner dans les versions 2 et 3. Les premières lignes de la fonction doivent être remplacées par:

def str_split(s, *delims, **kwargs):
    """...docstring..."""
    empty = kwargs.get('empty')
Oleh Prypin
la source
3

Non, mais il devrait être assez facile d'en écrire un avec itertools.takewhile().

ÉDITER:

Implémentation très simple, à moitié cassée:

import itertools
import string

def isplitwords(s):
  i = iter(s)
  while True:
    r = []
    for c in itertools.takewhile(lambda x: not x in string.whitespace, i):
      r.append(c)
    else:
      if r:
        yield ''.join(r)
        continue
      else:
        raise StopIteration()
Ignacio Vazquez-Abrams
la source
@Ignacio: l'exemple de la documentation utilise une liste d'entiers pour illustrer l'utilisation de takeWhile. Qu'est-ce qui serait bon predicatepour diviser une chaîne en mots (par défaut split) en utilisant takeWhile()?
Manoj Govindan
Recherchez la présence dans string.whitespace.
Ignacio Vazquez-Abrams
Le séparateur peut avoir plusieurs caractères,'abc<def<>ghi<><>lmn'.split('<>') == ['abc<def', 'ghi', '', 'lmn']
kennytm
@Ignacio: Pouvez-vous ajouter un exemple à votre réponse?
Manoj Govindan
1
Facile à écrire, mais beaucoup plus lent. C'est une opération qui devrait vraiment être implémentée en code natif.
Glenn Maynard
3

Je ne vois aucun avantage évident à une version génératrice de split() . L'objet générateur va devoir contenir toute la chaîne à parcourir afin que vous n'économisiez pas de mémoire en ayant un générateur.

Si vous vouliez en écrire un, ce serait assez simple:

import string

def gsplit(s,sep=string.whitespace):
    word = []

    for c in s:
        if c in sep:
            if word:
                yield "".join(word)
                word = []
        else:
            word.append(c)

    if word:
        yield "".join(word)
Dave Webb
la source
3
Vous réduirez de moitié la mémoire utilisée, en n'ayant pas à stocker une deuxième copie de la chaîne dans chaque partie résultante, plus la surcharge du tableau et de l'objet (qui est généralement plus que les chaînes elles-mêmes). Cela n'a généralement pas d'importance, cependant (si vous divisez des chaînes si grandes que cela compte, vous faites probablement quelque chose de mal), et même une implémentation de générateur C natif serait toujours beaucoup plus lente que de tout faire en même temps.
Glenn Maynard
@Glenn Maynard - Je viens de m'en rendre compte. Je pour une raison quelconque, à l'origine, le générateur stockait une copie de la chaîne plutôt qu'une référence. Une vérification rapide avec id()m'a mis raison. Et évidemment, comme les chaînes sont immuables, vous n'avez pas à vous soucier du fait que quelqu'un change la chaîne d'origine pendant que vous l'itérez.
Dave Webb
6
Le point principal de l'utilisation d'un générateur n'est-il pas l'utilisation de la mémoire, mais que vous pourriez vous éviter d'avoir à diviser la chaîne entière si vous vouliez quitter plus tôt? (Ce n'est pas un commentaire sur votre solution particulière, j'ai juste été surpris par la discussion sur la mémoire).
Scott Griffiths
@Scott: Il est difficile de penser à un cas où c'est vraiment une victoire - où 1: vous voulez arrêter de diviser à mi-parcours, 2: vous ne savez pas combien de mots vous divisez à l'avance, 3: vous avez un assez grande chaîne pour que cela compte, et 4: vous vous arrêtez systématiquement assez tôt pour que ce soit une victoire significative sur str.split. C'est un ensemble très restreint de conditions.
Glenn Maynard
4
Vous pouvez avoir un avantage beaucoup plus élevé si votre chaîne est également générée paresseusement (par exemple à partir du trafic réseau ou de la lecture de fichiers)
Lie Ryan
3

J'ai écrit une version de la réponse de @ ninjagecko qui se comporte plus comme string.split (c'est-à-dire que des espaces sont délimités par défaut et vous pouvez spécifier un délimiteur).

def isplit(string, delimiter = None):
    """Like string.split but returns an iterator (lazy)

    Multiple character delimters are not handled.
    """

    if delimiter is None:
        # Whitespace delimited by default
        delim = r"\s"

    elif len(delimiter) != 1:
        raise ValueError("Can only handle single character delimiters",
                        delimiter)

    else:
        # Escape, incase it's "\", "*" etc.
        delim = re.escape(delimiter)

    return (x.group(0) for x in re.finditer(r"[^{}]+".format(delim), string))

Voici les tests que j'ai utilisés (en python 3 et python 2):

# Wrapper to make it a list
def helper(*args,  **kwargs):
    return list(isplit(*args, **kwargs))

# Normal delimiters
assert helper("1,2,3", ",") == ["1", "2", "3"]
assert helper("1;2;3,", ";") == ["1", "2", "3,"]
assert helper("1;2 ;3,  ", ";") == ["1", "2 ", "3,  "]

# Whitespace
assert helper("1 2 3") == ["1", "2", "3"]
assert helper("1\t2\t3") == ["1", "2", "3"]
assert helper("1\t2 \t3") == ["1", "2", "3"]
assert helper("1\n2\n3") == ["1", "2", "3"]

# Surrounding whitespace dropped
assert helper(" 1 2  3  ") == ["1", "2", "3"]

# Regex special characters
assert helper(r"1\2\3", "\\") == ["1", "2", "3"]
assert helper(r"1*2*3", "*") == ["1", "2", "3"]

# No multi-char delimiters allowed
try:
    helper(r"1,.2,.3", ",.")
    assert False
except ValueError:
    pass

Le module regex de python dit qu'il fait «ce qu'il faut» pour les espaces unicode, mais je ne l'ai pas testé.

Également disponible sous forme de résumé .

dshepherd
la source
3

Si vous souhaitez également pouvoir lire un itérateur (et en renvoyer un), essayez ceci:

import itertools as it

def iter_split(string, sep=None):
    sep = sep or ' '
    groups = it.groupby(string, lambda s: s != sep)
    return (''.join(g) for k, g in groups if k)

Usage

>>> list(iter_split(iter("Good evening, world!")))
['Good', 'evening,', 'world!']
reubano
la source
3

more_itertools.split_atpropose un analogique str.splitpour les itérateurs.

>>> import more_itertools as mit


>>> list(mit.split_at("abcdcba", lambda x: x == "b"))
[['a'], ['c', 'd', 'c'], ['a']]

>>> "abcdcba".split("b")
['a', 'cdc', 'a']

more_itertools est un package tiers.

pylang
la source
1
Notez que more_itertools.split_at () utilise toujours une liste nouvellement allouée à chaque appel, donc bien que cela renvoie un itérateur, il ne satisfait pas à l'exigence de mémoire constante. Donc, selon la raison pour laquelle vous vouliez commencer par un itérateur, cela peut être utile ou non.
jcater
@jcater Bon point. Les valeurs intermédiaires sont en effet mises en tampon sous forme de sous-listes au sein de l'itérateur, en fonction de son implémentation . On pourrait adapter la source pour substituer des listes avec des itérateurs, ajouter itertools.chainet évaluer les résultats en utilisant une compréhension de liste. En fonction du besoin et de la demande, je peux poster un exemple.
pylang
2

Je voulais montrer comment utiliser la solution find_iter pour renvoyer un générateur pour des délimiteurs donnés, puis utiliser la recette par paires d'itertools pour créer une itération suivante précédente qui obtiendra les mots réels comme dans la méthode de fractionnement d'origine.


from more_itertools import pairwise
import re

string = "dasdha hasud hasuid hsuia dhsuai dhasiu dhaui d"
delimiter = " "
# split according to the given delimiter including segments beginning at the beginning and ending at the end
for prev, curr in pairwise(re.finditer("^|[{0}]+|$".format(delimiter), string)):
    print(string[prev.end(): curr.start()])

Remarque:

  1. J'utilise prev & curr au lieu de prev & next car remplacer next en python est une très mauvaise idée
  2. C'est assez efficace
Veltzer Doron
la source
1

Méthode la plus stupide, sans regex / itertools:

def isplit(text, split='\n'):
    while text != '':
        end = text.find(split)

        if end == -1:
            yield text
            text = ''
        else:
            yield text[:end]
            text = text[end + 1:]
Tavy
la source
0
def split_generator(f,s):
    """
    f is a string, s is the substring we split on.
    This produces a generator rather than a possibly
    memory intensive list. 
    """
    i=0
    j=0
    while j<len(f):
        if i>=len(f):
            yield f[j:]
            j=i
        elif f[i] != s:
            i=i+1
        else:
            yield [f[j:i]]
            j=i+1
            i=i+1
os voyageant
la source
pourquoi cédez-vous [f[j:i]]et non f[j:i]?
Moberg
0

voici une réponse simple

def gen_str(some_string, sep):
    j=0
    guard = len(some_string)-1
    for i,s in enumerate(some_string):
        if s == sep:
           yield some_string[j:i]
           j=i+1
        elif i!=guard:
           continue
        else:
           yield some_string[j:]
user1438644
la source