【问题标题】:Any reason not to use '+' to concatenate two strings?有什么理由不使用“+”来连接两个字符串?
【发布时间】:2012-04-20 01:55:51
【问题描述】:

Python 中的一个常见反模式是在循环中使用+ 连接字符串序列。这很糟糕,因为 Python 解释器必须为每次迭代创建一个新的字符串对象,并且最终会花费二次时间。 (最近的 CPython 版本在某些情况下显然可以对此进行优化,但其他实现则不能,因此不鼓励程序员依赖它。)''.join 是这样做的正确方法。

但是,我听说 (including here on Stack Overflow) 你应该永远使用+ 进行字符串连接,而始终使用''.join 或格式字符串。如果您只连接两个字符串,我不明白为什么会出现这种情况。如果我的理解是正确的,它不应该花费二次时间,而且我认为a + b''.join((a, b))'%s%s' % (a, b) 更干净,更具可读性。

使用+ 连接两个字符串是一种好习惯吗?还是有我不知道的问题?

【问题讨论】:

  • 它更整洁,您可以更好地控制不进行连接。但是它的速度稍慢,字符串抨击权衡:P
  • 您是说+ 更快还是更慢?为什么?
  • + 更快,In [2]: %timeit "a"*80 + "b"*801000000 loops, best of 3: 356 ns per loopIn [3]: %timeit "%s%s" % ("a"*80, "b"*80)1000000 loops, best of 3: 907 ns per loop
  • In [3]: %timeit "%s%s" % (a, b) 1000000 loops, best of 3: 590 ns per loop In [4]: %timeit a + b 10000000 loops, best of 3: 147 ns per loop
  • @JakobBowyer 和其他人:“字符串连接不好”参数几乎与速度无关,而是利用 __str__ 的自动类型转换。有关示例,请参阅我的答案。

标签: python string-concatenation anti-patterns


【解决方案1】:

+ 连接两个 字符串并没有错。确实比''.join([a, b]) 更容易阅读。

您是对的,尽管将 2 个以上的字符串与 + 连接是一个 O(n^2) 操作(与 join 的 O(n) 相比),因此效率低下。然而,这与使用循环无关。即使a + b + c + ... 也是 O(n^2),原因是每个连接都会产生一个新字符串。

CPython2.4 及更高版本试图缓解这种情况,但在连接超过 2 个字符串时仍建议使用 join

【讨论】:

  • @Mutant: .join 需要一个可迭代对象,所以.join([a,b]).join((a,b)) 都是有效的。
  • 有趣的时间提示在接受的答案(从 2013 年开始)中使用 ++= stackoverflow.com/a/12171382/378826(来自 Lennart Regebro)即使对于 CPython 2.3+ 并且只选择“附加/加入”模式,如果这更清楚地揭示了手头问题解决方案的想法。
【解决方案2】:

Plus 运算符是连接 两个 Python 字符串的完美解决方案。但是,如果您继续添加两个以上的字符串 (n > 25),您可能需要考虑其他问题。

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

【讨论】:

  • Tuple 会更快——代码只是一个例子 :) 通常长的多个字符串输入是动态的。
  • @martineau 我认为他的意思是动态生成和append()ing 字符串到列表。
  • 这里需要说一下:tuple通常是SLOWER结构,尤其是在增长的时候。使用 list 您可以使用 list.extend(list_of_items) 和 list.append(item) ,它们在动态连接内容时要快得多。
  • +1 表示n > 25。人类需要参考点才能从某个地方开始。
  • 实际上取决于字符串的大小,而不是它们的数量。如果字符串真的很长,即使是 3 个字符串连接也会更好。
【解决方案3】:

永远不应该使用 + 进行字符串连接,而是始终使用 ''.join 的假设可能是一个神话。确实,使用+ 会创建不可变字符串对象的不必要的临时副本,但另一个不经常引用的事实是,在循环中调用join 通常会增加function call 的开销。让我们举个例子。

创建两个列表,一个来自链接的 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. 串联操作数。

在开发中未成熟的优化偏离路线是邪恶的。

【讨论】:

  • 当然,我们的想法是不要在循环本身内部使用join - 而是循环会生成一个序列,该序列将被传递给加入。
【解决方案4】:

与多人一起工作时,有时很难确切地知道发生了什么。使用格式字符串而不是串联可以避免我们经常遇到的一个特殊烦恼:

比如说,一个函数需要一个参数,而你编写它时期望得到一个字符串:

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 提供的功能。

【讨论】:

  • +1 表示有充分理由的反对意见。我仍然认为我喜欢+
  • 为什么不把 foo 方法定义为:print 'bar: ' + str(zeta)?
  • @EngineerWithJava54321 例如,zeta = u"a\xac\u1234\u20ac\U00008000" - 所以你必须使用print 'bar: ' + unicode(zeta) 来确保它不会出错。 %s 做对了,不用多想,而且更短
  • @EngineerWithJava54321 其他示例在这里不太相关,但例如,"bar: %s" 可能会以其他语言翻译成"zrb: %s br"%s 版本可以正常工作,但 string-concat 版本会变得一团糟,无法处理所有情况,您的翻译人员现在需要处理两个单独的翻译
  • 如果他们不知道 foo 的实现是什么,他们会遇到任何def 的错误。
【解决方案5】:

根据 Python 文档,使用 str.join() 将为您提供跨各种 Python 实现的性能一致性。尽管 CPython 优化了 s = s + t 的二次行为,但其他 Python 实现可能不会。

CPython 实现细节:如果 s 和 t 都是字符串,一些 诸如 CPython 之类的 Python 实现通常可以就地执行 优化 s = s + t 或 s += t 形式的赋值。什么时候 适用,这种优化使二次运行时间大大减少 可能。这个优化既是版本也是实现 依赖。对于性能敏感的代码,最好使用 str.join() 方法确保一致的线性连接 跨版本和实现的性能。

Sequence Types in Python docs(见脚注[6])

【讨论】:

    【解决方案6】:

    我做了一个快速测试:

    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) 时间。

    所以至少在性能方面,使用+ 是可以的。

    【讨论】:

    • 您可以在此处与“加入”案例进行比较。还有其他Python实现的问题,比如pypy、jython、ironpython等……
    【解决方案7】:

    我在 python 3.8 中使用以下内容

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

    【讨论】:

      【解决方案8】:

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

      因为代码的编写方式不应损害 Python 的其他实现(PyPy、Jython、IronPython、Cython、Psyco 等)

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

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

      【讨论】:

      • a += b 适用于 Python 的所有实现,只是在其中一些实现中在循环内完成时需要二次时间;问题是关于循环的字符串连接outside
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-12-31
      • 2013-11-05
      • 2020-09-19
      相关资源
      最近更新 更多