正如@jdi提到的,Python文档建议使用str.join或io。StringIO用于字符串连接。并且说开发人员应该期望在循环中使用+=的二次时间,尽管自Python 2.4以来已经进行了优化。正如这个答案所说:
如果Python检测到左边的参数没有其他引用,它会调用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()