Python 中最有效的字符串串联方法是什么?

Python 中是否有一种有效的海量字符串串联方法(如 C # 中的 StringBuilder < a href = “ https://Learn.microsoft.com/en-us/dotnet/standard/base-types/stringBuilder”rel = “ noReferrer”> StringBuilder 或 Java 中的 StringBuffer) ?

我发现了以下方法 给你:

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

应该使用什么和为什么使用?

(这里有一个相关的问题。)

165697 次浏览

''.join(sequenceofstrings)通常工作得最好——最简单、最快。

你可能会对这个感兴趣: 一个优化的轶事 by 圭多。虽然这也是值得记住的,这是一个老文章,它早于像 ''.join这样的东西的存在(虽然我猜 string.joinfields或多或少是相同的)

凭借这个优势,如果你能把你的问题硬塞进 array模块 ,它将是最快的。但是 ''.join可能是 够快了,它具有惯用的优点,因此对于其他 Python 程序员来说更容易理解。

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

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

这取决于你在做什么。

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

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


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

然而,如果你在一个循环中放置一个字符串,你最好使用 list join 方法:

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

但请注意,你必须把相对较多的字符串组合在一起,才能发现其中的差异。

我遇到了一个情况,我需要一个可附加的未知大小的字符串。以下是基准测试结果(python2.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 链接的结果有点过时了。

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

受到 Jason Baker 的基准的启发,这里有一个简单的例子,比较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

它在很大程度上取决于每次新串联后新字符串的相对大小。

使用 +操作符,对于每个串联,都会生成一个新的字符串。如果中间字符串相对较长,那么 +就会变得越来越慢,因为新的中间字符串正在被存储。

考虑一下这个案例:

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

结果

10.00493192672729

20.000509023666382

30.00042200088501

40.000482797622681

对于1和2,我们添加一个大字符串,join ()的执行速度要快10倍左右。 在情况3和4中,我们添加一个小字符串,“ +”的执行速度稍微快一些

根据 John Fouhy 的回答,除非必须,否则不要优化,但是如果你在这里问这个问题,这可能正是因为你 必须的

在我的例子中,我需要从字符串变量中快速组装一些 URL。我注意到(到目前为止)似乎没有人在考虑字符串格式方法,所以我想我应该尝试一下,主要是出于兴趣,我想我应该把字符串插值运算符放在那里,以备不时之需。

老实说,我不认为这两个操作中的任何一个会堆叠成直接的“ +”操作或者“。加入()。但你猜怎么着?在我的 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), ))

结果:

# Python 2.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中,join 胜出。注意: 这些测试在我的系统上是非常可重复的。因此,‘ plus’在2.6上总是更快,‘ intp’在2.7上总是更快,而‘ join’在 Python 3.x 上总是更快。

# Python 2.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)


# Python 3.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)


# Python 3.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)


# Python 3.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)

经验教训:

  • 有时候,我的假设是完全错误的。
  • 针对系统环境进行测试。
  • 字符串插值还没死!

医生:

  • 如果使用 Python 2.6,请使用’+’运算符。
  • 如果使用 Python 2.7,请使用“%”运算符。
  • 如果您正在使用 Python 3.x,请使用“ . join ()。

一年后,让我们用 Python 3.4.3测试 Mkoistinen 的回答:

  • 加0.963564149000(同样快的95.83%)
  • 加入0.923408469000(速度为100.00%)
  • 表格1.501130934000(速度为61.51%)
  • Intp 1.019677452000(同样快的90.56%)

什么都没变。加入仍然是最快的方法。由于字符串插值(英特普)可以说是可读性方面的最佳选择,您可能还是希望使用字符串插值。

如果您事先知道所有组件,那么可以使用 Python 3.6中引入的 字符串插值字符串插值,也称为 f-字符串格式化字符串

给定来自 Mkoistinen 的回答的测试用例,使用字符串

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

竞争者及其在 Linux 上使用 Python 3.6的计算机上的执行时间(按 IPython 和 timeit 模块计时)如下

  • < p > f'http://{domain}/{lang}/{path}'-< 强 > 0.151 μs

  • < p > 'http://%s/%s/%s' % (domain, lang, path)-0.321 μs

  • < p > 'http://' + domain + '/' + lang + '/' + path-0.356微秒

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

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


这个速度可以和 Python 2中最快的方法进行比较,在我的计算机上是 +连接; 如果字符串都是 Unicode,则需要使用 < strong > 0.203 μs 和 < strong > 0.259 μs。

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

对于 短弦乐器很小的一组(即2或3个不超过几个字符的字符串) ,还有仍然快得多。在 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)

所以当你的代码在做大量独立的小连接时,加上 如果的速度是至关重要的首选方式。

也许“ Python 3.6中的 new 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

更新: Python 3.11对% 格式进行了一些优化,不过还是坚持使用 f 字符串比较好。

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

Performance

对于大数据,曲线图几乎是一样的,

Performance 2

使用 Perfplot,这是代码,大数据 = = 范围(8) ,小数据 = = 范围(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)],
)

当存在介质数据时,四个字符串是 x[0]x[1]x[2]x[3],而不是两个字符串:

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]

Performance 3

最好坚持使用 F 弦。而且 % s的速度与 。格式()相似。