web-dev-qa-db-fra.com

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

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

104
Manoj Govindan

Il est très probable que re.finditer utilise 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 itéré à travers l'itérable avec une boucle for (PAS une compréhension de liste, ce qui aurait généré de la mémoire supplémentaire). Cela n'a pas entraîné de croissance de la mémoire (c'est-à-dire, s'il y avait une croissance de la mémoire, elle était beaucoup moins que la chaîne de 1 Go).

64
ninjagecko

La manière la plus efficace que je puisse y penser pour en écrire un en utilisant le paramètre offset de la méthode str.find(). Cela évite beaucoup d'utilisation de la mémoire et s'appuie sur les frais généraux d'une expression rationnelle lorsqu'elle n'est pas nécessaire.

[modifier le 2016-8-2: mise à jour pour prendre en charge les séparateurs d'expressions régulières)

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ûts dans la chaîne chaque fois que find () ou le découpage est effectué, cela devrait être minime car les chaînes sont représentées comme des tableaux contingents en mémoire.

13
Eli Collins

Il s'agit de la version générateur 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 environnants si aucun caractère de séparateur n'est donné.

9
Bernd Petersohn

A fait quelques tests de performances 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 (L'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.split Avec maxsplit = 1) Ne se terminent pas dans un délai raisonnable, étant donné la vitesse de string.split, Elles peuvent mieux fonctionner sur des chaînes plus courtes, mais Je ne peux pas voir le cas d'utilisation pour les chaînes courtes où la mémoire n'est pas un problème de toute façon.

Testé avec timeit sur:

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 quant à la raison pour laquelle string.split Est tellement plus rapide malgré son utilisation de la mémoire.

8
c z

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

Je vais juste copier la docstring de la fonction principale str_split:


str_split(s, *delims, empty=None)

Fractionnez la chaîne s par le reste des arguments, en omettant éventuellement des parties vides (l'argument de mot clé empty en est responsable). Il s'agit d'une fonction de générateur.

Lorsqu'un seul délimiteur est fourni, la chaîne est simplement divisée par celui-ci. empty est alors True par 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 séquences les plus longues possibles de ces délimiteurs par défaut, ou, si empty est défini 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', ''

Quand 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 en Python 3, et un correctif facile, quoique 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')
6
Oleh Prypin

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

MODIFIER:

Implémentation très simple, à moitié interrompue:

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

Si vous souhaitez également pouvoir lire un itérateur (ainsi que retourner 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!']
3
reubano

J'ai écrit une version de la réponse de @ ninjagecko qui se comporte plus comme string.split (c'est-à-dire des espaces 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 (dans les deux 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 "la bonne chose" pour les espaces blancs unicode, mais je ne l'ai pas testé.

Également disponible en Gist .

3
dshepherd

Je ne vois aucun avantage évident pour une version de générateur de split(). L'objet générateur devra 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)
3
Dave Webb

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 paire d'itertools pour créer une précédente itération suivante 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 parce que remplacer next dans python est une très mauvaise idée
  2. C'est assez efficace
2
Veltzer Doron

more_itertools.spit_at offre un analogue à str.split pour 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.

2
pylang
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
0
travelingbones