2010-10-05 14 views
81

string.split() zwraca instancję o numerze z numerem. Czy istnieje wersja, która zamiast tego zwraca wartość generator? Czy istnieją jakieś powody, aby nie mieć wersji generatora?Czy istnieje wersja generatora `string.split()` w języku Python?

+3

[To pytanie] (http://stackoverflow.com/questions/3054604/) może być powiązane. –

+1

Powodem jest to, że bardzo trudno jest myśleć o przypadku, w którym jest to przydatne. Dlaczego tego chcesz? –

+9

@ Glenn: Ostatnio zobaczyłem pytanie o podział długiego łańcucha na fragmenty n słów. Jedno z rozwiązań "podzieliło" ciąg znaków, a następnie zwróciło generator działający na wynik 'split'. To sprawiło, że zastanawiałem się, czy istnieje sposób na "split", aby zwrócić generator na początek. –

Odpowiedz

41

Jest wysoce prawdopodobne, że re.finditer wykorzystuje dość minimalne obciążenie pamięci.

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: właśnie potwierdziła, że ​​bierze stałej pamięci w python 3.2.1, zakładając, że moja metoda badania była prawidłowa. Stworzyłem ciąg bardzo dużego rozmiaru (1 GB lub więcej), a następnie iterowałem przez iterację za pomocą pętli for (NIE Zrozumienie listy, które spowodowałoby dodatkową pamięć).Nie spowodowało to zauważalnego wzrostu pamięci (to znaczy, jeśli nastąpił wzrost pamięci, było to znacznie mniej niż ciąg 1GB).

+1

Doskonale! Zapomniałem o założycielu. Jeśli ktoś byłby zainteresowany zrobieniem czegoś takiego jak splitlines, sugerowałbym użycie tego RE: '(. * \ N |. + $)' Str.splitlines odciąga się od szkolącego się newline'a (coś, czego naprawdę nie lubię ...); jeśli chcesz zreplikować tę część zachowania, możesz użyć grupowania: (m.group (2) lub m.group (3) dla m in re.finditer ('((. *) \ n | (. +) $) ", s)). PS: Domyślam się, że zewnętrzny paren w RE nie jest potrzebny; Po prostu czuję się nieswojo przy użyciu | bez papieru: P – allyourcode

+1

A co z wydajnością? ponowne dopasowywanie powinno być wolniejsze niż zwykłe wyszukiwanie. –

+1

Jak przerobiłbyś tę funkcję split_iter tak, aby działała jak 'a_string.split (" ogranicznik ")'? – Moberg

2

Nie, ale powinno być łatwo napisać jedną przy użyciu itertools.takewhile().

EDIT:

Bardzo proste, pół złamane realizacja:

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

@Ignacio: Przykład w dokumencie używa listy liczb całkowitych do zilustrowania użycia 'takeWhile'. Jaki byłby dobry "predykat" do dzielenia ciągu na słowa (domyślnie 'split') za pomocą' takeWhile() '? –

+0

Wyszukaj obecność w 'string.whitespace'. –

+0

Separator może mieć wiele znaków, '' abc ghi <><> lmn'.split ('<>') == ['abc kennytm

3

nie widzę żadnych oczywistych korzyści wersji generatora z split(). Obiekt generatora będzie musiał zawierać cały ciąg do iteracji, więc nie będziesz zapisywać pamięci poprzez generator.

Jeśli chciałeś napisać byłoby dość łatwo jednak:

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

Połowę użytej pamięci można zmniejszyć o połowę, ponieważ nie trzeba przechowywać drugiej kopii ciągu znaków w każdej wynikowej części, a także narzutu tablicy i obiektu (zwykle jest to więcej niż same ciągi znaków). Generalnie nie ma to znaczenia (jeśli dzielisz łańcuchy tak duże, że to ma znaczenie, prawdopodobnie robisz coś źle), a nawet natywna implementacja generatora C zawsze byłaby znacznie wolniejsza niż wykonanie wszystkiego naraz. –

+0

@Glenn Maynard - Właśnie to sobie uświadomiłem. Z jakiegoś powodu pierwotnie generator zapisałby kopię ciągu, a nie referencję. Szybkie sprawdzenie za pomocą 'id()' mnie poprawiło. I oczywiście, ponieważ ciągi znaków są niezmienne, nie musisz się martwić, że ktoś zmienia oryginalny ciąg podczas iteracji. –

+5

Czy głównym celem użycia generatora nie jest użycie pamięci, ale czy można oszczędzić sobie konieczności dzielenia całego ciągu znaków, jeśli chcesz wyjść wcześniej? (To nie jest komentarz do twojego konkretnego rozwiązania, byłem zaskoczony dyskusją o pamięci). –

-1

można zbudować łatwo za pomocą samego str.split z limitem:

def isplit(s, sep=None): 
    while s: 
     parts = s.split(sep, 1) 
     if len(parts) == 2: 
      s = parts[1] 
     else: 
      s = '' 
     yield parts[0] 

ten sposób, nie muszą replikować funkcjonalności i zachowania strip() (np. gdy sep = None) i zależy to od jego możliwie szybkiej implementacji natywnej. Zakładam, że string.split przestanie skanować ciąg znaków dla separatorów, gdy ma wystarczająco dużo "części".

Jak zauważa Glenn Maynard, skala ta jest słaba w przypadku dużych ciągów znaków (O (n^2)). Potwierdziłem to poprzez testy "timit".

+3

To jest O (n^2), co powoduje, że jest on tragicznie wolny, gdy ciąg znaków zawiera dużo słów, np. '" abcd "* 1000000'. (Wyjaśniłem to już komuś, kto podał to samo rozwiązanie - skasował odpowiedź, więc teraz muszę powtórzyć ...) –

+0

@Glenn: podczas gdy szkoda, że ​​tak jasny kod nie ma dobrej złożoności Myślę, że dla łańcuchów o typowej długości byłoby dobrze. Jaka jest długość ciągów, które zazwyczaj dzielisz? – SilentGhost

+1

Możesz także poprawić wydajność i kod używając 'partition' (która nie pozwala na separator' None'): 'while s: a, _, s = s.partition (sep); yield a' – SilentGhost

7

To jest wersja generatora split() zaimplementowana za pośrednictwem re.search(), która nie ma problemu z przydzieleniem zbyt wielu podciągów.

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: Poprawiona obsługa otaczające spacje, jeśli nie ma znaków separatora podano.

+8

dlaczego to jest lepsze niż 're.finditer'? –

0

Potrzeba jest dla mnie, przynajmniej z plikami używanymi jako generatory.

To jest wersja zrobiłem w ramach przygotowań do niektórych dużych plików z pustą linią separacji bloki tekstu (ta musiałaby zostać gruntownie przetestowany w przypadkach narożnych w przypadku byłoby go używać w systemie produkcyjnym):

from __future__ import print_function 

def isplit(iterable, sep=None): 
    r = '' 
    for c in iterable: 
     r += c 
     if sep is None: 
      if not c.strip(): 
       r = r[:-1] 
       if r: 
        yield r 
        r = ''      
     elif r.endswith(sep): 
      r=r[:-len(sep)] 
      yield r 
      r = '' 
    if r: 
     yield r 


def read_blocks(filename): 
    """read a file as a sequence of blocks separated by empty line""" 
    with open(filename) as ifh: 
     for block in isplit(ifh, '\n\n'): 
      yield block.splitlines()   

if __name__ == "__main__": 
    for lineno, block in enumerate(read_blocks("logfile.txt"), 1): 
     print(lineno,':') 
     print('\n'.join(block)) 
     print('-'*40) 

    print('Testing skip with None.') 
    for word in isplit('\tTony \t Jarkko \n Veijalainen\n'): 
     print(word) 
9

Najbardziej efektywny sposób, w jaki można go napisać, używając parametru offset metody str.find(). Pozwala to uniknąć dużej ilości pamięci i opiera się na narzucie regexp, gdy nie jest potrzebne.

[edytuj 02/08/2016: updated to opcjonalnie wsparcie regex separatory]

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 

ten może być używany jak chcesz ...

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

Chociaż istnieje trochę poszukiwania kosztów w ciągu za każdym razem, gdy find() lub krojenie jest wykonywane, powinno to być minimalne, ponieważ łańcuchy są reprezentowane w pamięci jako ciągłe tablice.

3

Oto moja implementacja, która jest znacznie, dużo szybsza i bardziej kompletna niż inne odpowiedzi tutaj. Posiada 4 oddzielne podfunkcje dla różnych przypadków.

będę po prostu skopiować docstring głównego str_split funkcji:


str_split(s, *delims, empty=None) 

podzielić ciąg s przez resztę argumentów, prawdopodobnie pomijając pustych części (empty kluczowe argument jest odpowiedzialny za że). To jest funkcja generatora.

Gdy dostarczany jest tylko jeden ogranicznik, ciąg jest po prostu dzielony przez niego. empty to domyślnie True.

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

Gdy wiele ograniczniki są dostarczane, ciąg jest dzielony przez najdłuższy możliwe sekwencje tych ograniczników domyślnie, lub, jeśli jest ustawiony na empty True, puste struny między ogranicznikami są również zawarte. Zauważ, że ograniczniki w tym przypadku mogą być pojedynczymi znakami.

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

Gdy ma ograniczniki są dostarczane string.whitespace jest stosowany, a więc wpływ jest taka sama jak str.split(), oprócz tego, że jest to funkcja generatora.

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) 

Ta funkcja działa w Pythonie 3 i łatwym, choć dość brzydki, Fix można zastosować, aby pracować w obu wersjach 2 i 3. Pierwsze wiersze funkcji powinno być zmienione na:

def str_split(s, *delims, **kwargs): 
    """...docstring...""" 
    empty = kwargs.get('empty') 
1
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

dlaczego dajesz '[f [j: i]] 'a nie' f [j: i] '? – Moberg

2

Napisałem wersję użytkownika @ ninjagecko odpowiedź, która zachowuje się bardziej jak string.split (tj spacje domyślnie ograniczona i można określić separator).

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

Oto testów użyłem (zarówno w Pythonie 3 i Python 2): Moduł regex

# 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 

Pythona mówi, że does "the right thing" dla Unicode spacji, ale nie faktycznie przetestowane.

Dostępne również jako gist.

2

Jeżeli chcesz też mieć możliwość czytać iterator (a także powrotną jeden) Spróbuj:

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) 

Wykorzystanie

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

zrobił kilka testów wydajności na różne proponowane metody (nie będę ich tutaj powtarzać). Niektóre wyniki:

  • str.split (domyślne = 0,3461570239996945
  • ręczne wyszukiwanie (po znaku) (jedna odpowiedź Dave Webb użytkownika) = 0,8260340550004912
  • re.finditer (odpowiedź ninjagecko w) = 0,698872097000276
  • str.find (jeden ocalenia odpowiedzi Collinsa) = 0,7230395330007013
  • itertools.takewhile (odpowiedź Ignacio Vazquez-Abrams'S) = 2,023023967998597
  • str.split(..., maxsplit=1) rekurencji = N/A †

† odpowiedź rekursji (string.split z maxsplit = 1) nie zakończyć w rozsądnym czasie, biorąc pod uwagę string.split s prędkość mogą one działać lepiej na krótsze ciągi, ale potem nie mogę zobaczyć przypadek użycia krótkich łańcuchów, w których pamięć i tak nie jest problemem.

testowano przy użyciu timeit na:

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 

To rodzi kolejne pytanie, dlaczego string.split jest tak dużo szybciej pomimo jego użycia pamięci.

0

Chciałem pokazać, jak użyć rozwiązania find_iter, aby zwrócić generator dla danych ograniczników, a następnie użyć przepisu parowania z itertools do zbudowania poprzedniej następnej iteracji, która otrzyma rzeczywiste słowa jak w oryginalnej metodzie dzielenia.


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

uwaga:

  1. Używam prev & Curr zamiast poprz & obok ponieważ nadrzędnym obok w Pythonie jest to bardzo zły pomysł
  2. Jest to dość skuteczny
0

more_itertools.spit_at oferuje analog do str.split dla iteratorów.

>>> 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'] 
Powiązane problemy