Python'da `string.split () '' in oluşturucu sürümü var mı?


113

string.split()bir liste örneği döndürür . Bunun yerine bir jeneratör döndüren bir sürüm var mı ? Jeneratör versiyonuna sahip olmanın herhangi bir nedeni var mı?


3
Bu soru ilgili olabilir.
Björn Pollex

1
Nedeni, yararlı olduğu bir durum düşünmenin çok zor olmasıdır. Neden bunu istiyorsun
Glenn Maynard

10
@Glenn: Geçenlerde uzun bir dizeyi n kelimelik parçalara bölmekle ilgili bir soru gördüm. Çözümlerden biri splitdize ve ardından sonucu üzerinde çalışan bir jeneratör döndürdü split. Bu split, başlangıç için bir jeneratörü geri getirmenin bir yolu olup olmadığını düşündürdü .
Manoj Govindan

5
Python Sorunu izleyiciyle ilgili bir tartışma var: bugs.python.org/issue17343
saffsd

@GlennMaynard, gerçekten büyük çıplak dizgi / dosya ayrıştırma için yararlı olabilir, ancak herkes kendi oluşturduğu DFA ve verimi kullanarak jeneratör ayrıştırıcısını çok kolay yazabilir
Dmitry Ponyatov

Yanıtlar:


77

re.finditerOldukça minimum bellek ek yükü kullanması oldukça muhtemeldir .

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

Düzenleme: Test metodolojimin doğru olduğunu varsayarak, bunun python 3.2.1'de sabit bellek aldığını doğruladım. Çok büyük boyutlu (1GB veya daha fazla) bir dize oluşturdum, daha sonra bir fordöngü ile yinelenebilir (liste anlayışı DEĞİL, ekstra bellek oluşturacaktı) boyunca yineledim . Bu, bellekte gözle görülür bir artışa neden olmadı (yani, bellekte bir büyüme varsa, 1GB dizesinden çok daha azdı).


5
Mükemmel! Bulucuyu unutmuştum. Bölünmüş çizgiler gibi bir şey yapmakla ilgileniyorsanız, bu RE'yi kullanmanızı öneririm: '(. * \ N |. + $)' Str.splitlines, eğitici satırsonu satırını keser (gerçekten sevmediğim bir şey ... ); Davranışın bu kısmını kopyalamak istiyorsanız, gruplamayı kullanabilirsiniz: (m.group (2) veya m.group (3), re.finditer'da m için ('((. *) \ n | (. +) $) ', s)). Not: Sanırım RE'nin dış parantezine gerek yok; Sadece kullanmaktan rahatsız oluyorum |
parantez

3
Ya performans? yeniden eşleştirme, sıradan aramadan daha yavaş olmalıdır.
anatoly techtonik

1
Bu split_iter işlevini nasıl çalışacak şekilde yeniden yazarsınız? a_string.split("delimiter") ?
Moberg

split normal ifadeleri yine de kabul ediyor, bu yüzden gerçekten daha hızlı değil, döndürülen değeri önceki bir şekilde kullanmak istiyorsanız, alttaki cevabıma bakın ...
cevabıma bakın

str.split()normal ifadeleri kabul etmiyor, re.split()düşündüğünüz bu ...
alexis

17

Metodun offsetparametresini kullanarak bir tane yazmak düşünebildiğim en verimli yol str.find(). Bu, çok fazla bellek kullanımını önler ve gerekmediğinde bir regexp'in ek yüküne güvenir.

[düzenleme 2016-8-2: isteğe bağlı olarak normal ifade ayırıcılarını destekleyecek şekilde güncellendi]

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

Bu, istediğiniz gibi kullanılabilir ...

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

Find () veya dilimleme her gerçekleştirildiğinde dizge içinde biraz maliyet arama olsa da, dizeler bellekte bitişik diziler olarak temsil edildiğinden bu minimum olmalıdır.


10

Bu, çok fazla alt dizge ayırma sorunu olmayan, split()aracılığıyla uygulanan jeneratör sürümüdür 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('][')

DÜZENLEME: Ayırıcı karakter verilmezse çevreleyen boşluğun işlenmesi düzeltildi.


12
neden bundan daha iyi re.finditer?
Erik Kaplun

@ErikKaplun Çünkü öğelerin regex mantığı, ayırıcılarından daha karmaşık olabilir. Benim durumumda, her satırı ayrı ayrı işlemek istedim, böylece bir hat eşleşmezse geri bildirimde bulunabilirim.
rovyko

9

Önerilen çeşitli yöntemler üzerinde bazı performans testleri yaptım (burada tekrar etmeyeceğim). Bazı sonuçlar:

  • str.split (varsayılan = 0,3461570239996945
  • manuel arama (karaktere göre) (Dave Webb'in cevaplarından biri) = 0,8260340550004912
  • re.finditer (ninjagecko'nun cevabı) = 0.698872097000276
  • str.find (Eli Collins'in yanıtlarından biri) = 0.7230395330007013
  • itertools.takewhile (Ignacio Vazquez-Abrams'ın yanıtı) = 2.023023967998597
  • str.split(..., maxsplit=1) yineleme = N / A †

† Özyineleme yanıtları ( string.splitile maxsplit = 1) makul bir sürede tamamlanamaz, string.splithızları göz önüne alındığında , daha kısa dizelerde daha iyi çalışabilirler, ancak belleğin zaten sorun olmadığı kısa dizeler için kullanım durumunu göremiyorum.

Kullanılarak test timeitedildi:

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

Bu string.split, bellek kullanımına rağmen neden bu kadar hızlı olduğu konusunda başka bir soruyu gündeme getiriyor .


2
Bunun nedeni, belleğin cpu'dan daha yavaş olmasıdır ve bu durumda, liste, diğerlerinin tümü öğe öğe yüklenirken yığınlar tarafından yüklenir. Aynı notta, birçok akademisyen, bağlantılı listelerin daha hızlı olduğunu ve daha az karmaşık olduğunu söylerken, bilgisayarınız genellikle optimize etmeyi daha kolay bulduğu dizilerle daha hızlı olacaktır. Bir seçeneğin diğerinden daha hızlı olduğunu varsayamazsınız, test edin! Test için +1.
Benoît P

Sorun, bir işleme zincirinin sonraki adımlarında ortaya çıkar. Daha sonra belirli bir parçayı bulmak ve onu bulduğunuzda geri kalanını görmezden gelmek istiyorsanız, yerleşik çözüm yerine jeneratör tabanlı bir bölme kullanma gerekçesine sahipsiniz.
jgomo3

6

İşte benim uygulamam, buradaki diğer cevaplardan çok, çok daha hızlı ve daha eksiksiz. Farklı durumlar için 4 ayrı alt işlevi vardır.

Sadece ana str_splitişlevin dokümantasyonunu kopyalayacağım :


str_split(s, *delims, empty=None)

sDizeyi argümanların geri kalanına bölün , muhtemelen boş kısımları atlayın ( emptyanahtar kelime argümanı bundan sorumludur). Bu bir jeneratör işlevidir.

Yalnızca bir sınırlayıcı sağlandığında, dize basitçe onunla bölünür. emptydaha sonra Truevarsayılan olarak.

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

Birden çok sınırlayıcı sağlandığında, dize varsayılan olarak bu sınırlayıcıların olası en uzun dizilerine bölünür veya eğer emptyayarlanmışsa True, sınırlayıcılar arasındaki boş dizeler de dahil edilir. Bu durumda sınırlayıcıların yalnızca tek karakterler olabileceğini unutmayın.

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

Sınırlayıcı sağlanmadığında string.whitespacekullanıldığında, str.split()bu işlevin bir jeneratör olması dışında etki ile aynıdır .

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)

Bu işlev Python 3'te çalışır ve hem 2 hem de 3 sürümde çalışmasını sağlamak için oldukça çirkin olsa da kolay bir düzeltme uygulanabilir. İşlevin ilk satırları şu şekilde değiştirilmelidir:

def str_split(s, *delims, **kwargs):
    """...docstring..."""
    empty = kwargs.get('empty')

3

Hayır, ancak kullanarak bir tane yazmak yeterince kolay olmalı itertools.takewhile() .

DÜZENLE:

Çok basit, yarı bozuk uygulama:

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: Dokümanlardaki örnek, kullanımını göstermek için bir tamsayı listesi kullanır takeWhile. predicateBir dizeyi splitkullanarak kelimelere (varsayılan ) bölmek için ne iyi olur takeWhile()?
Manoj Govindan

İçinde varlığı arayın string.whitespace.
Ignacio Vazquez-Abrams

Ayırıcı birden çok karakter 'abc<def<>ghi<><>lmn'.split('<>') == ['abc<def', 'ghi', '', 'lmn']
içerebilir

@Ignacio: Cevabınıza bir örnek ekleyebilir misiniz?
Manoj Govindan

1
Yazması kolay, ancak birçok emir daha yavaştır. Bu, gerçekten yerel kodda uygulanması gereken bir işlemdir.
Glenn Maynard

3

Bir jeneratör versiyonunun bariz bir faydasını görmüyorum split(). Oluşturucu nesnenin, yinelemek için tüm dizeyi içermesi gerekecek, böylece bir oluşturucuya sahip olarak herhangi bir bellek kaydetmeyeceksiniz.

Bir tane yazmak isteseydin, oldukça kolay olurdu:

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
Sonuçta ortaya çıkan her bölümde dizenin ikinci bir kopyasını, artı dizi ve nesne ek yükünü (bu genellikle dizelerin kendisinden daha fazladır) depolamak zorunda kalmayarak kullanılan belleği yarıya indirirsiniz. Yine de bu genellikle önemli değildir (dizeleri bu kadar büyük bölerseniz, muhtemelen yanlış bir şey yapıyorsunuzdur) ve yerel bir C üreteci uygulaması bile her zaman hepsini aynı anda yapmaktan önemli ölçüde daha yavaş olacaktır.
Glenn Maynard

@Glenn Maynard - Bunu şimdi anladım. Ben bazı nedenlerden ötürü, aslında jeneratör bir referans yerine dizinin bir kopyasını saklayacaktı. Hızlı bir kontrol id()beni düzeltti. Ve tabii ki dizeler değişmez olduğu için, üzerinde yineleme yaparken birinin orijinal dizeyi değiştirmesi konusunda endişelenmenize gerek yok.
Dave Webb

6
Jeneratör kullanmanın asıl amacı bellek kullanımı değil, erken çıkmak istiyorsanız tüm diziyi bölmek zorunda kalmadan kendinizi kurtarmanız değil mi? (Bu sizin özel çözümünüz hakkında bir yorum değil, sadece bellek hakkındaki tartışmaya şaşırdım).
Scott Griffiths

@Scott: Bunun gerçekten bir kazanç olduğu bir durumu düşünmek zor - burada 1: yarılmayı yarıda kesmek istiyorsun, 2: önceden kaç kelimeyi böldüğünü bilmiyorsun, 3: bir önemli olması için yeterince büyük ve 4: str.split karşısında önemli bir galibiyet olması için sürekli olarak yeterince erken durursunuz. Bu çok dar bir koşullar dizisidir.
Glenn Maynard

4
Diziniz de tembel olarak oluşturulmuşsa (örneğin ağ trafiğinden veya dosya okumalarından) çok daha fazla avantaja sahip olabilirsiniz
Lie Ryan

3

@ Ninjagecko'nun yanıtının daha çok string.split gibi davranan bir versiyonunu yazdım (yani beyaz boşluklar varsayılan olarak sınırlandırılmış ve bir sınırlayıcı belirtebilirsiniz).

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

İşte kullandığım testler (hem python 3 hem de python 2'de):

# 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'un normal ifade modülü "doğru şeyi" yaptığını söylüyor unicode beyaz boşluk için , ancak aslında test etmedim.

Ayrıca özet olarak da mevcuttur .


3

Ayrıca bir yineleyici okuyabilmek (aynı zamanda geri dönmek ) istiyorsanız şunu deneyin:

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)

kullanım

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

3

more_itertools.split_atstr.splityineleyiciler için bir analog sunar .

>>> 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 üçüncü taraf bir pakettir.


1
More_itertools.split_at () her çağrıda hala yeni ayrılmış bir liste kullanıyor, bu nedenle bu bir yineleyici döndürürken, sabit bellek gereksinimini karşılamıyor. Dolayısıyla neden bir yineleyicinin başlamasını istediğinize bağlı olarak, bu yardımcı olabilir veya olmayabilir.
jcater

@jcater İyi nokta. Ara değerler, uygulamasına göre yineleyici içinde alt listeler olarak gerçekten tamponlanır . Kaynak, listeleri yineleyicilerle ikame edecek, itertools.chainsonuçlara eklenecek ve bir liste anlayışı kullanarak sonuçları değerlendirecek şekilde uyarlanabilir . İhtiyaç ve talebe göre bir örnek gönderebilirim.
pylang

2

Verilen sınırlayıcılar için bir oluşturucu döndürmek için find_iter çözümünün nasıl kullanılacağını göstermek ve ardından orijinal bölünme yönteminde olduğu gibi gerçek kelimeleri alacak bir sonraki yinelemeyi oluşturmak için itertools'dan ikili tarifi nasıl kullanacağımı göstermek istedim.


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

Not:

  1. Prev & next yerine prev & curr kullanıyorum çünkü python'da bir sonrakini geçersiz kılmak çok kötü bir fikir
  2. Bu oldukça verimli

1

Normal ifade / yineleme araçları olmadan en saçma yöntem:

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

neden pes ediyorsun [f[j:i]]ve vermiyorsun f[j:i]?
Moberg

0

işte basit bir cevap

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:]
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.