2010-10-05 3 views
81

string.split() возвращает экземпляр. Есть ли версия, которая возвращает generator? Существуют ли какие-либо причины против наличия версии генератора?Есть ли версия генератора `string.split()` в Python?

+3

[Этот вопрос] (http://stackoverflow.com/questions/3054604/) может быть связан. –

+1

Причина в том, что очень сложно придумать случай, когда это полезно. Почему вы хотите этого? –

+9

@Glenn: Недавно я увидел вопрос о разделении длинной строки на куски n слов. Одно из решений «разделило» строку, а затем вернуло генератор, работающий над результатом «split». Это заставило меня подумать, есть ли способ «сплит» вернуть генератор для начала. –

ответ

41

Весьма вероятно, что re.finditer использует довольно минимальные издержки памяти.

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

Демо:

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

редактировать: Я только что подтвердил, что это требует постоянной памяти в питон 3.2.1, предполагая, что моя методика тестирования была правильной. Я создал строку очень большого размера (1 ГБ или около того), а затем повторил ее с помощью цикла с нулевым циклом for (НЕ было понятным списком, которое создало бы дополнительную память).Это не привело к заметному росту памяти (т. Е. При росте памяти она была намного меньше, чем строка 1 ГБ).

+1

Отлично! Я забыл о находчиве. Если бы кто-то интересовался тем, что делал что-то вроде разделенных линий, я бы предложил использовать этот RE: '(. * \ N |. + $)' Str.splitlines отрубает новую строку перевода (что-то мне не очень нравится ...); если вы хотите воспроизвести эту часть поведения, вы можете использовать группировку: (m.group (2) или m.group (3) для m в re.finditer ('((. *) \ n | (. +) $) ', s)). PS: Я думаю, что внешний паран в RE не нужен; Я просто чувствую себя неловко в использовании | без пароля: P – allyourcode

+1

Как насчет производительности? повторное сопоставление должно быть медленнее обычного поиска. –

+1

Как бы вы переписали эту функцию split_iter, чтобы работать как 'a_string.split (" delimiter ")'? – Moberg

2

Нет, но должно быть достаточно легко написать один, используя itertools.takewhile().

EDIT:

Очень простой, наполовину сломана реализация:

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: В примере в документах используется список целых чисел, чтобы проиллюстрировать использование 'takeWhile'. Что было бы хорошим предикатом для разделения строки на слова (по умолчанию 'split') с помощью' takeWhile() '? –

+0

Ищите присутствие в 'string.whitespace'. –

+0

Разделитель может иметь несколько символов, ''abc ghi <><> lmn'.split (' <> ') == [' abc kennytm

3

Я не вижу какой-либо очевидной выгоды для версии генератора split(). Объект-генератор должен содержать всю строку для перебора, чтобы вы не сохраняли память с помощью генератора.

Если вы хотите, чтобы написать один, было бы довольно легко, хотя:

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

Вы сократили бы половину используемой памяти, не сохраняя вторую копию строки в каждой результирующей части, а также накладные расходы массива и объекта (как правило, больше, чем сами строки). Это вообще не имеет значения, хотя (если вы разделяете строки настолько большие, что это важно, вы, вероятно, делаете что-то не так), и даже встроенная реализация генератора C всегда будет значительно медленнее, чем делать все сразу. –

+0

@Glenn Maynard - Я просто это понял. По какой-то причине я изначально генератор сохранил копию строки, а не ссылку. Быстрая проверка с помощью 'id()' верна мне. И, очевидно, поскольку строки неизменяемы, вам не нужно беспокоиться о том, что кто-то меняет исходную строку, пока вы ее итерации. –

+5

Разве это не главное в использовании генератора, а не в использовании памяти, но что вы могли бы сэкономить себе, чтобы разделить целую строку, если вы хотите выйти раньше? (Это не комментарий вашего конкретного решения, меня просто удивило обсуждение памяти). –

-1

Вы можете построить легко, используя саму str.split с лимитом:

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

Таким образом, вы не нужно копировать функциональность и поведение strip() (например, когда sep = None), и это зависит от возможной быстрой реализации на месте. Я предполагаю, что string.split прекратит сканирование строки для разделителей, когда у нее будет достаточно «частей».

Как указывает Гленн Мейнард, это плохо масштабируется для больших строк (O (n^2)). Я подтвердил это с помощью тестов «timit».

+3

Это O (n^2), что делает его катастрофически медленным, когда строка содержит много слов, например. '" abcd "* 1000000'. (Я объяснил это уже кому-то, кто дал такое же решение - он удалил ответ, так что теперь я снова повторяюсь ...) –

+0

@Glenn: хотя жаль, что такой четкий код не имеет хорошей сложности , Я бы подумал, что для строк типичной длины это будет прекрасно. Какова длина строк, которые вы обычно раскалываете? – SilentGhost

+1

Кроме того, вы можете улучшить производительность и код с помощью 'partition' (который не допускает разделителя' None'): 'while s: a, _, s = s.partition (sep); yield a' – SilentGhost

7

Это версия генератора split(), реализованная через re.search(), у которой нет проблемы выделения слишком большого количества подстрок.

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: Исправлена ​​обработка окружающих пустое пространство, если не разделительные символы не приведены.

+8

Почему это лучше, чем 're.finditer'? –

0

Нужна для меня, по крайней мере, файлы, используемые в качестве генераторов.

Это версия, которую я сделал в подготовке некоторых больших файлов с пустой строкой отделившихся блоки текста (это необходимо будет тщательно протестированы для случаев угловых в случае, если бы его использовать в производственной системе):

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

Самый эффективный способ, я могу думать об этом, чтобы написать один, используя параметр offset метода str.find(). Это позволяет избежать большого объема использования памяти и полагаться на накладные расходы регулярного выражения, когда это не требуется.

[править 2016-8-2: обновлен это необязательно поддержка регулярных выражений Сепараторы]

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 

Это можно использовать, как вы хотите ...

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

Хотя существует немного поиска стоимости в строке при каждом поиске find() или разрезании, это должно быть минимальным, поскольку строки представлены в виде континуальных массивов в памяти.

3

Вот моя реализация, которая намного, намного быстрее и полнее, чем другие ответы здесь. Он имеет 4 отдельные подфункции для разных случаев.

Я просто скопировать строку документации главного str_split функции:


str_split(s, *delims, empty=None) 

Разделить строку s от остальных аргументов, возможно, опуская пустые части (empty ключевое слово аргумент отвечает за что). Это функция генератора.

Когда задан только один разделитель, строка просто разделяется им. empty - это True по умолчанию.

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

Когда несколько разделителей поставляются, строка разбиваются на длинный возможных последовательностей этих разделителей по умолчанию, или, если empty установлен в True, также включены пустые строки между разделителями. Обратите внимание: разделителями в этом случае могут быть только одиночные символы.

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

Когда никакие разделители поставляются, string.whitespace используется, так что эффект такой же, как str.split(), за исключением того, эта функция является генератором.

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) 

Эта функция работает в Python 3, и простой, хотя и довольно некрасиво, исправление может быть применен, чтобы сделать его работу в обоих 2 и 3 версии. Первые строки функции должны быть изменены на:

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

Почему вы даете '[f [j: i]] 'а не' f [j: i] '? – Moberg

2

Я написал версию @ ninjagecko отвечают, что ведет себя больше как String.Split (то есть пробелы разделителями по умолчанию, и вы можете указать разделитель).

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

Вот тесты, которые я использовал (как в Python 3 и 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 

питона говорит, что does "the right thing" для юникода пробелов, но я на самом деле не проверял.

Также доступен как gist.

2

Если вы хотели бы также, чтобы иметь возможность чтения итератор (а также возвращение один) попробуйте это:

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) 

Использование

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

ли некоторые тесты производительности на различные предложенные методы (я не буду их повторять здесь). Некоторые результаты:

  • str.split (по умолчанию = 0,3461570239996945
  • ручной поиск (по характеру) (один из ответа Дэйва Уэбба-х) = 0,8260340550004912
  • re.finditer (ответ ninjagecko в) = 0,698872097000276
  • str.find (один Илая ответы Коллинза) = 0,7230395330007013
  • itertools.takewhile (ответ Ignacio Vazquez-Абрамса) = 2,023023967998597
  • str.split(..., maxsplit=1) рекурсии = N/A †

† рекурсии ответы (string.split с maxsplit = 1) не в состоянии выполнить в разумные сроки, учитывая string.split с скорость они могут работать лучше на более короткие строки, но я не могу видеть прецедент для коротких строк, где память в любом случае не является проблемой.

опробованный с использованием 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 

В этой связи возникает еще один вопрос, почему string.split так намного быстрее, несмотря на использование памяти.

0

Я хотел показать, как использовать решение find_iter для возврата генератора для заданных разделителей, а затем использовать парный рецепт из itertools для построения предыдущей следующей итерации, которая будет получать фактические слова, как в исходном методе split.


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

примечание:

  1. Я использую ПРЕД & Curr вместо пред & следующий, потому что переопределение рядом с питоном очень плохая идея
  2. Это довольно эффективный
0

more_itertools.spit_at предлагает аналог str.split для итераторов.

>>> 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'] 
Смежные вопросы