在Python中连接字符串的首选方法是什么?


358

由于string无法更改Python ,因此我想知道如何更有效地连接字符串?

我可以这样写:

s += stringfromelsewhere

或像这样:

s = []
s.append(somestring)

later

s = ''.join(s)

在写这个问题时,我找到了一篇很好的文章,谈论这个话题。

http://www.skymind.com/~ocrow/python_string/

但是它在Python 2.x中,所以问题是在Python 3中会有所改变吗?


Answers:


433

将字符串附加到字符串变量的最佳方法是使用++=。这是因为它可读且快速。它们的速度也一样快,您选择的是一个品味问题,后者是最常见的。以下是该timeit模块的时间安排:

a = a + b:
0.11338996887207031
a += b:
0.11040496826171875

但是,那些建议拥有列表并附加到列表然后再连接这些列表的人之所以这么做,是因为将字符串附加到列表与扩展字符串相比可能非常快。在某些情况下,这可能是正确的。例如,这里是一字符字符串的一百万个追加,首先是字符串,然后是列表:

a += b:
0.10780501365661621
a.append(b):
0.1123361587524414

好的,事实证明,即使结果字符串的长度为一百万个字符,追加操作仍然更快。

现在让我们尝试将十千个字符长的字符串追加十万次:

a += b:
0.41823482513427734
a.append(b):
0.010656118392944336

因此,最终字符串的长度约为100MB。那太慢了,追加到列表上要快得多。那个时机不包括决赛a.join()。那要花多长时间?

a.join(a):
0.43739795684814453

哎呀 即使在这种情况下,append / join也较慢。

那么,该建议来自何处?Python 2?

a += b:
0.165287017822
a.append(b):
0.0132720470428
a.join(a):
0.114929914474

好吧,如果您使用的是非常长的字符串(通常不是,那么内存中100MB的字符串会是什么),append / join的速度会稍微快一些。

但是真正的关键是Python 2.3。我什至不告诉您时间安排,因为它是如此之慢以至于还没有完成。这些测试突然耗时数分钟。除了append / join之外,它和以后的Python一样快。

对。在石器时代,字符串连接在Python中非常缓慢。但是在2.4上已经不存在了(或者至少是Python 2.4.7),因此在2008年Python 2.3停止更新时,使用append / join的建议已过时,您应该停止使用它。:-)

(更新:当我更仔细地进行测试时发现,使用++=在Python 2.3上使用两个字符串的速度也更快。关于使用的建议''.join()一定是一种误解)

但是,这是CPython。其他实现可能还有其他问题。这是过早优化是万恶之源的又一个原因。除非先进行测量,否则不要使用被认为“更快”的技术。

因此,进行字符串连接的“最佳”版本是使用+或+ =。如果事实证明这对您来说很慢,那是不太可能的,那么请执行其他操作。

那么,为什么在我的代码中使用大量的添加/联接?因为有时它实际上更清晰。尤其是当您应将其串联在一起时,应以空格,逗号或换行符分隔。


10
如果您有多个字符串(n> 10),则“” .join(list_of_strings)仍然会更快
Mikko Ohtamaa

11
+ =快速的原因是,如果refcount为1,则cpython中会有一个性能缺陷-它在所有其他python实现中都无法使用(除了配置得相当特殊的pypy版本外)
罗尼

17
为什么这么多批评?如何使用仅在一个特定实现上有效的算法,并且本质上相当于修复二次时间算法的脆弱技巧,该如何更好?您也完全误解了“过早的优化是万恶之源”的观点。那句话是在谈论小优化。这是从O(n ^ 2)到O(n)的不小的优化。
2012年

12
这是实际的报价:“我们应该忘掉效率低下的情况,大约有97%的时间是这样:过早的优化是万恶之源。但是,我们不应该放弃那3%的临界机会。优秀的程序员不会被这样的推理弄得沾沾自喜,他将很明智地仔细查看关键代码;但只有在确定了该代码之后,“
Wes 2012年

2
没有人说a + b慢。当您多次执行a = a + b时,这是二次方的。a + b + c并不慢,我重复一遍也不慢,因为它只需要遍历每个字符串一次,而它必须使用a = a + b方法重新遍历先前的字符串很多次(假设处于循环中)某种)。请记住,字符串是不可变的。
2012年

52

如果要串联很多值,那么两者都不是。追加列表很昂贵。您可以为此使用StringIO。特别是如果您要通过大量操作来构建它。

from cStringIO import StringIO
# python3:  from io import StringIO

buf = StringIO()

buf.write('foo')
buf.write('foo')
buf.write('foo')

buf.getvalue()
# 'foofoofoo'

如果您已经有其他操作返回的完整列表,则只需使用 ''.join(aList)

从python常见问题解答:将许多字符串连接在一起的最有效方法是什么?

str和bytes对象是不可变的,因此将多个字符串连接在一起效率不高,因为每个串联都会创建一个新对象。在一般情况下,总运行时成本在总字符串长度中是二次方的。

要累积许多str对象,建议的惯用法是将它们放入列表中,并在最后调用str.join():

chunks = []
for s in my_strings:
    chunks.append(s)
result = ''.join(chunks)

(另一个合理有效的习惯用法是使用io.StringIO)

要累积许多字节对象,建议的惯用法是使用就地串联(+ =运算符)扩展一个bytearray对象:

result = bytearray()
for b in my_bytes_objects:
    result += b

编辑:我很愚蠢,并且将结果向后粘贴,使其看起来比cStringIO更快。我还添加了针对bytearray / str concat的测试,以及使用较大列表和较大字符串的第二轮测试。(python 2.7.3)

大量字符串的ipython测试示例

try:
    from cStringIO import StringIO
except:
    from io import StringIO

source = ['foo']*1000

%%timeit buf = StringIO()
for i in source:
    buf.write(i)
final = buf.getvalue()
# 1000 loops, best of 3: 1.27 ms per loop

%%timeit out = []
for i in source:
    out.append(i)
final = ''.join(out)
# 1000 loops, best of 3: 9.89 ms per loop

%%timeit out = bytearray()
for i in source:
    out += i
# 10000 loops, best of 3: 98.5 µs per loop

%%timeit out = ""
for i in source:
    out += i
# 10000 loops, best of 3: 161 µs per loop

## Repeat the tests with a larger list, containing
## strings that are bigger than the small string caching 
## done by the Python
source = ['foo']*1000

# cStringIO
# 10 loops, best of 3: 19.2 ms per loop

# list append and join
# 100 loops, best of 3: 144 ms per loop

# bytearray() +=
# 100 loops, best of 3: 3.8 ms per loop

# str() +=
# 100 loops, best of 3: 5.11 ms per loop

2
cStringIO在Py3中不存在。使用io.StringIO代替。
lvc 2012年

2
至于为什么反复附加到字符串上可能会很昂贵:joelonsoftware.com/articles/fog0000000319.html
Wes

36

在Python> = 3.6中,新的f字符串是连接字符串的有效方法。

>>> name = 'some_name'
>>> number = 123
>>>
>>> f'Name is {name} and the number is {number}.'
'Name is some_name and the number is 123.'

8

推荐的方法仍然是使用附加和联接。


1
从我的回答中可以看出,这取决于要连接的字符串数。我已经对此进行了一些计时(请参阅我在对答案的评论中链接到的谈话),并且通常除非超过十个,否则请使用+。
Lennart Regebro

1
PEP8提到了这一点(python.org/dev/peps/pep-0008/#programming-recommendations)。合理的是,尽管CPython对使用+ =的字符串连接进行了特殊的优化,但其他实现可能没有。
Quantum7年

8

如果要串联的字符串是文字,请使用字符串文字串联

re.compile(
        "[A-Za-z_]"       # letter or underscore
        "[A-Za-z0-9_]*"   # letter, digit or underscore
    )

如果要对字符串的一部分进行注释(如上)或要使用原始字符串,这将很有用文本的一部分(但不是全部)或三引号,。

由于这是在语法层发生的,因此它使用零个串联运算符。


7

你写这个函数

def str_join(*args):
    return ''.join(map(str, args))

然后,您可以随时随地致电

str_join('Pine')  # Returns : Pine
str_join('Pine', 'apple')  # Returns : Pineapple
str_join('Pine', 'apple', 3)  # Returns : Pineapple3

1
str_join = lambda *str_list: ''.join(s for s in str_list)
里克(Rick)支持莫妮卡(Monica)


6

就稳定性和交叉实现而言,最糟糕的串联方法是使用'+'进行就地字符串串联,因为它不支持所有值。PEP8标准不鼓励这样做,并鼓励长期使用format(),join()和append()。

如链接的“编程建议”部分所述:

例如,对于形式为+ = b或a = a + b的语句,请不要依赖CPython有效地实现就地字符串连接。即使在CPython中,这种优化也是脆弱的(仅适用于某些类型),并且在不使用引用计数的实现中根本没有这种优化。在库的性能敏感部分,应改用''.join()形式。这将确保在各种实现中串联发生在线性时间内。


5
参考链接本来很好:)

5

如@jdi所述,Python文档建议使用str.joinio.StringIO进行字符串连接。并说开发人员应该从+=循环中期待二次时间,即使自Python 2.4开始进行了优化。正如这个答案所说:

如果Python检测到left参数没有其他引用,它将调用realloc来尝试通过调整字符串的大小来避免复制。这不是您应该依靠的东西,因为它是一个实现细节,并且因为如果realloc最终需要频繁移动字符串,那么性能会下降到O(n ^ 2)。

我将展示一个天真地依赖于+=这种优化的真实代码示例,但它并不适用。下面的代码将可迭代的短字符串转换为更大的块,以用于批量API。

def test_concat_chunk(seq, split_by):
    result = ['']
    for item in seq:
        if len(result[-1]) + len(item) > split_by: 
            result.append('')
        result[-1] += item
    return result

由于二次时间的复杂性,该代码可能在文学上运行数小时。以下是具有建议的数据结构的替代方案:

import io

def test_stringio_chunk(seq, split_by):
    def chunk():
        buf = io.StringIO()
        size = 0
        for item in seq:
            if size + len(item) <= split_by:
                size += buf.write(item)
            else:
                yield buf.getvalue()
                buf = io.StringIO()
                size = buf.write(item)
        if size:
            yield buf.getvalue()

    return list(chunk())

def test_join_chunk(seq, split_by):
    def chunk():
        buf = []
        size = 0
        for item in seq:
            if size + len(item) <= split_by:
                buf.append(item)
                size += len(item)
            else:
                yield ''.join(buf)                
                buf.clear()
                buf.append(item)
                size = len(item)
        if size:
            yield ''.join(buf)

    return list(chunk())

还有一个微基准测试:

import timeit
import random
import string
import matplotlib.pyplot as plt

line = ''.join(random.choices(
    string.ascii_uppercase + string.digits, k=512)) + '\n'
x = []
y_concat = []
y_stringio = []
y_join = []
n = 5
for i in range(1, 11):
    x.append(i)
    seq = [line] * (20 * 2 ** 20 // len(line))
    chunk_size = i * 2 ** 20
    y_concat.append(
        timeit.timeit(lambda: test_concat_chunk(seq, chunk_size), number=n) / n)
    y_stringio.append(
        timeit.timeit(lambda: test_stringio_chunk(seq, chunk_size), number=n) / n)
    y_join.append(
        timeit.timeit(lambda: test_join_chunk(seq, chunk_size), number=n) / n)
plt.plot(x, y_concat)
plt.plot(x, y_stringio)
plt.plot(x, y_join)
plt.legend(['concat', 'stringio', 'join'], loc='upper left')
plt.show()

微观基准


5

您可以采用不同的方式。

str1 = "Hello"
str2 = "World"
str_list = ['Hello', 'World']
str_dict = {'str1': 'Hello', 'str2': 'World'}

# Concatenating With the + Operator
print(str1 + ' ' + str2)  # Hello World

# String Formatting with the % Operator
print("%s %s" % (str1, str2))  # Hello World

# String Formatting with the { } Operators with str.format()
print("{}{}".format(str1, str2))  # Hello World
print("{0}{1}".format(str1, str2))  # Hello World
print("{str1} {str2}".format(str1=str_dict['str1'], str2=str_dict['str2']))  # Hello World
print("{str1} {str2}".format(**str_dict))  # Hello World

# Going From a List to a String in Python With .join()
print(' '.join(str_list))  # Hello World

# Python f'strings --> 3.6 onwards
print(f"{str1} {str2}")  # Hello World

我通过以下文章创建了这个小总结。


3

我的用例略有不同。我不得不构造一个查询,其中有20多个字段是动态的。我遵循了这种使用格式化方法的方法

query = "insert into {0}({1},{2},{3}) values({4}, {5}, {6})"
query.format('users','name','age','dna','suzan',1010,'nda')

这对我来说相对来说比较简单,而不是使用+或其他方式


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.