Bir dizenin satırlarını yineleyin


119

Şu şekilde tanımlanmış çok satırlı bir dizem var:

foo = """
this is 
a multi-line string.
"""

Yazdığım bir ayrıştırıcı için test girdisi olarak kullandığımız bu dizge. Ayrıştırıcı işlevi, filegirdi olarak bir nesneyi alır ve üzerinde yineler. Ayrıca, next()doğrudan satırları atlamak için yöntemi çağırır , bu yüzden gerçekten bir yineleyici değil, girdi olarak bir yineleyiciye ihtiyacım var. Bir file-nesnenin bir metin dosyasının satırları üzerinde olacağı gibi bu dizenin tek tek satırlarını yineleyen bir yineleyiciye ihtiyacım var . Elbette şöyle yapabilirim:

lineiterator = iter(foo.splitlines())

Bunu yapmanın daha doğrudan bir yolu var mı? Bu senaryoda, dizge bölme için bir kez ve sonra ayrıştırıcı tarafından tekrar geçmek zorundadır. Benim test durumumda bunun bir önemi yok, orada ip çok kısa olduğu için meraktan soruyorum. Python'un bu tür şeyler için pek çok kullanışlı ve verimli yerleşik yapısı var, ancak bu ihtiyaca uyan hiçbir şey bulamadım.


12
Tekrar tekrar yapabileceğinin farkındasın foo.splitlines()değil mi?
SilentGhost

"Ayrıştırıcı tarafından tekrar" derken neyi kastediyorsunuz?
danben

4
@SilentGhost: Bence önemli olan dizeyi iki kez yinelememek. Bir kez splitlines()ve bu yöntemin sonucu üzerinde yineleyerek ikinci kez yinelenir .
Felix Kling

2
Splitlines () öğesinin varsayılan olarak bir yineleyici döndürmemesinin belirli bir nedeni var mı? Eğilimin genellikle bunu yinelemeler için yapmak olduğunu sanıyordum. Yoksa bu yalnızca dict.keys () gibi belirli işlevler için mi geçerli?
Cerno

Yanıtlar:


144

İşte üç olasılık:

foo = """
this is 
a multi-line string.
"""

def f1(foo=foo): return iter(foo.splitlines())

def f2(foo=foo):
    retval = ''
    for char in foo:
        retval += char if not char == '\n' else ''
        if char == '\n':
            yield retval
            retval = ''
    if retval:
        yield retval

def f3(foo=foo):
    prevnl = -1
    while True:
      nextnl = foo.find('\n', prevnl + 1)
      if nextnl < 0: break
      yield foo[prevnl + 1:nextnl]
      prevnl = nextnl

if __name__ == '__main__':
  for f in f1, f2, f3:
    print list(f())

Bunu ana komut dosyası olarak çalıştırmak, üç işlevin eşdeğer olduğunu doğrular. İle timeit(ve bir * 100için foodaha kesin ölçüm için önemli dizeleri almak için):

$ python -mtimeit -s'import asp' 'list(asp.f3())'
1000 loops, best of 3: 370 usec per loop
$ python -mtimeit -s'import asp' 'list(asp.f2())'
1000 loops, best of 3: 1.36 msec per loop
$ python -mtimeit -s'import asp' 'list(asp.f1())'
10000 loops, best of 3: 61.5 usec per loop

list()Yineleyicilerin yalnızca oluşturulmasını değil, çaprazlanmasını sağlamak için çağrıya ihtiyacımız olduğunu unutmayın .

IOW, saf uygulama o kadar hızlı ki komik bile değil: findÇağrılarla denememden 6 kat daha hızlı , bu da daha düşük seviyeli bir yaklaşımdan 4 kat daha hızlı.

Akılda tutulması gereken dersler: ölçüm her zaman iyi bir şeydir (ancak doğru olmalıdır); gibi dize yöntemleri splitlinesçok hızlı bir şekilde uygulanır; dizeleri çok düşük bir seviyede (özellikle +=çok küçük parçalardan oluşan döngülerle) programlayarak bir araya getirmek oldukça yavaş olabilir.

Düzenleme : @ Jacob'ın teklifi eklendi, diğerleri ile aynı sonuçları verecek şekilde biraz değiştirildi (bir satırdaki sondaki boşluklar tutulur), yani:

from cStringIO import StringIO

def f4(foo=foo):
    stri = StringIO(foo)
    while True:
        nl = stri.readline()
        if nl != '':
            yield nl.strip('\n')
        else:
            raise StopIteration

Ölçüm verir:

$ python -mtimeit -s'import asp' 'list(asp.f4())'
1000 loops, best of 3: 406 usec per loop

.findtemelli yaklaşım kadar iyi değil - yine de akılda tutmaya değer çünkü tek tek küçük hatalara daha az eğilimli olabilir ( f3yukarıdaki gibi +1 ve -1 oluşumlarını gördüğünüz herhangi bir döngü otomatik olarak Tek tek şüpheleri tetikler - ve bu tür ince ayarlardan yoksun olan ve bunlara sahip olması gereken birçok döngü de gerekir - ancak kodumun da çıktısını diğer işlevlerle kontrol edebildiğim için doğru olduğuna inanıyorum ').

Ancak bölünmüş tabanlı yaklaşım hala geçerli.

Bir kenara: muhtemelen daha iyi bir stil f4:

from cStringIO import StringIO

def f4(foo=foo):
    stri = StringIO(foo)
    while True:
        nl = stri.readline()
        if nl == '': break
        yield nl.strip('\n')

en azından biraz daha az ayrıntılı. Takip eden s'leri sıyırma ihtiyacı \nmaalesef whiledöngünün daha net ve daha hızlı değiştirilmesini yasaklıyor return iter(stri)(bu iterkısım Python'un modern sürümlerinde gereksizdir, inanıyorum ki 2.3 veya 2.4'ten beri ama aynı zamanda zararsızdır). Belki de denemeye değer:

    return itertools.imap(lambda s: s.strip('\n'), stri)

veya bunların varyasyonları - ama burada duruyorum çünkü bu temelde strip, en basit ve en hızlı olanı olan teorik bir alıştırma .


Ayrıca (line[:-1] for line in cStringIO.StringIO(foo))oldukça hızlıdır; neredeyse saf uygulama kadar hızlı, ama tam olarak değil.
Matt Anderson

Bu harika cevap için teşekkür ederim. Sanırım buradaki ana ders (python'da yeni olduğum için) timeitbir alışkanlık oluşturmaktır.
Björn Pollex

@Space, evet, zaman iyidir, performansı ne zaman önemserseniz kullanın (dikkatli bir şekilde kullandığınızdan emin olun, örneğin bu durumda listtüm ilgili kısımları gerçekten zamanlamak için bir çağrıya ihtiyaç duyma notuma bakın ! -).
Alex Martelli

6
Peki ya hafıza tüketimi? split()Listenin yapılarına ek olarak tüm bölümlerin bir kopyasını tutarak hafızayı açıkça performans için takas eder.
ivan_pozdeev

3
İlk başta gerçekten kafam karıştı çünkü zamanlama sonuçlarını uygulama ve numaralandırmanın tersi sırasına göre listelediniz. = P
jamesdlin

53

"Sonra ayrıştırıcıyla" derken neyi kastettiğinden emin değilim. Bölme işlemi tamamlandıktan sonra, dizginin başka geçişi yoktur , yalnızca bölünmüş dizeler listesinin bir geçişi vardır . İpinizin boyutu kesinlikle çok büyük olmadığı sürece, muhtemelen bunu başarmanın en hızlı yolu bu olacaktır. Python'un değişmez dizeler kullanması , her zaman yeni bir dizge oluşturmanız gerektiği anlamına gelir , bu nedenle bunun bir noktada yapılması gerekir.

Diziniz çok büyükse, dezavantaj bellek kullanımındadır: orijinal dizeye ve bellekte bölünmüş dizelerin bir listesine aynı anda sahip olacaksınız ve bu da gereken belleği iki katına çıkaracaktır. Yineleyici bir yaklaşım, "bölme" cezasını yine de ödese de, gerektiğinde bir dizge oluşturarak bunu kurtarabilir. Bununla birlikte, dizeniz o kadar büyükse, genellikle bölünmemiş dizenin bile bellekte olmasını önlemek istersiniz . Dizeyi bir dosyadan okumak daha iyi olur, bu da zaten satırlar halinde yineleme yapmanıza izin verir.

Ancak, halihazırda bellekte çok büyük bir dizeniz varsa, bir yaklaşım, bir dizeye dosya benzeri bir arabirim sunan StringIO'yu kullanmak olacaktır, buna satır bazında yinelemeye izin vermek de dahildir (dahili olarak sonraki satırsonunu bulmak için .find kullanarak). Daha sonra şunları elde edersiniz:

import StringIO
s = StringIO.StringIO(myString)
for line in s:
    do_something_with(line)

5
Not: python 3 ioiçin bunun için paketi kullanmanız gerekir , örneğin io.StringIOyerine kullanın StringIO.StringIO. Bkz docs.python.org/3/library/io.html
Attila123

Kullanımı StringIOaynı zamanda yüksek performanslı evrensel yeni satır işleme elde etmenin iyi bir yoludur.
martineau

3

Ben okursanız Modules/cStringIO.cdoğru, bu (biraz ayrıntılı rağmen) oldukça verimli olması gerekir:

from cStringIO import StringIO

def iterbuf(buf):
    stri = StringIO(buf)
    while True:
        nl = stri.readline()
        if nl != '':
            yield nl.strip()
        else:
            raise StopIteration

3

Normal ifade tabanlı arama bazen oluşturucu yaklaşımdan daha hızlıdır:

RRR = re.compile(r'(.*)\n')
def f4(arg):
    return (i.group(1) for i in RRR.finditer(arg))

2
Bu soru belirli bir senaryo hakkındadır, bu nedenle en yüksek puanlama cevabının yaptığı gibi basit bir kıyaslama göstermek faydalı olacaktır.
Björn Pollex

1

Sanırım kendi başınıza dönebilirsiniz:

def parse(string):
    retval = ''
    for char in string:
        retval += char if not char == '\n' else ''
        if char == '\n':
            yield retval
            retval = ''
    if retval:
        yield retval

Bu uygulamanın ne kadar verimli olduğundan emin değilim, ancak bu yalnızca dizinizi bir kez yineleyecektir.

Mmm, jeneratörler.

Düzenle:

Elbette, yapmak istediğiniz her tür ayrıştırma eylemini de eklemek isteyeceksiniz, ancak bu oldukça basit.


Uzun satırlar için oldukça verimsizdir ( +=parça en kötü durum O(N squared)performansına sahiptir, ancak birkaç uygulama hilesi mümkün olduğunda bunu düşürmeye çalışır).
Alex Martelli

Evet - bunu son zamanlarda öğreniyordum. Bir karakter listesine eklemek ve ardından ".join (karakterler) daha hızlı olur mu? Yoksa bu kendim üstlenmem gereken bir deney mi? ;)
Wayne Werner

lütfen kendinizi ölçün, öğretici - ve hem OP örneğindeki gibi kısa satırları hem de uzun olanları deneyin! -)
Alex Martelli

Kısa dizeler için (<~ 40 karakter) + = aslında daha hızlıdır, ancak en kötü durumu hızlı bir şekilde vurur. Daha uzun diziler için, .joinyöntem aslında O (N) karmaşıklığına benziyor. SO üzerinde yapılan özel karşılaştırmayı henüz bulamadığım için, bir soru stackoverflow.com/questions/3055477/… başlattım (şaşırtıcı bir şekilde benimkinden daha fazla yanıt aldı!)
Wayne Werner

0

Sondaki yeni satır karakteri de dahil olmak üzere satırlar üreten "bir dosya" üzerinde yineleme yapabilirsiniz. Bir dizeden "sanal dosya" oluşturmak için şunları kullanabilirsiniz StringIO:

import io  # for Py2.7 that would be import cStringIO as io

for line in io.StringIO(foo):
    print(repr(line))
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.