【问题标题】:fast string modification in pythonpython中的快速字符串修改
【发布时间】:2009-10-02 09:06:17
【问题描述】:

这部分是一个理论问题:

我有一个字符串(比如 UTF-8),我需要修改它,使每个字符(不是字节)变成 2 个字符,例如:

"Nissim" becomes "N-i-s-s-i-m-"


"01234" becomes "0a1b2c3d4e" 

等等。 我怀疑循环中的幼稚连接会太昂贵(这是瓶颈,这应该一直发生)。

我要么使用一个数组(预先分配),要么尝试制作我自己的 C 模块来处理这个问题。

有人对这种事情有更好的想法吗?

(请注意,问题始终与多字节编码有关,对于 UTF-8 也必须解决),

哦,它的 Python 2.5,所以这里没有闪亮的 Python 3 东西可用。

谢谢

【问题讨论】:

  • “太贵”是什么意思?你觉得这种天真的方法有多昂贵?
  • 从未对其进行基准测试,但我怀疑内存将被分配、释放和重新分配,N 次输入中的 N 个字符。
  • 立即对其进行分析。 Python 优化了字符串操作。不要永远假设某些东西如果没有被描述就太昂贵了。
  • @gnosis - 请记住,使用诸如 Python 之类的分代垃圾收集器,分配和释放短期对象实际上非常快。这方面的假设可能很危险。

标签: python string


【解决方案1】:

@gnosis,当心所有好心的回应者说你应该衡量时间:是的,你应该(因为程序员的直觉往往不符合性能),但是衡量一个单一的这种情况,就像目前为止提供的所有timeit 示例一样,忽略了一个关键的考虑因素——big-O

您的直觉是正确的:一般而言(在极少数特殊情况下,最近的 Python 版本可以稍微优化一些东西,但不会延伸很远),通过循环构建字符串+= 的碎片(或reduce 等)必须是O(N**2),因为许多中间对象分配和这些对象内容的不可避免的重复复制;加入、正则表达式和上述答案中未提及的第三个选项(cStringIO.StringIO 实例的write 方法)是O(N) 解决方案,因此除非您碰巧知道 确保您要操作的字符串的长度有适度的上限。

那么,如果有的话,您正在处理的字符串的长度上限是多少?如果您能给我们一个想法,可以在代表性感兴趣的长度范围内运行基准测试(例如,“通常少于 100 个字符,但在某些情况下可能只有几千个字符" 将是这个性能评估的一个很好的规范:IOW,它不需要非常精确,只是表明你的问题空间)。

我还注意到,似乎没有人遵循您规范中的一个关键和困难点:字符串是 Python 2.5 多字节、UTF-8 编码、strs,并且插入必须仅在每个“完整字符”之后发生, not 在每个 byte 之后。每个人似乎都在“在 str 上循环”,它给出了每个字节,而不是您明确指定的每个 字符

在多字节编码字节str 中“循环字符”确实没有好的、快速的方法;最好的办法是.decode('utf-8'),给出一个 unicode 对象——处理 unicode 对象(循环 do 正确地遍历字符!),然后在最后返回.encode。到目前为止,通常最好的方法是在整个代码的核心中仅使用 not 编码 strs 的 unicode 对象;仅在 I/O 上对字节字符串进行编码和解码(如果必须,因为您需要与仅支持字节字符串而不支持正确 Unicode 的子系统进行通信)。

因此,我强烈建议您考虑这种“最佳方法”并相应地重组您的代码:unicode 无处不在,除非在必要时才可以对其进行编码/解码的边界。对于“处理”部分,你会更喜欢 unicode 对象,而不是拖着笨拙的多字节编码字符串!-)

编辑:忘记评论您提到的一种可能的方法——array.array。这确实是 O(N) if 你只是 追加 到你正在构建的新数组的末尾(一些追加会使数组增长到超过以前分配的容量,因此需要重新分配和复制数据,但是,就像列表一样,中等指数的过度分配策略允许 append摊销 O(1),因此 N 追加为 O(N))。

但是,通过中间重复的insert 操作来构建一个数组(同样,就像一个列表)是O(N**2),因为每个 O(N) 插入必须移动所有 O(N)以下项目(假设先前存在的项目的数量和新插入的项目的数量相互成比例,这似乎是您的具体要求的情况)。

所以,array.array('u') 加上重复的appends(not inserts!-)是可以解决您的问题的第四种 O(N) 方法(在除了我已经提到的三个之外:joinrecStringIO) -- 那些 一旦你明确了感兴趣的长度范围,就值得进行基准测试,因为我上面提到过。

【讨论】:

  • 亚历克斯,很好的回应!考虑到 array.array 方法 - 它应该只有一个分配总数,因为我知道我将在输出时有多少个字符。到目前为止,我将测试这个加 re 和 zip - zip 可能会让 python 有机会正确处理输入的分配和故障。将发布结果。
  • @gnosis,创建一次数组(例如,如果这是您需要附加到其他每个字符的字符,则用'-'填充它)然后分配给它的一些项目是有希望的。但是您感兴趣的字符串的长度分布是多少?
  • @alex-martelli,长度分布和你猜的差不多——我会说大部分时间(比如 80%)低于 200 个字符。为什么填充很重要?我只需分配一次内存,然后通过简单的分配填充数据。如果我可以允许输出为 UTF-16,那将是一场爆炸……
  • @gnosis,没有“分配”一个数组而不用一些东西填充它。因此,您不妨从一开始就用正确的字符填充它,例如x=array.array('U', (2*len(s))*'-') 分配它用破折号填充,然后x[::2]=array.array('U', x),这可能是最快的方法(或者如果您需要稍微不同的东西,则可以使用其他切片分配)。您在内部确实需要 unicode,但最终的 .encode('utf-8') 不应该让你减慢太多(输入时的初始 .decode 也不应该)。
【解决方案2】:

尝试使用re module 构建结果。它将在引擎盖下进行令人讨厌的连接,因此性能应该没问题。示例:

 import re
 re.sub(r'(.)', r'\1-', u'Nissim')

 count = 1
 def repl(m):
     global count
     s = m.group(1) + unicode(count)
     count += 1
     return s
 re.sub(r'(.)', repl, u'Nissim')

【讨论】:

  • 我还没有测试过,但我认为 re 模块的性能最低,而且在这里有点矫枉过正
  • 因为你没有测试,所以你有 90% 的机会是错的 :)
【解决方案3】:

这可能是一个 python 有效的解决方案:

s1="Nissim"
s2="------"
s3=''.join([''.join(list(x)) for x in zip(s1,s2)])

【讨论】:

  • 冗余的整个列表创建会减慢它的速度。将 [] 放在外部 .join() 中,并使用 itertools.izip 而不是 zip。
  • ''.join(i+j for i, j in zip(s1, s2)) 就足够了
  • @SlientHost:实际上你的解决方案比较慢,看我的回答
  • @kaizer.se - 这是一个值得分析或计时的建议。请记住,zip 是一个内置函数,因此调用起来比 izip 快。我不知道哪个可能更快,但我知道不要相信我在这方面的直觉。 :-)
  • 我没有看到你在哪里测试了 my 解决方案,Anurag
【解决方案4】:

你有没有测试过它有多慢或者你需要多快,我认为这样的东西就足够快了

s = u"\u0960\u0961"
ss = ''.join(sum(map(list,zip(s,"anurag")),[]))

所以尝试最简单,如果还不够,请尝试改进它,C 模块应该是最后的选择

编辑:这也是最快的

import timeit

s1="Nissim"
s2="------"

timeit.f1=lambda s1,s2:''.join(sum(map(list,zip(s1,s2)),[]))
timeit.f2=lambda s1,s2:''.join([''.join(list(x)) for x in zip(s1,s2)])
timeit.f3=lambda s1,s2:''.join(i+j for i, j in zip(s1, s2))

N=100000

print "anurag",timeit.Timer("timeit.f1('Nissim', '------')","import timeit").timeit(N)
print "dweeves",timeit.Timer("timeit.f2('Nissim', '------')","import timeit").timeit(N)
print "SilentGhost",timeit.Timer("timeit.f3('Nissim', '------')","import timeit").timeit(N)

输出是

anurag 1.95547590546
dweeves 2.36131184271
SilentGhost 3.10855625505

【讨论】:

  • 这根本不公平比较,看看我关于如何改进这个答案的建议。它会比你的快两倍。
  • @SlientGhost 我已经添加了你的解决方案,它是最慢的
  • hmmm 是 py2.5 和 py3 的区别还是测试风格?
  • 我先用你的方法测试过,结果没有改变
  • 我猜 SilentGhost 解决方案是最快的:anurag 0.775900713444 dweeves 0.740725330615 SilentGhost 0.599001875506 我在 Windows 上有 Python 2.6
【解决方案5】:

这是我的时间安排。注意,是py3.1

>>> s1
'Nissim'
>>> s2 = '-' * len(s1)
>>> timeit.timeit("''.join(i+j for i, j in zip(s1, s2))", "from __main__ import s1, s2")
3.5249209707199043
>>> timeit.timeit("''.join(sum(map(list,zip(s1,s2)),[]))", "from __main__ import s1, s2")
5.903614027402
>>> timeit.timeit("''.join([''.join(list(x)) for x in zip(s1,s2)])", "from __main__ import s1, s2")
6.04072124013328
>>> timeit.timeit("''.join(i+'-' for i in s1)", "from __main__ import s1, s2")
2.484378367653335
>>> timeit.timeit("reduce(lambda x, y : x+y+'-', s1, '')", "from __main__ import s1; from functools import reduce")
2.290644129319844

【讨论】:

    【解决方案6】:

    使用减少。

    >>> str = "Nissim"
    >>> reduce(lambda x, y : x+y+'-', str, '')
    'N-i-s-s-i-m-'
    

    数字也一样,只要你知道哪个字符映射到哪个字符。 [dict可以很方便]

    >>> mapper = dict([(repr(i), chr(i+ord('a'))) for i in range(9)])
    >>> str1 = '0123'
    >>> reduce(lambda x, y : x+y+mapper[y], str1, '')
    '0a1b2c3d'
    

    【讨论】:

    • 我必须看到数字才能相信这是一个快速的解决方案。 Python 中函数调用的开销实际上可能相当高。
    • @Jason: 奇怪的是,reduce 到目前为止更快
    【解决方案7】:
    string="™¡™©€"
    unicode(string,"utf-8")
    s2='-'*len(s1)
    ''.join(sum(map(list,zip(s1,s2)),[])).encode("utf-8")
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-03-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-11-01
      • 1970-01-01
      相关资源
      最近更新 更多