Gibt es eine Generatorversion von `string.split ()` in Python?

113

string.split()gibt eine Liste Instanz. Gibt es eine Version, die stattdessen einen Generator zurückgibt ? Gibt es Gründe gegen eine Generatorversion?

Manoj Govindan
quelle
3
Diese Frage könnte verwandt sein.
Björn Pollex
1
Der Grund ist, dass es sehr schwer ist, sich einen Fall vorzustellen, in dem es nützlich ist. Warum willst du das?
Glenn Maynard
10
@Glenn: Vor kurzem habe ich eine Frage zum Teilen einer langen Zeichenfolge in Stücke von n Wörtern gesehen. Eine der Lösungen war splitder String und gab dann einen Generator zurück, der am Ergebnis von arbeitete split. Das brachte mich zum Nachdenken, ob es eine Möglichkeit gab split, zunächst einen Generator zurückzugeben.
Manoj Govindan
5
Es gibt eine relevante Diskussion über den Python Issue Tracker: bugs.python.org/issue17343
saffsd
@GlennMaynard es kann nützlich sein für wirklich große nackte String / Datei-Analyse, aber jeder kann Generator-Parser selbst sehr einfach mit selbst gebrautem DFA und Ausbeute schreiben
Dmitry Ponyatov

Antworten:

77

Es ist sehr wahrscheinlich, dass re.finditerder Speicheraufwand relativ gering ist.

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

Demo:

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

edit: Ich habe gerade bestätigt, dass dies in Python 3.2.1 konstanten Speicher benötigt, vorausgesetzt, meine Testmethode war korrekt. Ich habe eine Zeichenfolge von sehr großer Größe (ca. 1 GB) erstellt und dann mit einer forSchleife durch die Iterable iteriert (KEIN Listenverständnis, das zusätzlichen Speicher generiert hätte). Dies führte nicht zu einem merklichen Speicherwachstum (das heißt, wenn das Speicherwachstum zunahm, war es weit weniger als die 1-GB-Zeichenfolge).

Ninjagecko
quelle
5
Ausgezeichnet! Ich hatte den Finditer vergessen. Wenn man daran interessiert wäre, so etwas wie Splitlines zu machen, würde ich vorschlagen, diese RE zu verwenden: '(. * \ N |. + $)' Str.splitlines hackt die trainling newline ab (etwas, das ich nicht wirklich mag ... ); Wenn Sie diesen Teil des Verhaltens replizieren möchten, können Sie die Gruppierung verwenden: (m.group (2) oder m.group (3) für m in re.finditer ('((. *) \ n | (. +)) $) ', s)). PS: Ich denke, die äußeren Paren im RE werden nicht benötigt. Ich fühle mich einfach unwohl bei der Verwendung von | ohne paren: P
allyourcode
3
Was ist mit Leistung? Der Abgleich sollte langsamer sein als die normale Suche.
Anatoly Techtonik
1
Wie würden Sie diese split_iter-Funktion so umschreiben, dass sie funktioniert a_string.split("delimiter")?
Moberg
split akzeptiert ohnehin reguläre Ausdrücke, daher ist es nicht wirklich schneller. Wenn Sie den zurückgegebenen Wert auf die nächste Weise verwenden möchten, sehen Sie sich meine Antwort unten an ...
Veltzer Doron
str.split()akzeptiert keine regulären Ausdrücke, daran re.split()denken Sie ...
alexis
17

Die effizienteste Art, wie ich mir vorstellen kann, eine mit dem offsetParameter der str.find()Methode zu schreiben . Dies vermeidet viel Speicherbedarf und ist auf den Overhead eines regulären Ausdrucks angewiesen, wenn dieser nicht benötigt wird.

[2016-8-2 bearbeiten: aktualisiert, um optional Regex-Trennzeichen zu unterstützen]

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

Dies kann verwendet werden, wie Sie wollen ...

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

Während jedes Mal, wenn find () oder Slicing ausgeführt wird, ein wenig nach Kosten in der Zeichenfolge gesucht wird, sollte dies minimal sein, da Zeichenfolgen als zusammenhängende Arrays im Speicher dargestellt werden.

Eli Collins
quelle
10

Dies ist eine Generatorversion von split()implementiert über re.search(), die nicht das Problem hat, zu viele Teilzeichenfolgen zuzuweisen.

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('][')

BEARBEITEN: Der Umgang mit umgebenden Leerzeichen wurde korrigiert, wenn keine Trennzeichen angegeben wurden.

Bernd Petersohn
quelle
12
warum ist das besser als re.finditer?
Erik Kaplun
@ErikKaplun Da die Regex-Logik für die Elemente komplexer sein kann als für ihre Trennzeichen. In meinem Fall wollte ich jede Zeile einzeln verarbeiten, damit ich zurückmelden kann, wenn eine Zeile nicht übereinstimmt.
Rovyko
8

Habe einige Leistungstests für die verschiedenen vorgeschlagenen Methoden durchgeführt (ich werde sie hier nicht wiederholen). Einige Ergebnisse:

  • str.split (Standard = 0,3461570239996945
  • manuelle Suche (nach Zeichen) (eine der Antworten von Dave Webb) = 0.8260340550004912
  • re.finditer (Antwort von Ninjagecko) = 0,698872097000276
  • str.find (eine der Antworten von Eli Collins) = 0,7230395330007013
  • itertools.takewhile (Antwort von Ignacio Vazquez-Abrams) = 2.023023967998597
  • str.split(..., maxsplit=1) Rekursion = N / A †

† Die Rekursionsantworten ( string.splitmit maxsplit = 1) werden nicht in angemessener Zeit abgeschlossen, da string.splitsie bei kürzeren Zeichenfolgen möglicherweise besser funktionieren, aber dann kann ich den Anwendungsfall für kurze Zeichenfolgen nicht sehen, bei denen der Speicher ohnehin kein Problem darstellt.

Getestet mit timeit:

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

Dies wirft eine weitere Frage auf, warum string.splites trotz seiner Speichernutzung so viel schneller geht.

cz
quelle
1
Dies liegt daran, dass der Speicher langsamer als die CPU ist und in diesem Fall die Liste von Blöcken geladen wird, wobei alle anderen Elemente Element für Element geladen werden. Aus dem gleichen Grund werden viele Wissenschaftler Ihnen sagen, dass verknüpfte Listen schneller und weniger komplex sind, während Ihr Computer mit Arrays häufig schneller ist, was sich leichter optimieren lässt. Sie können nicht davon ausgehen, dass eine Option schneller als eine andere ist. Testen Sie sie! +1 zum Testen.
Benoît P
Das Problem tritt in den nächsten Schritten einer Verarbeitungskette auf. Wenn Sie dann einen bestimmten Block suchen und den Rest ignorieren möchten, wenn Sie ihn finden, haben Sie die Berechtigung, anstelle der integrierten Lösung einen generatorbasierten Split zu verwenden.
jgomo3
6

Hier ist meine Implementierung, die viel, viel schneller und vollständiger ist als die anderen Antworten hier. Es hat 4 separate Unterfunktionen für verschiedene Fälle.

Ich kopiere einfach die Dokumentzeichenfolge der str_splitHauptfunktion:


str_split(s, *delims, empty=None)

sTeilen Sie die Zeichenfolge durch die restlichen Argumente und lassen Sie möglicherweise leere Teile weg (das emptySchlüsselwortargument ist dafür verantwortlich). Dies ist eine Generatorfunktion.

Wenn nur ein Trennzeichen angegeben wird, wird die Zeichenfolge einfach durch dieses getrennt. emptyist dann Truestandardmäßig.

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

Wenn mehrere Trennzeichen angegeben werden, wird die Zeichenfolge standardmäßig durch die längstmöglichen Folgen dieser Trennzeichen geteilt, oder, falls emptyfestgelegt True, werden auch leere Zeichenfolgen zwischen den Trennzeichen eingeschlossen. Beachten Sie, dass die Trennzeichen in diesem Fall nur einzelne Zeichen sein dürfen.

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

Wenn keine Trennzeichen angegeben sind, string.whitespacewird verwendet, sodass der Effekt der gleiche ist wie str.split(), außer dass diese Funktion ein Generator ist.

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)

Diese Funktion funktioniert in Python 3, und eine einfache, wenn auch ziemlich hässliche Korrektur kann angewendet werden, damit sie sowohl in 2 als auch in 3 Versionen funktioniert. Die ersten Zeilen der Funktion sollten geändert werden in:

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

Nein, aber es sollte einfach genug sein, eine mit zu schreiben itertools.takewhile().

BEARBEITEN:

Sehr einfache, halb kaputte Implementierung:

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
quelle
@Ignacio: Das Beispiel in docs verwendet eine Liste von Ganzzahlen, um die Verwendung von zu veranschaulichen takeWhile. Was wäre gut, predicateum eine Zeichenfolge mit (standardmäßig split) in Wörter aufzuteilen takeWhile()?
Manoj Govindan
Suchen Sie nach Präsenz in string.whitespace.
Ignacio Vazquez-Abrams
Das Trennzeichen kann mehrere Zeichen haben,'abc<def<>ghi<><>lmn'.split('<>') == ['abc<def', 'ghi', '', 'lmn']
kennytm
@Ignacio: Können Sie Ihrer Antwort ein Beispiel hinzufügen?
Manoj Govindan
1
Einfach zu schreiben, aber viele Größenordnungen langsamer. Dies ist eine Operation, die wirklich in nativem Code implementiert werden sollte.
Glenn Maynard
3

Ich sehe keinen offensichtlichen Vorteil für eine Generatorversion von split(). Das Generatorobjekt muss die gesamte Zeichenfolge enthalten, über die iteriert werden soll, damit Sie keinen Speicher mit einem Generator sparen.

Wenn Sie eine schreiben wollten, wäre es allerdings ziemlich einfach:

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
quelle
3
Sie würden den verwendeten Speicher halbieren, indem Sie nicht in jedem resultierenden Teil eine zweite Kopie der Zeichenfolge sowie den Array- und Objekt-Overhead speichern müssen (der normalerweise höher ist als die Zeichenfolgen selbst). Dies spielt jedoch im Allgemeinen keine Rolle (wenn Sie Zeichenfolgen so groß aufteilen, dass dies wichtig ist, machen Sie wahrscheinlich etwas falsch), und selbst eine native C-Generator-Implementierung wäre immer erheblich langsamer als alles auf einmal.
Glenn Maynard
@Glenn Maynard - das habe ich gerade gemerkt. Aus irgendeinem Grund würde der Generator ursprünglich eine Kopie der Zeichenfolge anstelle einer Referenz speichern. Ein kurzer Check mit id()hat mich richtig gestellt. Und da Zeichenfolgen unveränderlich sind, müssen Sie sich natürlich keine Sorgen machen, dass jemand die ursprüngliche Zeichenfolge ändert, während Sie darüber iterieren.
Dave Webb
6
Ist der Hauptpunkt bei der Verwendung eines Generators nicht die Speichernutzung, sondern die Möglichkeit, dass Sie sich die gesamte Zeichenfolge teilen müssen, wenn Sie vorzeitig beenden möchten? (Das ist kein Kommentar zu Ihrer speziellen Lösung, ich war nur überrascht von der Diskussion über das Gedächtnis).
Scott Griffiths
@Scott: Es ist schwer, sich einen Fall vorzustellen, in dem das wirklich ein Gewinn ist - 1: Sie möchten aufhören, sich auf halbem Weg zu teilen, 2: Sie wissen nicht, wie viele Wörter Sie im Voraus teilen, 3: Sie haben eine groß genug, damit es wichtig ist, und 4: Sie hören konsequent früh genug auf, damit es ein bedeutender Gewinn für str.split ist. Das sind sehr enge Bedingungen.
Glenn Maynard
4
Sie können einen viel höheren Nutzen haben, wenn Ihre Zeichenfolge ebenfalls träge generiert wird (z. B. durch Netzwerkverkehr oder Lesen von Dateien)
Lie Ryan
3

Ich habe eine Version von @ ninjageckos Antwort geschrieben, die sich eher wie string.split verhält (dh Leerzeichen, die standardmäßig begrenzt sind und Sie können ein Trennzeichen angeben).

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

Hier sind die Tests, die ich verwendet habe (sowohl in Python 3 als auch in 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

Das Regex-Modul von Python sagt, dass es "das Richtige" für Unicode-Leerzeichen tut , aber ich habe es nicht wirklich getestet.

Auch als Kernstück erhältlich .

dshepherd
quelle
3

Wenn Sie möchten auch in der Lage sein lesen einen Iterator (sowie Rückkehr eins) versuchen Sie dies:

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)

Verwendung

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

more_itertools.split_atbietet ein Analogon str.splitfür Iteratoren.

>>> 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 ist ein Paket von Drittanbietern.

Pylang
quelle
1
Beachten Sie, dass more_itertools.split_at () bei jedem Aufruf immer noch eine neu zugewiesene Liste verwendet. Dies gibt zwar einen Iterator zurück, erreicht jedoch nicht den konstanten Speicherbedarf. Je nachdem, warum Sie einen Iterator haben wollten, kann dies hilfreich sein oder auch nicht.
Jcater
@jcater Guter Punkt. Die Zwischenwerte werden tatsächlich entsprechend ihrer Implementierung als Unterlisten innerhalb des Iterators gepuffert . Man könnte die Quelle anpassen, um Listen durch Iteratoren zu ersetzen, itertools.chainErgebnisse mit einem Listenverständnis anzufügen und auszuwerten. Je nach Bedarf und Anfrage kann ich ein Beispiel posten.
Pylang
2

Ich wollte zeigen, wie man mit der find_iter-Lösung einen Generator für bestimmte Trennzeichen zurückgibt und dann mit dem paarweisen Rezept von itertools eine vorherige nächste Iteration erstellt, die die tatsächlichen Wörter wie in der ursprünglichen Split-Methode erhält.


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

Hinweis:

  1. Ich verwende prev & curr anstelle von prev & next, da das Überschreiben von next in Python eine sehr schlechte Idee ist
  2. Das ist sehr effizient
Veltzer Doron
quelle
1

Dümmste Methode ohne 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
quelle
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
Reiseknochen
quelle
warum gibst du nach [f[j:i]]und nicht f[j:i]?
Moberg
0

Hier ist eine einfache Antwort

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
quelle