Чи існує версія генератора `string.split ()` в Python?


113

string.split()повертає екземпляр списку . Чи є версія, яка замість цього повертає генератор ? Чи є якісь причини проти версії генератора?


3
Це питання може бути пов’язане.
Бьорн Поллекс

1
Причина в тому, що дуже важко придумати випадок, коли це корисно. Чому ти цього хочеш?
Гленн Мейнард

10
@Glenn: Нещодавно я побачив питання про розділення довгих рядків на шматки n слів. Одне з рішень splitрядок, а потім повернув генератор, що працює на результат split. Це змусило мене задуматися, чи є спосіб splitповернути генератор для початку.
Маной Говіндан

5
Існує відповідна дискусія щодо трекера випуску Python Issue: bugs.python.org/issue17343
saffsd

@GlennMaynard це може бути корисно для дійсно великого синтаксичного розбору рядків / файлів, але кожен може дуже просто написати генератор-аналізатор, використовуючи самозаварений DFA та вихід
Дмитро Понятов

Відповіді:


77

Цілком ймовірно, що 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']

редагувати: Я щойно підтвердив, що це вимагає постійної пам'яті в python 3.2.1, припускаючи, що моя методика тестування була правильною. Я створив рядок дуже великого розміру (1 Гб або близько того), потім повторився через ітерабельний forцикл (НЕ розуміння списку, що створило б додаткову пам'ять). Це не призвело до помітного зростання пам’яті (тобто якщо відбулося зростання пам’яті, це було набагато менше, ніж рядок 1 ГБ).


5
Відмінно! Я забув про Finditer. Якщо хтось зацікавився зробити щось на зразок роздвоєних ліній, я б запропонував скористатися цим RE: '(. * \ N |. + $)' Str.splitlines відрізає трейлінговий новий рядок, хоча (те, що мені не дуже подобається ... ); якщо ви хотіли скопіювати цю частину поведінки, ви можете використовувати групування: (m.group (2) або m.group (3) для m в re.finditer ('((. *) \ n | (. +) $) ', s)). PS: Я здогадуюсь, зовнішній парен в RE не потрібен; Мені просто неприємно користуватися | без батьків: P
батьків allyourcode

3
А як щодо продуктивності? Відповідність повинна бути повільнішою, ніж звичайний пошук.
анатолійний технонік

1
Як би ви переписали цю функцію split_iter для роботи a_string.split("delimiter")?
Moberg

розкол приймає регулярні вирази в будь-якому випадку, тому це не дуже швидко, якщо ви хочете використовувати повернене значення попередньо наступним чином, подивіться на мою відповідь внизу ...
Вельтцер Дорон

str.split()не приймає регулярних виразів, це re.split()ви думаєте про ...
alexis

17

Найефективніший спосіб, який я можу придумати, написати це, використовуючи 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 () або нарізання виконується, потрібно небагато витрат на рядок, це повинно бути мінімальним, оскільки рядки представлені як пам'ятні масиви в пам'яті.


10

Це генераторна версія, 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: Виправлено обробку навколишнього пробілу, якщо не вказано розділових знаків.


12
чому це краще, ніж re.finditer?
Ерік Каплун

@ErikKaplun Оскільки логіка регулярних виразів для елементів може бути складнішою, ніж для їх роздільників. У моєму випадку я хотів обробити кожен рядок окремо, тому можу повідомити про те, що рядок не збігся.
rovyko

9

Провели тестування на різні запропоновані методи (я не повторюю їх тут). Деякі результати:

  • 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:

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так швидше, незважаючи на використання пам'яті.


1
Це тому, що пам'ять повільніше, ніж процесор, і в цьому випадку список завантажується шматками, куди як і всі інші завантажуються елемент за елементом. У цій же примітці багато науковців скажуть, що пов’язані списки швидші та мають меншу складність, тоді як ваш комп’ютер часто буде швидше з масивами, які оптимізувати буде простіше. Ви не можете припустити, що варіант швидший за інший, протестуйте його! +1 для тестування.
Benoît P

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

6

Ось моя реалізація, яка набагато, набагато швидша і повніша, ніж інші відповіді тут. Він має 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')

3

Ні, але це має бути досить просто, щоб написати одне за допомогою itertools.takewhile().

Редагувати:

Дуже проста, напівзламана реалізація:

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: Приклад у документах використовує список цілих чисел для ілюстрації використання takeWhile. Що було б корисно predicateдля розділення рядка на слова (за замовчуванням split) за допомогою takeWhile()?
Маной Говіндан

Шукайте присутність у string.whitespace.
Ігнасіо Васкес-Абрамс

У роздільника може бути кілька символів,'abc<def<>ghi<><>lmn'.split('<>') == ['abc<def', 'ghi', '', 'lmn']
kennytm

@Ignacio: Чи можете ви додати приклад до своєї відповіді?
Маной Говіндан

1
Легко писати, але на багато порядків повільніше. Це операція, яка дійсно повинна бути реалізована в рідному коді.
Гленн Мейнард

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 генератора завжди буде значно повільніше, ніж робити це все відразу.
Гленн Мейнард

@Glenn Maynard - Я щойно це зрозумів. Я чомусь генератор спочатку зберігав би копію рядка, а не посилання. Швидкий чек з id()мене правильно. І очевидно, оскільки рядки незмінні, вам не потрібно турбуватися про те, що хтось змінить початковий рядок, поки ви повторюєте його.
Дейв Вебб

6
Хіба не головним моментом у використанні генератора є не використання пам’яті, а те, що ви могли врятувати себе, щоб розділити цілу рядок, якщо хочете вийти зранку? (Це не коментар до вашого конкретного рішення, я просто був здивований дискусією про пам’ять).
Скотт Гріффітс

@Scott: Важко придумати випадок, коли це справді виграш - де 1: ти хочеш перестати ділити частково наскрізь, 2: ти не знаєш, скільки слів ти розділяєш заздалегідь, 3: у тебе є достатньо велика струна, щоб вона мала значення, і 4: ви послідовно зупиняєтесь досить рано, щоб це було значним перемогою над str.split. Це дуже вузький набір умов.
Глен Мейнард

4
Ви можете отримати набагато більшу вигоду, якщо ваш рядок також ліниво генерується (наприклад, з мережевого трафіку чи зчитування файлів)
Lie Ryan

3

Я написав версію відповіді @ 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 каже, що він робить "правильно" для пробілу unicode, але я насправді не перевіряв його.

Також доступний як суть .


3

Якщо ви також хочете мати можливість прочитати ітератор (а також повернути його), спробуйте це:

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

more_itertools.split_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 є стороннім пакетом.


1
Зауважте, що more_itertools.split_at () все ще використовує щойно виділений список для кожного виклику, тому, хоча це і повертає ітератор, він не досягає постійної потреби в пам'яті. Отже, залежно від того, з чого ви хотіли почати ітератор, це може бути, а може і не корисно.
jcater

@jcater Добре. Проміжні значення дійсно буферизовані як підсписи всередині ітератора, відповідно до його реалізації . Можна було адаптувати джерело до заміни списків ітераторами, додавати itertools.chainта оцінювати результати, використовуючи розуміння списку. Залежно від потреби та запиту, я можу розмістити приклад.
pylang

2

Я хотів показати, як за допомогою рішення 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()])

Примітка:

  1. Я використовую prev & curr замість prev & next, оскільки переосмислення наступного в python - дуже погана ідея
  2. Це досить ефективно

1

Найбільш глухий метод, без регулярних виразів / 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

чому ви поступаєтесь, [f[j:i]]а ні f[j:i]?
Моберг

0

ось проста відповідь

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:]
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.