【问题标题】:What is the most efficient string concatenation method in Python?python中最有效的字符串连接方法是什么?
【发布时间】:2010-11-21 23:02:12
【问题描述】:

Python 中是否有任何有效的批量字符串连接方法(如 C# 中的 StringBuilder 或 Java 中的 StringBuffer)?我发现了以下方法here

  • 使用+ 进行简单连接
  • 使用字符串列表和join方法
  • 使用来自MutableString 模块的UserString
  • 使用字符数组和array 模块
  • 使用来自StringIO 模块的cStringIO

但是您的专家使用或建议什么,为什么?

[A related question here]

【问题讨论】:

  • 为了将已知片段连接成一个片段,Python 3.6 将具有f'' 格式字符串,这将比以前 Python 版本中的任何替代方法都快。

标签: python string


【解决方案1】:

您可能对此感兴趣:Guido 的An optimization anecdote。虽然值得记住的是,这是一篇旧文章,并且早于 ''.join 之类的东西的存在(尽管我猜 string.joinfields 或多或少相同)

在此基础上,array 模块可能是最快的,如果你能把你的问题硬塞进去的话。但是''.join 可能足够快,并且具有惯用语的优点,因此其他 python 程序员更容易理解。

最后,优化的黄金法则:除非你知道自己需要,否则不要优化,衡量而不是猜测。

您可以使用timeit 模块测量不同的方法。这可以告诉你哪个最快,而不是互联网上的随机陌生人猜测。

【讨论】:

  • 想要补充一点关于何时优化:确保针对最坏的情况进行测试。例如,我可以增加我的示例,以便我当前的代码从 0.17 秒运行到 170 秒。好吧,我想测试更大的样本量,因为那里的变化较小。
  • “在你知道你需要之前不要优化。”除非您只是使用名义上不同的习语,并且可以通过很少的额外工作来避免代码返工。
  • 你知道你需要的一个地方是面试(这总是一个很好的时间来加深你的理解)。不幸的是,我还没有找到任何关于此的现代文章。 (1) Java/C# String 在 2017 年还那么糟糕吗? (2)C++呢? (3) 现在讲述 Python 中最新和最伟大的案例,重点关注我们需要进行数百万次连接的情况。我们可以相信 join 会在线性时间内起作用吗?
  • “足够快”对于.join() 意味着什么?主要问题是,它是否 a) 创建一个用于连接的字符串的副本(类似于s = s + 'abc'),这需要 O(n) 运行时,或者 b) 只需附加到现有字符串而不创建副本,这需要 O (1)?
  • @CGFoX s = s + 'abc' 创建一个全新的str 对象,然后使s 引用它而不是s 引用的原始对象。如果您在循环中执行此操作,您将重复将s 的(越来越长的)值复制到一系列新对象中。但是,''.joinstr 类型“内部”运行。它只需要访问一次操作数的内容,就可以复制到预先分配的str 对象中,该对象的大小足以容纳结果。
【解决方案2】:

Python 3.6 改变了使用Literal String Interpolation 连接已知组件的字符串。

给定来自mkoistinen's answer 的测试用例,有字符串

domain = 'some_really_long_example.com'
lang = 'en'
path = 'some/really/long/path/'

竞争者是

  • f'http://{domain}/{lang}/{path}' - 0.151 µs

  • 'http://%s/%s/%s' % (domain, lang, path) - 0.321 µs

  • 'http://' + domain + '/' + lang + '/' + path - 0.356 µs

  • ''.join(('http://', domain, '/', lang, '/', path)) - 0.249 µs(请注意,构建一个恒定长度的元组比构建一个恒定长度的列表稍快)。

因此目前最短、最漂亮的代码也是最快的。

在 Python 3.6 的 alpha 版本中,f'' 字符串的实现是可能的最慢 - 实际上生成的字节码几乎等同于 ''.join() 的情况,对str.__format__ 进行了不必要的调用没有参数只会返回 self 不变。这些低效率的问题在 3.6 final 之前得到解决。

速度可以对比一下Python 2最快的方法,在我的电脑上是+拼接; 8 位字符串需要 0.203 µs,如果字符串都是 Unicode,则需要 0.259 µs。

【讨论】:

  • 这是“哪个最快”问题的实际答案。当前接受的答案过于固执己见,认为这是一种过早的优化,以至于它甚至不费心测试各种可用的技术。
【解决方案3】:

''.join(sequenceofstrings) 通常最有效——最简单、最快。

【讨论】:

  • @mshsayem,在 Python 中,序列可以是任何可枚举对象,甚至是函数。
  • 我非常喜欢 ''.join(sequence) 成语。生成逗号分隔的列表特别有用:', '.join([1, 2, 3]) 给出字符串 '1, 2, 3'
  • @mshsayem: "".join(chr(x) for x in xrange(65,91)) --- 在这种情况下,join 的参数是一个迭代器,通过生成器表达式创建。没有构建临时列表。
  • @balpha: 但是生成器版本比列表理解版本慢: C:\temp>python -mtimeit "''.join(chr(x) for x in xrange(65,91 ))" 100000 个循环,最好的 3:每个循环 9.71 微秒 C:\temp>python -mtimeit "''.join([chr(x) for x in xrange(65,91)])" 100000 个循环,最好的3:每个循环 7.1 微秒
  • @hughdbrown,是的,当您有空闲内存时,wazoo(典型的 timeit 情况)listcomp 可以比 genexp 更好地优化,通常优化 20-30%。当内存紧张的事情不同时——虽然很难及时重现!-)
【解决方案4】:

这取决于你在做什么。

在 Python 2.5 之后,使用 + 运算符的字符串连接非常快。如果您只是连接几个值,使用 + 运算符效果最好:

>>> x = timeit.Timer(stmt="'a' + 'b'")
>>> x.timeit()
0.039999961853027344

>>> x = timeit.Timer(stmt="''.join(['a', 'b'])")
>>> x.timeit()
0.76200008392333984

但是,如果你将一个字符串放在一个循环中,你最好使用列表连接方法:

>>> join_stmt = """
... joined_str = ''
... for i in xrange(100000):
...   joined_str += str(i)
... """
>>> x = timeit.Timer(join_stmt)
>>> x.timeit(100)
13.278000116348267

>>> list_stmt = """
... str_list = []
... for i in xrange(100000):
...   str_list.append(str(i))
... ''.join(str_list)
... """
>>> x = timeit.Timer(list_stmt)
>>> x.timeit(100)
12.401000022888184

...但请注意,在差异变得明显之前,您必须将相对较多的字符串放在一起。

【讨论】:

  • 1) 在您的第一次测量中,可能是列表构建需要时间。尝试使用元组。 2) CPython 的表现都很好,但是其他 Python 实现在使用 + 和 +=时表现得更差
【解决方案5】:

根据 John Fouhy 的回答,除非必须,否则不要优化,但如果您在这里提出这个问题,可能正是因为您必须。就我而言,我需要从字符串变量中组装一些 URL……快。我注意到(到目前为止)似乎没有人在考虑字符串格式方法,所以我想我会尝试一下,并且主要是出于轻微的兴趣,我想我会将字符串插值运算符扔在那里以获得更好的测量值。老实说,我认为这两种方法都不会叠加到直接的“+”操作或“.join()”中。但猜猜怎么了?在我的 Python 2.7.5 系统上,字符串插值运算符将它们全部统治,而 string.format() 的表现最差:

# concatenate_test.py

from __future__ import print_function
import timeit

domain = 'some_really_long_example.com'
lang = 'en'
path = 'some/really/long/path/'
iterations = 1000000

def meth_plus():
    '''Using + operator'''
    return 'http://' + domain + '/' + lang + '/' + path

def meth_join():
    '''Using ''.join()'''
    return ''.join(['http://', domain, '/', lang, '/', path])

def meth_form():
    '''Using string.format'''
    return 'http://{0}/{1}/{2}'.format(domain, lang, path)

def meth_intp():
    '''Using string interpolation'''
    return 'http://%s/%s/%s' % (domain, lang, path)

plus = timeit.Timer(stmt="meth_plus()", setup="from __main__ import meth_plus")
join = timeit.Timer(stmt="meth_join()", setup="from __main__ import meth_join")
form = timeit.Timer(stmt="meth_form()", setup="from __main__ import meth_form")
intp = timeit.Timer(stmt="meth_intp()", setup="from __main__ import meth_intp")

plus.val = plus.timeit(iterations)
join.val = join.timeit(iterations)
form.val = form.timeit(iterations)
intp.val = intp.timeit(iterations)

min_val = min([plus.val, join.val, form.val, intp.val])

print('plus %0.12f (%0.2f%% as fast)' % (plus.val, (100 * min_val / plus.val), ))
print('join %0.12f (%0.2f%% as fast)' % (join.val, (100 * min_val / join.val), ))
print('form %0.12f (%0.2f%% as fast)' % (form.val, (100 * min_val / form.val), ))
print('intp %0.12f (%0.2f%% as fast)' % (intp.val, (100 * min_val / intp.val), ))

结果:

# python2.7 concatenate_test.py
plus 0.360787868500 (90.81% as fast)
join 0.452811956406 (72.36% as fast)
form 0.502608060837 (65.19% as fast)
intp 0.327636957169 (100.00% as fast)

如果我使用更短的域和更短的路径,插值仍然会胜出。不过,如果字符串越长,差异就越大。

现在我有了一个不错的测试脚本,我还在 Python 2.6、3.3 和 3.4 下进行了测试,结果如下。在 Python 2.6 中,加号运算符是最快的!在 Python 3 上,加入胜出。注意:这些测试在我的系统上非常可重复。因此,“plus”在 2.6 上总是更快,“intp”在 2.7 上总是更快,而“join”在 Python 3.x 上总是更快。

# python2.6 concatenate_test.py
plus 0.338213920593 (100.00% as fast)
join 0.427221059799 (79.17% as fast)
form 0.515371084213 (65.63% as fast)
intp 0.378169059753 (89.43% as fast)

# python3.3 concatenate_test.py
plus 0.409130576998 (89.20% as fast)
join 0.364938726001 (100.00% as fast)
form 0.621366866995 (58.73% as fast)
intp 0.419064424001 (87.08% as fast)

# python3.4 concatenate_test.py
plus 0.481188605998 (85.14% as fast)
join 0.409673971997 (100.00% as fast)
form 0.652010936996 (62.83% as fast)
intp 0.460400978001 (88.98% as fast)

# python3.5 concatenate_test.py
plus 0.417167026084 (93.47% as fast)
join 0.389929617057 (100.00% as fast)
form 0.595661019906 (65.46% as fast)
intp 0.404455224983 (96.41% as fast)

经验教训:

  • 有时,我的假设完全错误。
  • 针对系统环境进行测试。您将在生产环境中运行。
  • 字符串插值还没有死!

tl;博士:

  • 如果您使用 2.6,请使用 + 运算符。
  • 如果您使用的是 2.7,请使用“%”运算符。
  • 如果您使用的是 3.x,请使用 ''.join()。

【讨论】:

  • 注意:对于 3.6+,文字字符串插值仍然更快:f'http://{domain}/{lang}/{path}'
  • 另外,.format()有三种形式,从快到慢依次为:"{}".format(x)"{0}".format(x)"{x}".format(x=x)
  • 真正的教训:当您的问题域很小时,例如组成短字符串,方法通常无关紧要。即使在重要的时候,例如你真的在构建一百万个字符串,开销通常更重要。这是担心错误问题的典型症状。仅当开销不显着时,例如当将整本书构建为字符串时,方法差异开始很重要。
  • 这是一个基于上述基准的示例,包括 f-strings gist.github.com/holmanb/84be00eab35477565cb95a1d62a741a9
【解决方案6】:

这在很大程度上取决于每次新连接后新字符串的相对大小。 使用+ 运算符,每次连接都会生成一个新字符串。如果中间字符串比较长,+ 会变得越来越慢,因为正在存储新的中间字符串。

考虑这种情况:

from time import time
stri=''
a='aagsdfghfhdyjddtyjdhmfghmfgsdgsdfgsdfsdfsdfsdfsdfsdfddsksarigqeirnvgsdfsdgfsdfgfg'
l=[]
#case 1
t=time()
for i in range(1000):
    stri=stri+a+repr(i)
print time()-t

#case 2
t=time()
for i in xrange(1000):
    l.append(a+repr(i))
z=''.join(l)
print time()-t

#case 3
t=time()
for i in range(1000):
    stri=stri+repr(i)
print time()-t

#case 4
t=time()
for i in xrange(1000):
    l.append(repr(i))
z=''.join(l)
print time()-t

结果

1 0.00493192672729

2 0.000509023666382

3 0.00042200088501

4 0.000482797622681

在 1&2 的情况下,我们添加一个大字符串,join() 的执行速度大约快了 10 倍。 案例 3&4 中,我们添加了一个小字符串,'+' 的执行速度略快

【讨论】:

    【解决方案7】:

    对于 python 3.8.6/3.9, 我不得不做一些肮脏的黑客攻击,因为 perfplot 给出了一些错误。这里假设x[0]ax[1]b

    大数据的图几乎相同。对于小数据,

    由 perfplot 拍摄,这是代码,大数据 == range(8),小数据 == range(4)。

    import perfplot
    
    from random import choice
    from string import ascii_lowercase as letters
    
    def generate_random(x):
        data = ''.join(choice(letters) for i in range(x))
        sata = ''.join(choice(letters) for i in range(x))
        return [data,sata]
    
    def fstring_func(x):
        return [ord(i) for i in f'{x[0]}{x[1]}']
    
    def format_func(x):
        return [ord(i) for i in "{}{}".format(x[0], x[1])]
    
    def replace_func(x):
        return [ord(i) for i in "|~".replace('|', x[0]).replace('~', x[1])]
    
    def join_func(x):
        return [ord(i) for i in "".join([x[0], x[1]])]
    
    perfplot.show(
        setup=lambda n: generate_random(n),
        kernels=[
            fstring_func,
            format_func,
            replace_func,
            join_func,
        ],
        n_range=[int(k ** 2.5) for k in range(4)],
    )
    

    当存在中等数据时,存在 4 个字符串 x[0]x[1]x[2]x[3] 而不是 2 个字符串:

    def generate_random(x):
        a =  ''.join(choice(letters) for i in range(x))
        b =  ''.join(choice(letters) for i in range(x))
        c =  ''.join(choice(letters) for i in range(x))
        d =  ''.join(choice(letters) for i in range(x))
        return [a,b,c,d]
    

    最好坚持使用 fstrings。 %s 的速度也类似于 .format()

    【讨论】:

      【解决方案8】:

      我遇到了一种情况,我需要一个大小未知的可附加字符串。这些是基准测试结果(python 2.7.3):

      $ python -m timeit -s 's=""' 's+="a"'
      10000000 loops, best of 3: 0.176 usec per loop
      $ python -m timeit -s 's=[]' 's.append("a")'
      10000000 loops, best of 3: 0.196 usec per loop
      $ python -m timeit -s 's=""' 's="".join((s,"a"))'
      100000 loops, best of 3: 16.9 usec per loop
      $ python -m timeit -s 's=""' 's="%s%s"%(s,"a")'
      100000 loops, best of 3: 19.4 usec per loop
      

      这似乎表明 '+=' 是最快的。 skymind 链接的结果有点过时了。

      (我意识到第二个示例不完整,需要加入最终列表。但这确实表明,简单地准备列表比字符串 concat 花费的时间更长。)

      【讨论】:

      • 第三次和第四次测试的时间不到 1 秒。为什么你得到这么高的时间? pastebin.com/qabNMCHS
      • @ronnieaka:他的所有测试时间都在 1 秒以内。他在第 3 次和第 4 次获得 >1 µs,而你没有。我在这些测试中也得到了较慢的时间(在 Python 2.7.5、Linux 上)。可能是 CPU、版本、构建标志,谁知道呢。
      • 这些基准测试结果毫无用处。尤其是第一种情况,它不做任何字符串连接,只是原封不动地返回第二个字符串值。
      【解决方案9】:

      一年后,让我们用 python 3.4.3 测试 mkoistinen 的答案:

      • 加上 0.963564149000(最快 95.83%)
      • 加入 0.923408469000(100.00% 一样快)
      • 表格 1.501130934000(最快 61.51%)
      • intp 1.019677452000(最快 90.56%)

      什么都没有改变。加入仍然是最快的方法。就可读性而言,intp 可以说是最佳选择,但您可能仍希望使用 intp。

      【讨论】:

      • 也许它可能是对 mkoistinen 答案的补充,因为它有点缺乏完整的答案(或至少添加您正在使用的代码)。
      【解决方案10】:

      可能“Python 3.6 中的新 f 字符串”是连接字符串的最有效方式。

      使用 %s

      >>> timeit.timeit("""name = "Some"
      ... age = 100
      ... '%s is %s.' % (name, age)""", number = 10000)
      0.0029734770068898797
      

      使用 .format

      >>> timeit.timeit("""name = "Some"
      ... age = 100
      ... '{} is {}.'.format(name, age)""", number = 10000)
      0.004015227983472869
      

      使用 f

      >>> timeit.timeit("""name = "Some"
      ... age = 100
      ... f'{name} is {age}.'""", number = 10000)
      0.0019175919878762215
      

      来源:https://realpython.com/python-f-strings/

      【讨论】:

        【解决方案11】:

        受@JasonBaker 基准测试的启发,这里有一个简单的比较10 个"abcdefghijklmnopqrstuvxyz" 字符串,表明.join() 更快;即使变量有这么微小的增加:

        级联

        >>> x = timeit.Timer(stmt='"abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz"')
        >>> x.timeit()
        0.9828147209324385
        

        加入

        >>> x = timeit.Timer(stmt='"".join(["abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz"])')
        >>> x.timeit()
        0.6114138159765048
        

        【讨论】:

        【解决方案12】:

        对于 小集合 短字符串(即 2 或 3 个不超过几个字符的字符串),plus 仍然可行快点。在 Python 2 和 3 中使用 mkoistinen 的精彩脚本:

        plus 2.679107467004 (100.00% as fast)
        join 3.653773699996 (73.32% as fast)
        form 6.594011374000 (40.63% as fast)
        intp 4.568015249999 (58.65% as fast)
        

        因此,当您的代码执行大量单独的小连接时,plus 是首选方式 if 速度至关重要。

        【讨论】:

          猜你喜欢
          • 2013-05-17
          • 1970-01-01
          • 1970-01-01
          • 2010-09-06
          • 2015-07-14
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多