代码之家  ›  专栏  ›  技术社区  ›  Tom Gruff

在Python中,如何在不创建中间列表的情况下拆分字符串并重新连接它?

  •  9
  • Tom Gruff  · 技术社区  · 15 年前

    假设我有如下情况:

    dest = "\n".join( [line for line in src.split("\n") if line[:1]!="#"] )
    

    (即从多行字符串中去掉以#开头的任何行 src

    src公司 很大,所以我想 .split()

    澄清 :这是由于我的代码内存不足引起的。我知道有一些方法可以完全重写我的代码来解决这个问题,但问题是关于Python的:是否有一个split()版本(或等效的习惯用法),它的行为类似于生成器,因此不会生成另一个工作副本 src公司 ?

    5 回复  |  直到 15 年前
        1
  •  5
  •   John La Rooy    15 年前

    下面是一种使用itertools进行一般类型拆分的方法

    >>> import itertools as it
    >>> src="hello\n#foo\n#bar\n#baz\nworld\n"
    >>> line_gen = (''.join(j) for i,j in it.groupby(src, "\n".__ne__) if i)
    >>> '\n'.join(s for s in line_gen if s[0]!="#")
    'hello\nworld'
    

    groupby单独处理src中的每个字符,因此性能可能并不理想,但它确实避免了创建任何中间的大型数据结构

    >>> src="hello\n#foo\n#bar\n#baz\nworld\n"
    >>>
    >>> def isplit(s, t): # iterator to split string s at character t
    ...     i=j=0
    ...     while True:
    ...         try:
    ...             j = s.index(t, i)
    ...         except ValueError:
    ...             if i<len(s):
    ...                 yield s[i:]
    ...             raise StopIteration
    ...         yield s[i:j]
    ...         i = j+1
    ...
    >>> '\n'.join(x for x in isplit(src, '\n') if x[0]!='#')
    'hello\nworld'
    

    re 有一个名为 finditer ,也可用于此目的

    >>> import re
    >>> src="hello\n#foo\n#bar\n#baz\nworld\n"
    >>> line_gen = (m.group(1) for m in re.finditer("(.*?)(\n|$)",src))
    >>> '\n'.join(s for s in line_gen if not s.startswith("#"))
    'hello\nworld'
    

    比较性能是OP尝试真实数据的一个练习

        2
  •  5
  •   Matthew Flaschen    15 年前
    buffer = StringIO(src)
    dest = "".join(line for line in buffer if line[:1]!="#")
    

    当然,如果你使用 StringIO 贯穿始终。它的工作原理与文件基本相同。您可以查找、读取、写入、迭代(如图所示)等。

        3
  •  4
  •   Mark Byers    15 年前

    dest = "\n".join(line for line in src.split("\n") if line[:1]!="#")
    

    这个非常小的更改避免了在代码中构建两个临时列表中的一个,并且不需要您付出任何努力。

    避免临时构造两个列表的完全不同的方法是使用正则表达式:

    import re
    regex = re.compile('^#.*\n?', re.M)
    dest = regex.sub('', src)
    

    这不仅可以避免创建临时列表,还可以避免为输入中的每一行创建临时字符串。以下是建议解决方案的一些性能度量:

    init = r'''
    import re, StringIO
    regex = re.compile('^#.*\n?', re.M)
    src = ''.join('foo bar baz\n' for _ in range(100000))
    '''
    
    method1 = r'"\n".join([line for line in src.split("\n") if line[:1] != "#"])'
    method2 = r'"\n".join(line for line in src.split("\n") if line[:1] != "#")'
    method3 = 'regex.sub("", src)'
    method4 = '''
    buffer = StringIO.StringIO(src)
    dest = "".join(line for line in buffer if line[:1] != "#")
    '''
    
    import timeit
    
    for method in [method1, method2, method3, method4]:
        print timeit.timeit(method, init, number = 100)
    

    结果:

     9.38s   # Split then join with temporary list
     9.92s   # Split then join with generator
     8.60s   # Regular expression
    64.56s   # StringIO
    

    如您所见,正则表达式是最快的方法。

    从你的评论我可以看出你不是 有兴趣避免创建临时对象。您真正想要的是减少程序的内存需求。临时对象不一定会影响程序的内存消耗,因为Python擅长快速清除内存。问题来自于对象在内存中的持续时间超过了它们需要的时间,所有这些方法都有这个问题。

    如果你的内存仍然不足,那么我建议你不应该完全在内存中进行这个操作。相反,将输入和输出存储在磁盘上的文件中,并以流方式从中读取。这意味着您从输入中读取一行,向输出中写入一行,读取一行,写入一行,等等。这将创建许多临时字符串,但即使如此,它也几乎不需要内存,因为您一次只需要处理一个字符串。

        4
  •  2
  •   loevborg    15 年前

    如果我正确理解您关于“对split()的更通用调用”的问题,您可以使用 re.finditer ,就像这样:

    output = ""
    
    for i in re.finditer("^.*\n",input,re.M):
        i=i.group(0).strip()
        if i.startswith("#"):
            continue
        output += i + "\n"
    

    在这里,您可以用更复杂的东西替换正则表达式。

        5
  •  1
  •   Andrew Jaffe    15 年前