在字符串的行上迭代

我有一个这样定义的多行字符串:

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

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

lineiterator = iter(foo.splitlines())

还有更直接的方法吗?在这种情况下,字符串必须为分割遍历一次,然后再由解析器遍历一次。在我的测试用例中,这并不重要,因为字符串在那里非常短,我只是出于好奇问一下。Python 有如此多有用和高效的内置功能,但是我找不到任何适合这种需要的内置功能。

163625 次浏览

我想你可以自己卷:

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

我不确定这个实现的效率如何,但是它只在字符串上迭代一次。

发电机。

编辑:

当然,您也可以添加任何类型的解析操作,但是这非常简单。

如果我正确地阅读了 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

这里有三种可能性:

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(和 * 100foo得到更精确测量的实质性字符串) :

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

IOW,幼稚的实现是如此之快,以至于一点都不好笑: 比我尝试使用 find调用快6倍,而 find调用又是低级方法的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的方法好,但是仍然值得记住,因为它可能不太容易出现小的 off-by-one 错误(任何出现 + 1和 -1的循环,就像我的 f3一样,应该会自动触发 off-by-one 怀疑——许多缺乏这种调整的循环也应该有这样的怀疑——尽管我相信我的代码也是正确的,因为我可以用其他函数检查它的输出)。

但是基于分割的方法仍然占主导地位。

顺便说一句,对 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循环(iter部分在 Python 的现代版本中是多余的,我相信从2.3或2.4开始,但它也是无害的)。也许值得一试:

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

或者它们的变化-,但是我在这里停下来,因为这基本上是一个理论练习,基于 strip的,最简单和最快的,一个。

我不明白您说的“然后再由解析器”是什么意思。在分割完成之后,就没有对 绳子的进一步遍历了,只有对分割字符串的 名单的遍历。只要字符串的大小不是绝对的巨大,这可能是实现这一点的最快方法。Python 使用不可变字符串的事实意味着 必须的总是创建一个新字符串,因此无论如何都要在某个时候这样做。

如果您的字符串非常大,那么缺点是内存使用: 您将在内存中同时拥有原始字符串和拆分字符串列表,这将使所需的内存增加一倍。迭代器方法可以根据需要构建字符串,从而避免这种情况,尽管它仍然要支付“分割”的代价。但是,如果字符串那么大,那么通常要避免甚至 不分开字符串存在于内存中。最好只从文件中读取字符串,因为它已经允许您以行的形式对其进行迭代。

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

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

基于正则表达式的搜索有时比生成器方法更快:

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

您可以在“ a file”上迭代,该文件生成行,包括后面的换行符。要用字符串创建一个“虚拟文件”,可以使用 StringIO:

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


for line in io.StringIO(foo):
print(repr(line))