Подтвердить что ты не робот

Есть ли версия генератора `string.split()` в Python?

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

4b9b3361

Ответ 1

Весьма вероятно, что 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 RegEx test.") )
['A', "programmer's", 'RegEx', 'test']

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

Ответ 2

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

[edit 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() или slicing выполняется немного по порядку, это должно быть минимальным, поскольку строки представлены в виде конгруэнтных массивов в памяти.

Ответ 3

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

Ответ 4

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

Я просто скопирую docstring основной функции 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')

Ответ 5

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

  • str.split (по умолчанию = 0.3461570239996945
  • поиск вручную (по характеру) (один из ответов Dave Webb) = 0.8260340550004912
  • re.finditer (ответ ninjagecko) = 0.698872097000276
  • str.find (один из ответов Эли Коллинза) = 0.7230395330007013
  • itertools.takewhile (Ответ Игнасио Васкеса-Абрама) = 2.023023967998597
  • str.split(..., maxsplit=1) рекурсия = N/A †

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

Протестировано с помощью timeit on:

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 работает намного быстрее, несмотря на использование памяти.

Ответ 6

Нет, но должно быть достаточно просто написать один, используя 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()

Ответ 7

Я не вижу явных преимуществ для версии генератора 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)

Ответ 8

Я написал версию ответа @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 "\", "*" 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

python regex module говорит, что делает "правильную вещь" для unicode-пробелов, но я его на самом деле не тестировал.

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

Ответ 9

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

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

Ответ 10

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


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

Примечание:

  • Я использую prev и curr вместо prev и next, потому что переопределение следующего в python - очень плохая идея.
  • Это довольно эффективно

Ответ 11

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

more_itertools - сторонний пакет.

Ответ 12

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

Ответ 13

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

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

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)

Ответ 14

вот простой ответ

def gen_str(some_string):
    j=0
    guard = len(some_string)-1
    for i,s in enumerate(some_string):
        if s == '\n':
           yield some_string[j:i]
           j=i+1
        elif i!=guard:
           continue
        else:
           yield some_string[j:]

Ответ 15

Вы можете легко построить его, используя 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.