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?
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).
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.
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é.
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,3461570239996945re.finditer
(Réponse de ninjagecko) = 0,698872097000276str.find
(L'une des réponses d'Eli Collins) = 0,7230395330007013itertools.takewhile
(Réponse d'Ignacio Vazquez-Abrams) = 2.023023967998597str.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.
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')
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!']
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 .
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)
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:
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.
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