在字符串行上迭代

2024-03-29 15:58:58 发布

您现在位置:Python中文网/ 问答频道 /正文

我有一个多行字符串定义如下:

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

这个字符串用作我正在编写的解析器的测试输入。parser函数接收一个file对象作为输入并对其进行迭代。它还直接调用next()方法来跳过行,所以我真的需要一个迭代器作为输入,而不是iterable。 我需要一个迭代器,它可以像file对象那样遍历字符串的每一行,对象可以遍历文本文件的每一行。我当然可以这样做:

lineiterator = iter(foo.splitlines())

有更直接的方法吗?在这种情况下,字符串必须遍历一次进行拆分,然后由解析器再次遍历。这在我的测试用例中并不重要,因为那里的字符串很短,我只是出于好奇而问。Python有很多有用且高效的内置组件,但是我找不到任何适合这种需要的东西。


Tags: 对象方法函数字符串parser解析器string定义
3条回答

我不知道你说的“然后再由解析器”是什么意思。拆分完成后,不再遍历字符串,只遍历被拆分字符串的列表。这可能是实现这一点的最快方法,只要字符串的大小不是非常大。python使用不可变字符串的事实意味着您必须始终创建一个新字符串,因此无论如何这必须在某个时刻完成。

如果字符串非常大,缺点是内存使用:同时在内存中有原始字符串和拆分字符串列表,所需的内存将增加一倍。迭代器方法可以帮您节省这一点,根据需要构建一个字符串,尽管它仍然要支付“拆分”的代价。但是,如果字符串太大,则通常要避免内存中包含未拆分的字符串。最好只是从文件中读取字符串,它已经允许您以行的形式遍历它。

但是,如果内存中已经有一个巨大的字符串,一种方法是使用StringIO,它为字符串提供类似文件的接口,包括允许按行迭代(内部使用.find查找下一个换行符)。然后你得到:

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

有三种可能性:

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

运行这个作为主脚本确认这三个函数是等价的。使用timeit(和* 100用于foo以获得更精确测量所需的大量字符串):

$ 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()调用来确保遍历迭代器,而不仅仅是构建迭代器。

听着,这种简单的实现要快得多,甚至一点也不好笑:比我尝试的find调用快6倍,而这又比低级方法快4倍。

要记住的教训:度量总是一件好事(但必须是准确的);像splitlines这样的字符串方法以非常快的方式实现;通过在非常低的级别编程(特别是通过非常小的片段的+=循环)将字符串组合在一起可能会非常慢。

编辑:添加了@Jacob的建议,稍加修改以获得与其他建议相同的结果(保留一行的尾随空白),即:

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

测量给出:

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

还不如基于.find的方法——仍然值得记住,因为它可能不太容易被一个bug所忽略(任何出现+1和-1的循环,就像上面的f3,都应该由一个怀疑自动触发——许多缺少这种调整并应该有它们的循环也应该如此——尽管相信我的代码也是正确的,因为我可以用其他函数检查它的输出。

但基于拆分的方法仍然适用。

旁白:可能更好的f4样式是:

from cStringIO import StringIO

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

至少,不那么冗长了。遗憾的是,删除尾部\n的需要禁止用return iter(stri)更清晰、更快地替换while循环(我相信,在现代版本的Python中,iter部分是多余的,从2.3或2.4开始,但它也是无害的)。或许值得一试:

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

或者它的变体——但是我在这里停下来,因为这几乎是一个基于strip的、最简单和最快的理论练习。

如果我正确地阅读了Modules/cStringIO.c,这应该非常有效(尽管有些冗长):

from cStringIO import StringIO

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

相关问题 更多 >