Esiste una versione generatrice di `string.split ()` in Python?


113

string.split()restituisce un'istanza di elenco . Esiste invece una versione che restituisce un generatore ? Ci sono ragioni per non avere una versione generatore?


3
Questa domanda potrebbe essere correlata.
Björn Pollex,

1
Il motivo è che è molto difficile pensare a un caso in cui sia utile. Perché vuoi questo?
Glenn Maynard

10
@ Glenn: Recentemente ho visto una domanda sulla divisione di una lunga stringa in blocchi di n parole. Una delle soluzioni splitla stringa e poi ha restituito un generatore lavorando sul risultato di split. Questo mi ha fatto pensare se ci fosse un modo per splitrestituire un generatore per iniziare.
Manoj Govindan

5
C'è una discussione pertinente sul tracker dei problemi di
saffsd

@GlennMaynard può essere utile per l'analisi di stringhe / file di grandi dimensioni, ma chiunque può scrivere un parser generatore da solo molto facilmente usando DFA auto-preparato e resa
Dmitry Ponyatov

Risposte:


77

È molto probabile che re.finditerutilizzi un sovraccarico di memoria abbastanza minimo.

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']

modifica: ho appena confermato che questo richiede memoria costante in Python 3.2.1, assumendo che la mia metodologia di test fosse corretta. Ho creato una stringa di dimensioni molto grandi (1 GB o giù di lì), quindi ho iterato attraverso l'iterabile con un forciclo (NON una comprensione di elenchi, che avrebbe generato memoria extra). Ciò non ha comportato una notevole crescita della memoria (ovvero, se c'era una crescita nella memoria, era di gran lunga inferiore alla stringa da 1 GB).


5
Eccellente! Mi ero dimenticato di Finditer. Se uno fosse interessato a fare qualcosa come le linee di divisione, suggerirei di usare questa RE: '(. * \ N |. + $)' Str.splitlines taglia la nuova riga di addestramento (qualcosa che non mi piace molto ... ); se volessi replicare quella parte del comportamento, potresti usare il raggruppamento: (m.group (2) o m.group (3) for m in re.finditer ('((. *) \ n | (. +) $) ', s)). PS: immagino che le parentesi esterne nella RE non siano necessarie; Mi sento solo a disagio nell'usare | senza parentesi: P
allyourcode

3
E le prestazioni? la corrispondenza dovrebbe essere più lenta della normale ricerca.
anatoly techtonik

1
Come riscriveresti questa funzione split_iter in modo che funzioni a_string.split("delimiter")?
Moberg

split accetta comunque le espressioni regolari, quindi non è molto più veloce, se vuoi usare il valore restituito in modo precedente, guarda la mia risposta in fondo ...
Veltzer Doron

str.split()non accetta espressioni regolari, è quello re.split()che stai pensando a ...
alexis

17

Il modo più efficiente in cui posso pensare di scriverne uno utilizzando il offsetparametro del str.find()metodo. Ciò evita un uso eccessivo della memoria e fa affidamento sull'overhead di un'espressione regolare quando non è necessaria.

[modifica 2016-8-2: aggiornato per supportare facoltativamente i separatori regex]

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

Questo può essere usato come vuoi ...

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

Sebbene ci sia un po 'di ricerca dei costi all'interno della stringa ogni volta che si esegue find () o slicing, questo dovrebbe essere minimo poiché le stringhe sono rappresentate come array contingenti in memoria.


10

Questa è la versione generatore di split()implementata tramite re.search()che non ha il problema di allocare troppe sottostringhe.

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: corretta gestione degli spazi bianchi circostanti se non vengono forniti caratteri di separazione.


12
perché questo è meglio di re.finditer?
Erik Kaplun

@ErikKaplun Perché la logica regex per gli elementi può essere più complessa rispetto ai separatori. Nel mio caso, volevo elaborare ogni riga individualmente, in modo da poter segnalare se una riga non corrispondeva.
rovyko

9

Ho fatto alcuni test delle prestazioni sui vari metodi proposti (non li ripeterò qui). Alcuni risultati:

  • str.split (predefinito = 0,3461570239996945
  • ricerca manuale (per carattere) (una delle risposte di Dave Webb) = 0,8260340550004912
  • re.finditer (risposta di ninjagecko) = 0,698872097000276
  • str.find (una delle risposte di Eli Collins) = 0,7230395330007013
  • itertools.takewhile (Risposta di Ignacio Vazquez-Abrams) = 2.023023967998597
  • str.split(..., maxsplit=1) ricorsione = N / A †

† Le risposte di ricorsione ( string.splitcon maxsplit = 1) non vengono completate in un tempo ragionevole, data string.splitla velocità potrebbero funzionare meglio su stringhe più brevi, ma poi non riesco a vedere il caso d'uso per stringhe brevi in ​​cui la memoria non è comunque un problema.

Testato utilizzando timeitsu:

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

Ciò solleva un'altra domanda sul perché string.splitè così molto più veloce nonostante l'utilizzo della memoria.


2
Questo perché la memoria è più lenta della cpu e in questo caso la lista viene caricata a blocchi dove come tutte le altre vengono caricate elemento per elemento. Sulla stessa nota, molti accademici ti diranno che gli elenchi collegati sono più veloci e hanno meno complessità mentre il tuo computer sarà spesso più veloce con gli array, che trova più facile da ottimizzare. Non puoi presumere che un'opzione sia più veloce di un'altra, provala! +1 per il test.
Benoît P,

Il problema sorge nelle fasi successive di una catena di elaborazione. Se poi vuoi trovare un pezzo specifico e ignorare il resto quando lo trovi, allora hai la giustificazione per usare una divisione basata su generatore invece della soluzione integrata.
jgomo3

6

Ecco la mia implementazione, che è molto, molto più veloce e più completa delle altre risposte qui. Ha 4 sottofunzioni separate per diversi casi.

Copierò semplicemente la docstring della str_splitfunzione principale :


str_split(s, *delims, empty=None)

Dividi la stringa sper il resto degli argomenti, possibilmente omettendo parti vuote (l' emptyargomento della parola chiave è responsabile di ciò). Questa è una funzione del generatore.

Quando viene fornito un solo delimitatore, la stringa viene semplicemente divisa da esso. emptyè quindi Trueper impostazione predefinita.

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

Quando vengono forniti più delimitatori, la stringa viene suddivisa per impostazione predefinita per le sequenze più lunghe possibili di tali delimitatori o, se emptyè impostato su True, vengono incluse anche stringhe vuote tra i delimitatori. Notare che i delimitatori in questo caso possono essere solo caratteri singoli.

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

Quando non vengono forniti delimitatori, string.whitespaceviene utilizzato, quindi l'effetto è lo stesso di str.split(), tranne che questa funzione è un generatore.

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)

Questa funzione funziona in Python 3 e può essere applicata una soluzione semplice, anche se piuttosto brutta, per farlo funzionare in entrambe le versioni 2 e 3. Le prime righe della funzione dovrebbero essere modificate in:

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

3

No, ma dovrebbe essere abbastanza facile scriverne uno usando itertools.takewhile().

MODIFICARE:

Implementazione molto semplice e incompleta:

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: L'esempio in docs utilizza un elenco di numeri interi per illustrare l'uso di takeWhile. Cosa sarebbe utile predicateper dividere una stringa in parole (impostazione predefinita split) usando takeWhile()?
Manoj Govindan

Cerca la presenza in string.whitespace.
Ignacio Vazquez-Abrams,

Il separatore può contenere più caratteri,'abc<def<>ghi<><>lmn'.split('<>') == ['abc<def', 'ghi', '', 'lmn']
kennytm

@ Ignacio: puoi aggiungere un esempio alla tua risposta?
Manoj Govindan

1
Facile da scrivere, ma molti ordini di grandezza più lenti. Questa è un'operazione che dovrebbe davvero essere implementata nel codice nativo.
Glenn Maynard

3

Non vedo alcun vantaggio evidente in una versione generatore di split(). L'oggetto generatore dovrà contenere l'intera stringa su cui eseguire l'iterazione, quindi non salverai alcuna memoria avendo un generatore.

Se volessi scriverne uno sarebbe abbastanza facile però:

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
Dimezzeresti la memoria utilizzata, non dovendo memorizzare una seconda copia della stringa in ciascuna parte risultante, oltre all'overhead dell'array e dell'oggetto (che in genere è maggiore delle stringhe stesse). Ciò in genere non ha importanza, però (se stai dividendo stringhe così grandi da essere importante, probabilmente stai facendo qualcosa di sbagliato), e anche un'implementazione del generatore C nativo sarebbe sempre significativamente più lenta rispetto a farlo tutto in una volta.
Glenn Maynard

@ Glenn Maynard - Me ne sono appena reso conto. Per qualche motivo, originariamente, il generatore avrebbe memorizzato una copia della stringa anziché un riferimento. Un rapido controllo con id()mi ha rimesso a posto. E ovviamente, poiché le stringhe sono immutabili, non devi preoccuparti che qualcuno cambi la stringa originale mentre stai iterando su di essa.
Dave Webb

6
Il punto principale nell'usare un generatore non non è l'utilizzo della memoria, ma che potresti risparmiarti di dover dividere l'intera stringa se volessi uscire prima? (Questo non è un commento sulla tua soluzione particolare, sono rimasto solo sorpreso dalla discussione sulla memoria).
Scott Griffiths

@Scott: È difficile pensare a un caso in cui sia davvero una vittoria - dove 1: vuoi smettere di dividere a metà, 2: non sai quante parole stai dividendo in anticipo, 3: hai un stringa abbastanza grande perché abbia importanza, e 4: ti fermi costantemente abbastanza presto perché sia ​​una vittoria significativa su str.split. È un insieme di condizioni molto ristretto.
Glenn Maynard

4
Puoi avere vantaggi molto maggiori se anche la tua stringa viene generata pigramente (ad esempio dal traffico di rete o dalle letture di file)
Lie Ryan

3

Ho scritto una versione della risposta di @ ninjagecko che si comporta più come string.split (cioè spazi bianchi delimitati per impostazione predefinita e puoi specificare un delimitatore).

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

Ecco i test che ho usato (sia in python 3 che 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

Il modulo regex di python dice che fa "la cosa giusta" per gli spazi bianchi unicode, ma in realtà non l'ho testato.

Disponibile anche come sintesi .


3

Se vuoi anche essere in grado di leggere un iteratore (oltre a restituirne uno) prova questo:

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)

uso

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

3

more_itertools.split_atoffre un analogo a str.splitper gli iteratori.

>>> 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 è un pacchetto di terze parti.


1
Si noti che more_itertools.split_at () sta ancora utilizzando un elenco appena allocato su ogni chiamata, quindi sebbene questo restituisca un iteratore, non raggiunge il requisito di memoria costante. Quindi, a seconda del motivo per cui si desidera iniziare con un iteratore, questo può o non può essere utile.
jcater

@jcater Buon punto. I valori intermedi sono infatti bufferizzati come sottoelenchi all'interno dell'iteratore, in base alla sua implementazione . Si potrebbe adattare la fonte per sostituire elenchi con iteratori, aggiungere itertools.chaine valutare i risultati utilizzando una comprensione dell'elenco. A seconda della necessità e della richiesta, posso inserire un esempio.
pylang

2

Volevo mostrare come utilizzare la soluzione find_iter per restituire un generatore per determinati delimitatori e quindi utilizzare la ricetta pairwise da itertools per costruire una precedente iterazione successiva che otterrà le parole effettive come nel metodo di divisione originale.


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

Nota:

  1. Uso prev & curr invece di prev & next perché sovrascrivere next in python è una pessima idea
  2. Questo è abbastanza efficiente

1

Metodo più stupido, senza 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:]

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

perché ti arrendi [f[j:i]]e no f[j:i]?
Moberg

0

ecco una semplice risposta

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:]
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.