Существует ли генераторная версия `string.split ()` в Python? - PullRequest
101 голосов
/ 05 октября 2010

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

Ответы [ 14 ]

61 голосов
/ 19 марта 2012

Весьма вероятно, что 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']

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

12 голосов
/ 19 марта 2012

Самый эффективный способ, которым я могу придумать, это написать один, используя параметр 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','']

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

9 голосов
/ 05 октября 2010

Это версия генератора 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('][')

РЕДАКТИРОВАТЬ: Исправлена ​​обработка окружающих пробеловесли символы-разделители не указаны.

8 голосов
/ 21 февраля 2017

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

  • str.split (по умолчанию = 0,3461570239996945
  • ручной поиск (по символам) (один из ответов Дейва Уэбба) = 0,8260340550004912
  • re.finditer (ответ Ниндзягеко) = 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 голосов
/ 07 октября 2012

Вот моя реализация, которая намного, намного быстрее и полнее, чем другие ответы здесь. Он имеет 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')
3 голосов
/ 08 января 2016

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

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!']
3 голосов
/ 17 апреля 2015

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

Модуль регулярных выражений Python говорит, что он делает "правильную вещь" для пробелов Юникода, но я на самом деле не проверял его.

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

3 голосов
/ 05 октября 2010

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

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

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 - сторонний пакет.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...