用“+”连接两个字符串有什么不好的原因吗?

138

Python中常见的反模式是在循环中使用+来连接一系列字符串。这样做不好,因为Python解释器必须为每次迭代创建一个新的字符串对象,最终导致时间复杂度为二次方。 (最近的CPython版本可以在某些情况下进行优化,但其他实现可能无法进行优化,因此程序员被劝阻依赖此功能)。正确的方法是使用''.join

然而,我听说过(包括Stack Overflow上)你应该永远不要使用+来连接字符串,而是始终使用''.join或格式化字符串。如果你只是连接两个字符串,我不明白为什么要这样做。如果我的理解是正确的,它不应该花费二次方时间,我认为a + b''.join((a, b))'%s%s' % (a, b)更加简洁易读。

在Python中使用+连接两个字符串是否是好的做法?还是有我不知道的问题?


1
  • 更快,
In [2]: %timeit "a"*80 + "b"*80 1000000 循环,最佳 3 次: 每个循环 356 纳秒In [3]: %timeit "%s%s" % ("a"*80, "b"*80) 1000000 循环,最佳 3 次: 每个循环 907 纳秒
- Jakob Bowyer
4
在Python中,使用字符串格式化操作符%连接两个字符串ab的速度相对较慢,而使用加号+连接字符串的速度要快得多。在以上代码中,%timeit函数用于多次测试代码的执行时间,并返回最佳执行时间和每轮循环执行的平均时间。在这个例子中,%timeit "%s%s" % (a, b)表示测试字符串格式化操作符%连接字符串的执行时间,%timeit a + b表示测试使用加号+连接字符串的执行时间。 - Jakob Bowyer
1
@JakobBowyer 和其他人:关于“字符串拼接不好”的论点,几乎与速度无关,而是利用了 __str__ 的自动类型转换。请参见我的答案以获取示例。 - Izkata
如果你走捷径,效率怪兽会找上门来。有些人认为怪兽是不存在的;但它就在潜伏着。 - David Rivers
在Python 3.6中,文字串插值/格式化字串将比它们中的任何一个更快,在常见情况下。 - Antti Haapala -- Слава Україні
显示剩余3条评论
8个回答

132

使用+连接两个字符串没有任何问题。实际上,与''.join([a, b])相比,这种方式更容易阅读。

您说得对,使用+连接超过两个字符串是一个O(n^2)操作(与join的O(n)相比),因此效率较低。但是这与使用循环无关。即使是a+b+c+...也是O(n^2),原因是每次连接都会生成一个新字符串。

CPython2.4及以上版本会尝试缓解此问题,但连接超过两个字符串时仍建议使用join


6
.join 方法接受可迭代对象作为参数,因此 .join([a,b]).join((a,b)) 都是合法的。 - foundling
1
有趣的时间提示表明即使对于CPython 2.3+,使用++=也是被接受答案(来自Lennart Regebro)的一个好选择,并且只有在这更清晰地暴露问题解决方案的情况下才选择“添加/连接”模式。参考自https://dev59.com/XWct5IYBdhLWcg3wV7-k#12171382(出自 Lennart Regebro)的回答。 - Dilettant

52

加号运算符是连接两个Python字符串的完美解决方案。但如果你要添加超过两个字符串(n>25),你可能需要考虑其他方法。

''.join([a, b, c])技巧是一种性能优化。


7
元组会更快 - 代码只是一个例子 :) 通常长的多字符串输入是动态的。 - Mikko Ohtamaa
5
我认为他的意思是动态生成字符串并将其添加到列表中。 - Peter C
5
需要说明的是:元组通常是比较慢的数据结构,特别是当它在不断增长时。使用列表时,可以使用list.extend(list_of_items)和list.append(item)来动态地连接数据,这两个方法速度更快。 - Antti Haapala -- Слава Україні
9
对于 n > 25 给予加一的评价。人们需要一个起点作为参考。 - n611x007
3
实际上取决于字符串的大小,而不是它们的数量。即使有三个字符串,如果这些字符串非常长,使用连接操作仍然更好。 - Antti Haapala -- Слава Україні
显示剩余2条评论

8
假设永远不应该使用 + 进行字符串拼接,而始终使用 ''.join 可能是一种错误的观念。虽然使用 + 会创建不必要的临时复制不可变的字符串对象,但另一个经常被忽略的事实是,在循环中调用 join 通常会增加函数调用的开销。让我们看一个例子。
创建两个列表,一个来自链接的 SO 问题,另一个是更大的虚构列表。
>>> myl1 = ['A','B','C','D','E','F']
>>> myl2=[chr(random.randint(65,90)) for i in range(0,10000)]

让我们创建两个函数,UseJoinUsePlus 来使用各自的 join+ 功能。

>>> def UsePlus():
    return [myl[i] + myl[i + 1] for i in range(0,len(myl), 2)]

>>> def UseJoin():
    [''.join((myl[i],myl[i + 1])) for i in range(0,len(myl), 2)]

让我们使用第一个列表运行 timeit

>>> myl=myl1
>>> t1=timeit.Timer("UsePlus()","from __main__ import UsePlus")
>>> t2=timeit.Timer("UseJoin()","from __main__ import UseJoin")
>>> print "%.2f usec/pass" % (1000000 * t1.timeit(number=100000)/100000)
2.48 usec/pass
>>> print "%.2f usec/pass" % (1000000 * t2.timeit(number=100000)/100000)
2.61 usec/pass
>>> 

它们几乎具有相同的运行时间。

让我们使用cProfile。

>>> myl=myl2
>>> cProfile.run("UsePlus()")
         5 function calls in 0.001 CPU seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.001    0.001    0.001    0.001 <pyshell#1376>:1(UsePlus)
        1    0.000    0.000    0.001    0.001 <string>:1(<module>)
        1    0.000    0.000    0.000    0.000 {len}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
        1    0.000    0.000    0.000    0.000 {range}


>>> cProfile.run("UseJoin()")
         5005 function calls in 0.029 CPU seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.015    0.015    0.029    0.029 <pyshell#1388>:1(UseJoin)
        1    0.000    0.000    0.029    0.029 <string>:1(<module>)
        1    0.000    0.000    0.000    0.000 {len}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
     5000    0.014    0.000    0.014    0.000 {method 'join' of 'str' objects}
        1    0.000    0.000    0.000    0.000 {range}

看起来使用Join会导致不必要的函数调用,这可能会增加开销。

现在回到问题。是否应该在所有情况下都不鼓励使用+而使用join

我认为不是,应该考虑以下几点:

  1. 问题字符串的长度
  2. 连接操作的次数

当然,在开发早期过度优化是有害的。


8
当然,想法是不在循环内部使用 join - 相反,循环将生成一个序列,该序列将被传递到 join - jsbueno

8
当与多人合作时,有时很难确切知道正在发生什么。使用格式化字符串而不是连接字符串可以避免我们经历了很多次的一个特定烦恼:
假设一个函数需要一个参数,并且您编写它希望得到一个字符串:
In [1]: def foo(zeta):
   ...:     print 'bar: ' + zeta

In [2]: foo('bang')
bar: bang

因此,这个函数可能经常在代码中使用。你的同事可能知道它的功能,但不一定了解其内部工作原理,并且可能不知道该函数需要一个字符串参数。因此,他们可能会得到以下结果:

In [3]: foo(23)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)

/home/izkata/<ipython console> in <module>()

/home/izkata/<ipython console> in foo(zeta)

TypeError: cannot concatenate 'str' and 'int' objects

如果只使用格式化字符串,就不会有问题:

In [1]: def foo(zeta):
   ...:     print 'bar: %s' % zeta
   ...:     
   ...:     

In [2]: foo('bang')
bar: bang

In [3]: foo(23)
bar: 23

同样适用于所有定义了__str__的对象类型,这些对象也可以被传递进来:
In [1]: from datetime import date

In [2]: zeta = date(2012, 4, 15)

In [3]: print 'bar: ' + zeta
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)

/home/izkata/<ipython console> in <module>()

TypeError: cannot concatenate 'str' and 'datetime.date' objects

In [4]: print 'bar: %s' % zeta
bar: 2012-04-15

所以,如果你能使用格式化字符串,请一定要使用,并且充分利用Python所提供的功能。

2
对于一个有理有据的反对意见,我会点赞。不过我仍然认为我更喜欢 + - Taymon
2
为什么你不直接定义foo方法为:print 'bar: ' + str(zeta)? - EngineerWithJava54321
2
@EngineerWithJava54321 举个例子,zeta = u"a\xac\u1234\u20ac\U00008000" - 因此,您必须使用 print 'bar: ' + unicode(zeta) 来确保它不会导致错误。 使用 %s 可以正确运行,而无需考虑太多,并且更短。 - Izkata
@EngineerWithJava54321 其他例子在这里不太相关,但例如,“bar:%s”可能会被翻译成其他语言中的“zrb:%s br”。%s版本将正常工作,但字符串连接版本将变得混乱,以处理所有情况,并且您的翻译人员现在必须处理两个单独的翻译。 - Izkata
如果他们不知道foo的实现是什么,他们将在任何“def”中遇到此错误。 - insidesin

4
根据Python文档,使用str.join()可以在各种Python实现中提供性能一致性。尽管CPython优化了s = s + t的二次行为,但其他Python实现可能没有这样做。
CPython实现细节:如果s和t都是字符串,则像CPython这样的某些Python实现通常可以执行s = s + t或s += t形式的就地优化。如果适用,此优化将使二次运行时不太可能发生。此优化既取决于版本又取决于实现。对于性能敏感的代码,最好使用str.join()方法,它确保在各个版本和实现中具有一致的线性连接性能。
请参阅Python文档中的序列类型(请参见脚注[6])。

3

我已经进行了一次快速测试:

import sys

str = e = "a xxxxxxxxxx very xxxxxxxxxx long xxxxxxxxxx string xxxxxxxxxx\n"

for i in range(int(sys.argv[1])):
    str = str + e

并计时:

mslade@mickpc:/binks/micks/ruby/tests$ time python /binks/micks/junk/strings.py  8000000
8000000 times

real    0m2.165s
user    0m1.620s
sys     0m0.540s
mslade@mickpc:/binks/micks/ruby/tests$ time python /binks/micks/junk/strings.py  16000000
16000000 times

real    0m4.360s
user    0m3.480s
sys     0m0.870s

显然,对于a = a + b的情况存在一种优化方法。它并没有像人们想象的那样展现出O(n^2)的时间复杂度。

因此,从性能上来说,使用+是可以接受的。


3
这里可以将其与“join”情况进行比较。还有其他Python实现的问题,如pypy、jython、ironpython等等... - jsbueno

3

我使用的是 Python 3.8

string4 = f'{string1}{string2}{string3}'

1

''.join([a, b])+更好的解决方案。

因为代码应该以不会劣化其他Python实现(如PyPy,Jython,IronPython,Cython,Psyco等)的方式编写。

使用a += b或a = a + b即使在CPython中也很脆弱,在不使用引用计数的实现中根本不存在。(引用计数是一种存储对资源(如对象、内存块、磁盘空间或其他资源)的引用、指针或句柄数量的技术。)

https://www.python.org/dev/peps/pep-0008/#programming-recommendations


1
a += b 在所有 Python 实现中都可以工作,只是在某些实现中,当它在循环内部执行时,它需要二次时间;而问题是关于循环外的字符串连接。 - Taymon

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