414

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

我可以这样写:

s += stringfromelsewhere

或像这样:

s = []

s.append(somestring)
    
# later
    
s = ''.join(s)

在写这个问题时,我发现了一篇关于这个话题的好文章。

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

但它在 Python 2.x. 中,所以问题是在 Python 3 中是否发生了一些变化?

4

12 回答 12

480

将字符串附加到字符串变量的最佳+方法是使用or +=。这是因为它可读且快速。它们也一样快,您选择哪一种是口味问题,后者是最常见的。以下是该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

哎呀。事实证明,即使在这种情况下,追加/加入也更慢。

那么这个推荐是从哪里来的呢?蟒蛇2?

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

好吧,如果您使用极长的字符串(通常不是,那么在内存中会有 100MB 的字符串,追加/连接会稍微快一些吗?)

但真正的关键是 Python 2.3。我什至不会告诉你时间,因为它太慢了,它还没有完成。这些测试突然需要几分钟。除了追加/连接,它和后来的 Python 一样快。

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

(更新:事实证明,当我在 Python 2.3 上更仔细地进行测试时,使用+并且+=对于两个字符串也更快。使用的建议''.join()一定是一个误解)

但是,这是 CPython。其他实现可能有其他问题。这只是过早优化是万恶之源的另一个原因。除非您首先测量它,否则不要使用所谓的“更快”的技术。

因此,进行字符串连接的“最佳”版本是使用 + 或 +=。如果这对你来说很慢,这不太可能,然后做其他事情。

那么为什么我在我的代码中使用大量的附加/连接呢?因为有时它实际上更清楚。特别是当你应该连接在一起的任何东西都应该用空格、逗号或换行符分隔时。

于 2012-08-29T05:24:50.297 回答
56

如果您要连接很多值,那么两者都不是。附加列表是昂贵的。您可以为此使用 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 FAQ:将许多字符串连接在一起的最有效方法是什么?

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 的测试,以及使用更大字符串的更大列表的第二轮测试。(蟒蛇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
于 2012-08-29T01:48:41.507 回答
48

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

>>> name = 'some_name'
>>> number = 123
>>>
>>> f'Name is {name} and the number is {number}.'
'Name is some_name and the number is 123.'
于 2018-05-22T18:45:08.327 回答
18

就稳定性和交叉实现而言,使用“+”就地字符串连接是最差的连接方法,因为它不支持所有值。PEP8 标准不鼓励这种做法,并鼓励使用 format()、join() 和 append() 以供长期使用。

正如链接的“编程建议”部分所引用的:

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

于 2014-07-15T13:40:38.347 回答
9

你写这个函数

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
于 2017-07-15T08:20:06.270 回答
8

推荐的方法仍然是使用 append 和 join。

于 2012-08-29T01:48:02.503 回答
8

如果您要连接的字符串是文字,请使用字符串文字连接

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

如果您想对字符串的一部分(如上)进行评论,或者如果您想使用原始字符串或三引号作为文字的一部分但不是全部,这很有用。

由于这发生在语法层,它使用零连接运算符。

于 2016-08-06T17:21:02.110 回答
7

正如@jdi 提到的那样,Python 文档建议使用str.joinorio.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()

微基准

于 2018-09-28T18:37:07.290 回答
7

你可以用不同的方式来做。

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

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

于 2019-06-24T11:07:50.140 回答
6

虽然有些过时,但Code Like a Pythonista: Idiomatic Python建议join()+ 本节中结束。与PythonSpeedPerformanceTips在其关于字符串连接的部分中一样,具有以下免责声明:

本节的准确性对于 Python 的更高版本存在争议。在 CPython 2.5 中,字符串连接相当快,尽管这可能不适用于其他 Python 实现。有关讨论,请参阅 ConcatenationTestCode。

于 2012-08-29T01:57:35.450 回答
4

我的用例略有不同。我必须构建一个查询,其中超过 20 个字段是动态的。我遵循了这种使用格式方法的方法

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

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

于 2018-04-29T03:33:35.333 回答
3

你也可以使用这个(更有效)。(https://softwareengineering.stackexchange.com/questions/304445/why-is-s-better-than-for-concatenation

s += "%s" %(stringfromelsewhere)
于 2016-06-24T07:46:20.663 回答