Python字符串拼接性能优化

17

网络上有很多关于Python性能的文章。你第一眼看到的是不应该使用'+'来连接字符串;避免使用s1 + s2 + s3,而是使用str.join

我尝试了以下操作:将两个字符串作为目录路径的一部分进行连接,有三种方法:

  1. '+' 这是我不应该使用的方法
  2. str.join
  3. os.path.join

这是我的代码:

import os, time

s1 = '/part/one/of/dir'
s2 = 'part/two/of/dir'
N = 10000

t = time.clock()
for i in xrange(N):
    s = s1 + os.sep + s2
print time.clock() - t

t = time.clock()
for i in xrange(N):
    s = os.sep.join((s1, s2))
print time.clock() - t

t = time.clock()
for i in xrange(N):
    s = os.path.join(s1, s2)
print time.clock() - t

以下是结果(在 Windows XP 上使用 Python 2.5):

0.0182201927899
0.0262544541275
0.120238186697

难道不该正好相反吗?

1
如果我可以的话,我建议将您的问题标题重命名为“Python字符串连接性能”,这样对于可能会提交重复问题的人来说更加明显。 - Eddie Parker
1
另外,有点跑题了,但你可能想看看 'timeit' 模块来进行计时。 - Eddie Parker
请提供一份参考资料,说明“不应使用'+'来连接字符串:避免使用s1+s2+s3,而应该使用str.join”。我想这个建议肯定有一些背景信息被省略了。 - S.Lott
Python维基足够好吗?(http://wiki.python.org/moin/PythonSpeed/PerformanceTips#StringConcatenation) <cite> 避免:out = "<html>" + head + prologue + query + tail + "</html>" </cite> 这就是为什么我知道s1+s2+s3不好。感谢提问,现在我明白了我的错误。 - Danny
这是一篇有些陈旧的文章(2004年),但它提供了Python中各种字符串拼接习惯用法的很好比较。链接为:http://www.skymind.com/~ocrow/python_string/ - harijay
Python 3.6 将实现 PEP 498 字符串字面插值从那时起它将是最快的 - Antti Haapala -- Слава Україні
7个回答

14

字符串拼接的性能问题大多是渐进性能的问题,因此当您连接许多长字符串时,差异变得最显著。

在您的示例中,您执行了许多次相同的连接。您没有构建任何长字符串,可能 Python 解释器正在优化您的循环。这解释了为什么当您转换为 str.joinpath.join 时,时间会增加 - 它们是更复杂的函数,不容易被简化。(os.path.join 在连接之前会对字符串进行许多检查,以查看它们是否需要以任何方式重写。这为实现可移植性牺牲了一些性能。)

顺便说一下,由于文件路径通常不是很长,因此出于可移植性的考虑,您几乎肯定希望使用 os.path.join。如果连接的性能成为问题,则您正在处理非常奇怪的文件系统操作。


7
这篇建议是关于连接许多字符串的。为了计算s = s1 + s2 + ... + sn,有两种方法: 1. 使用+号。创建新字符串s1+s2,然后创建新字符串s1+s2+s3,以此类推,因此涉及大量的内存分配和复制操作。实际上,s1被复制n-1次,s2被复制n-2次,等等。 2. 使用"".join([s1, s2, ..., sn])。连接在一次遍历中完成,并且每个字符串中的每个字符只复制一次。
在你的代码中,每次迭代都调用join,所以就像使用+一样。正确的方法是将项目收集到数组中,然后在其上调用join。

5

确实不应该使用“+”符号。您的示例非常特殊。请尝试使用以下代码:

s1 = '*' * 100000
s2 = '+' * 100000

然后第二个版本(str.join)速度要快得多。


5
“难道不应该完全相反吗?”“不一定。我不太了解Python的内部情况,不能具体评论,但一些常见观察是,你的第一个循环使用简单的运算符+,这可能由运行时作为原语实现。相比之下,其他循环首先必须解析模块名称,解析找到的变量/类,然后调用其中的成员函数。”
“另一个注意点是,你的循环可能太小,无法产生显着的数字。考虑到您的总运行时间较短,这可能使您的测试无效。”
“此外,你的测试用例高度专门化于两个短字符串。这样的情况永远不会给出边缘情况性能的清晰图片。”

1

字符串拼接(+)在CPython上有优化的实现。但是在其他架构上,如JythonIronPython,情况可能并非如此。因此,当您希望代码在这些解释器上表现良好时,应该使用字符串的.join()方法。 os.path.join()专门用于连接文件系统路径。它也会处理不同的路径分隔符。这将是构建文件名的正确方式。


1

这里有一个链接到Python维基百科,其中有关于字符串连接的注释,以及“这个部分在Python 2.5上有些错误。Python 2.5的字符串连接相当快”。

我相信自Python 2.5以来,字符串连接已经有了很大的改进,虽然str.join仍然更快(特别是对于大字符串),但你不会像在旧版本的Python中看到那么多的改进。

String Concatenation


1

尝试在2020年使用Python 3.9再次进行相同的测试,join仍然非常快,但是普通的连接也有所改善:

from io import StringIO
from array import array

loop_count = 10000
strings = [str(num) for num in range(loop_count)]
bytestrings = [b'%d' % num for num in range(loop_count)]

# 1.1453419709578156 seconds for 1000 repetitions (fastest of 5)
def concat():
  out = ''
  for s in strings:
    out += s
  return out

# 1.468063525040634 seconds for 1000 repetitions
# Removing decode() does not make it faster
def bytearray():
  out = array('b')
  for b in bytestrings:
    out.frombytes(b)
  return out.tobytes().decode()

# 0.9110414159949869 seconds for 1000 repetitions
def join():
  # I am rebuilding the list on purpose: I don't want to include
  # the overhead of printing numbers, but I do want to include
  # the overhead of building the list
  str_list = []
  for s in strings:
    str_list.append(s)
  return ''.join(str_list)

# 1.174764319031965 seconds for 1000 repetitions
def stringio():
  io = StringIO()
  for s in strings:
    io.write(s)
  return io.getvalue()

你测试它的系统是什么(硬件(CPU名称/类型,包括其时钟速度、核心数、L1缓存大小、L2缓存大小、L3缓存大小、RAM等),操作系统(包括版本和版本号),Python版本(例如CPython),Python编译配置(如果有的话)以及其他相关信息?请通过编辑(更改)您的答案来回应,而不是在评论中回答(不要添加“编辑:”、“更新:”或类似的内容 - 答案应该看起来像是今天写的)。 - Peter Mortensen
具有18个有效数字的结果没有意义(即使是量子力学也无法超越)。您能够将它们四舍五入以反映预计或已知的精度吗?我希望三个有效数字是最大值。 - Peter Mortensen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接