如何在 Python 中模拟可变字符串(如 Java 中的 StringBuffer 或 C# 中的 StringBuilder)?

How can I emulate a mutable string in Python (like StringBuffer in Java or StringBuilder in C#)?

提问人:user2902773 提问时间:11/12/2013 最后编辑:Karl Knechteluser2902773 更新时间:3/25/2023 访问量:90303

问:

由于 Python 的字符串是不可变的,因此在循环中重复编辑它们是低效的。如何使用可变数据结构实现字符串操作,从而避免产生大量临时字符串?

字符串 可变

评论

4赞 Joachim Sauer 11/12/2013
通过构建字符串列表并在循环后使用它,您可能会获得类似的效果。但我敢肯定有一种更pythonic的方式(可能涉及列表理解)。join()

答:

3赞 Kamlesh Meghwal 11/12/2013 #1

此链接可能对 Python 中的串联有用

http://pythonadventures.wordpress.com/2010/09/27/stringbuilder/

上面链接中的示例:

def g():
    sb = []
    for i in range(30):
        sb.append("abcdefg"[i%7])

    return ''.join(sb)

print g()   

# abcdefgabcdefgabcdefgabcdefgab

评论

0赞 Joachim Sauer 11/12/2013
虽然这在理论上可以回答这个问题,但最好在这里包括答案的基本部分,并提供链接以供参考。
13赞 unutbu 11/12/2013 #2

也许使用bytearray

In [1]: s = bytearray('Hello World')

In [2]: s[:5] = 'Bye'

In [3]: s
Out[3]: bytearray(b'Bye World')

In [4]: str(s)
Out[4]: 'Bye World'

使用字节数组的吸引力在于其内存效率和方便的语法。它也可以比使用临时列表更快:

In [36]: %timeit s = list('Hello World'*1000); s[5500:6000] = 'Bye'; s = ''.join(s)
1000 loops, best of 3: 256 µs per loop

In [37]: %timeit s = bytearray('Hello World'*1000); s[5500:6000] = 'Bye'; str(s)
100000 loops, best of 3: 2.39 µs per loop

请注意,速度的大部分差异可归因于容器的创建:

In [32]: %timeit s = list('Hello World'*1000)
10000 loops, best of 3: 115 µs per loop

In [33]: %timeit s = bytearray('Hello World'*1000)
1000000 loops, best of 3: 1.13 µs per loop

评论

0赞 Joachim Sauer 11/12/2013
这将使用什么编码?在 Java 中,类似的结构会非常有问题,因为它们使用平台默认编码,可以是任何东西......
0赞 unutbu 11/12/2013
@JoachimSauer:像 一样,编码由您决定。就 而言,每个值都只是一个字节。strbytearray
0赞 bruno desthuilliers 11/12/2013
ByteArray 对于真正低级的东西很有用——顾名思义,它实际上是关于“字节数组”,而不是“字符串”。
0赞 fikr4n 2/3/2014
"...但这比使用临时列表要慢。什么是临时列表?是(Python 的默认)列表,比如?['s', 't', 'r', 'i', 'n', 'g']
0赞 unutbu 2/3/2014
@BornToCode:临时列表将采用 bruno desthuilliers 的代码mylist
15赞 bruno desthuilliers 11/12/2013 #3

取决于你想做什么。如果你想要一个可变的序列,内置类型就是你的朋友,从 str 到 list 再返回就像:list

 mystring = "abcdef"
 mylist = list(mystring)
 mystring = "".join(mylist)

如果你想使用 for 循环构建一个大字符串,pythonic 方法通常是构建一个字符串列表,然后用适当的分隔符(换行符或其他)将它们连接在一起。

否则,您还可以使用一些文本模板系统,或解析器或任何最适合这项工作的专用工具。

评论

0赞 9/22/2015
“”.join(mylist) 的复杂度是 O(n) 吗?
0赞 Cecil Curry 6/15/2016
@user2374515 是的,方法是 O(n) 复杂度。根据官方文档:“对于性能敏感的代码,最好使用确保不同版本和实现之间一致的线性串联性能的方法。str.join()str.join()
120赞 6 revs, 5 users 65%georg #4

蟒蛇 3

文档中:

连接不可变序列总是会产生一个新对象。这意味着通过重复串联构建序列将在总序列长度中产生二次运行时成本。若要获得线性运行时成本,必须切换到以下替代方法之一: 如果连接 str 对象,您可以构建一个列表并在末尾使用 str.join() 或写入 IO。StringIO 实例,并在完成后检索其值

试验比较几个选项的运行时:

import sys
import timeit
from io import StringIO
from array import array


def test_concat():
    out_str = ''
    for _ in range(loop_count):
        out_str += 'abc'
    return out_str


def test_join_list_loop():
    str_list = []
    for _ in range(loop_count):
        str_list.append('abc')
    return ''.join(str_list)


def test_array():
    char_array = array('b')
    for _ in range(loop_count):
        char_array.frombytes(b'abc')
    return str(char_array.tostring())


def test_string_io():
    file_str = StringIO()
    for _ in range(loop_count):
        file_str.write('abc')
    return file_str.getvalue()


def test_join_list_compr():
    return ''.join(['abc' for _ in range(loop_count)])


def test_join_gen_compr():
    return ''.join('abc' for _ in range(loop_count))


loop_count = 80000

print(sys.version)

res = {}

for k, v in dict(globals()).items():
    if k.startswith('test_'):
        res[k] = timeit.timeit(v, number=10)

for k, v in sorted(res.items(), key=lambda x: x[1]):
    print('{:.5f} {}'.format(v, k))

results

3.7.5 (default, Nov  1 2019, 02:16:32) 
[Clang 11.0.0 (clang-1100.0.33.8)]
0.03738 test_join_list_compr
0.05681 test_join_gen_compr
0.09425 test_string_io
0.09636 test_join_list_loop
0.11976 test_concat
0.19267 test_array

Python 2

Efficient String Concatenation in Python is a rather old article and its main statement that the naive concatenation is far slower than joining is not valid anymore, because this part has been optimized in CPython since then. From the docs:

CPython implementation detail: If s and t are both strings, some Python implementations such as CPython can usually perform an in-place optimization for assignments of the form s = s + t or s += t. When applicable, this optimization makes quadratic run-time much less likely. This optimization is both version and implementation dependent. For performance sensitive code, it is preferable to use the str.join() method which assures consistent linear concatenation performance across versions and implementations.

I've adapted their code a bit and got the following results on my machine:

from cStringIO import StringIO
from UserString import MutableString
from array import array

import sys, timeit

def method1():
    out_str = ''
    for num in xrange(loop_count):
        out_str += `num`
    return out_str

def method2():
    out_str = MutableString()
    for num in xrange(loop_count):
        out_str += `num`
    return out_str

def method3():
    char_array = array('c')
    for num in xrange(loop_count):
        char_array.fromstring(`num`)
    return char_array.tostring()

def method4():
    str_list = []
    for num in xrange(loop_count):
        str_list.append(`num`)
    out_str = ''.join(str_list)
    return out_str

def method5():
    file_str = StringIO()
    for num in xrange(loop_count):
        file_str.write(`num`)
    out_str = file_str.getvalue()
    return out_str

def method6():
    out_str = ''.join([`num` for num in xrange(loop_count)])
    return out_str

def method7():
    out_str = ''.join(`num` for num in xrange(loop_count))
    return out_str


loop_count = 80000

print sys.version

print 'method1=', timeit.timeit(method1, number=10)
print 'method2=', timeit.timeit(method2, number=10)
print 'method3=', timeit.timeit(method3, number=10)
print 'method4=', timeit.timeit(method4, number=10)
print 'method5=', timeit.timeit(method5, number=10)
print 'method6=', timeit.timeit(method6, number=10)
print 'method7=', timeit.timeit(method7, number=10)

Results:

2.7.1 (r271:86832, Jul 31 2011, 19:30:53) 
[GCC 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2335.15.00)]
method1= 0.171155929565
method2= 16.7158739567
method3= 0.420584917068
method4= 0.231794118881
method5= 0.323612928391
method6= 0.120429992676
method7= 0.145267963409

Conclusions:

  • join still wins over concat, but marginally
  • list comprehensions are faster than loops (when building a list)
  • joining generators is slower than joining lists
  • other methods are of no use (unless you're doing something special)

评论

3赞 Adam Oren 12/25/2015
MutableString 类在 python 2.6 中被弃用,在 Python 3 中被完全删除,这可能毫无价值。看这里
2赞 jtschoonhoven 7/4/2020
警告!CPython 优化此功能的声明在最新版本 (v3.5-v3.8+) 中不再适用。这已被一个警告所取代,即以这种方式连接不可变性始终是二次的:docs.python.org/3/library/stdtypes.html
0赞 georg 7/7/2020
@jtschoonhoven:我已经把帖子变成了CW,请编辑你的评论。谢谢!
0赞 Pod 10/28/2022
这根本不能回答问题
11赞 rhaertel80 3/12/2015 #5

之前提供的答案几乎总是最好的。但是,有时字符串是在许多方法调用和/或循环中构建的,因此构建行列表然后联接它们并不一定很自然。而且由于不能保证您使用的是 CPython,或者 CPython 的优化将适用,因此另一种方法是仅使用 !print

下面是一个示例 helper 类,尽管 helper 类是微不足道的,而且可能没有必要,但它用于说明该方法 (Python 3):

import io

class StringBuilder(object):

    def __init__(self):
        self._stringio = io.StringIO()
    
    def __str__(self):
        return self._stringio.getvalue()
    
    def append(self, *objects, sep=' ', end=''):
        print(*objects, sep=sep, end=end, file=self._stringio)

sb = StringBuilder()
sb.append('a')
sb.append('b', end='\n')
sb.append('c', 'd', sep=',', end='\n')
print(sb)  # 'ab\nc,d\n'
2赞 Roee Gavirel 9/13/2017 #6

只是我在 python 3.6.2 上运行的一个测试,表明“加入”仍然赢得大奖!

from time import time


def _with_format(i):
    _st = ''
    for i in range(0, i):
        _st = "{}{}".format(_st, "0")
    return _st


def _with_s(i):
    _st = ''
    for i in range(0, i):
        _st = "%s%s" % (_st, "0")
    return _st


def _with_list(i):
    l = []
    for i in range(0, i):
        l.append("0")
    return "".join(l)


def _count_time(name, i, func):
    start = time()
    r = func(i)
    total = time() - start
    print("%s done in %ss" % (name, total))
    return r

iterationCount = 1000000

r1 = _count_time("with format", iterationCount, _with_format)
r2 = _count_time("with s", iterationCount, _with_s)
r3 = _count_time("with list and join", iterationCount, _with_list)

if r1 != r2 or r2 != r3:
    print("Not all results are the same!")

输出为:

with format done in 17.991968870162964s
with s done in 18.36879801750183s
with list and join done in 0.12142801284790039s

评论

1赞 Gringo Suave 1/31/2018
正如你所发现的,使用 printf 和 .format 来连接字符串的效率更低。
4赞 Martlark 4/5/2019 #7

我在 Roee Gavirel 的代码中添加了 2 个额外的测试,这些测试最终表明,在 Python 3.6 之前,将列表连接成字符串并不比 s += “something” 快。更高版本具有不同的结果。

结果:

Python 2.7.15rc1    

Iterations: 100000
format    done in 0.317540168762s
%s        done in 0.151262044907s
list+join done in 0.0055148601532s
str cat   done in 0.00391721725464s
    
Python 3.6.7

Iterations: 100000
format    done in 0.35594654083251953s
%s        done in 0.2868080139160156s
list+join done in 0.005924701690673828s
str cat   done in 0.0054128170013427734s
f str     done in 0.12870001792907715s

Python 3.8.5

Iterations: 100000
format    done in 0.1859891414642334s
%s        done in 0.17499303817749023s
list+join done in 0.008001089096069336s
str cat   done in 0.014998912811279297s
f str     done in 0.1600024700164795s

法典:

from time import time


def _with_cat(i):
    _st = ''
    for i in range(0, i):
        _st += "0"
    return _st


def _with_f_str(i):
    _st = ''
    for i in range(0, i):
        _st = f"{_st}0"
    return _st


def _with_format(i):
    _st = ''
    for i in range(0, i):
        _st = "{}{}".format(_st, "0")
    return _st


def _with_s(i):
    _st = ''
    for i in range(0, i):
        _st = "%s%s" % (_st, "0")
    return _st


def _with_list(i):
    l = []
    for i in range(0, i):
        l.append("0")
    return "".join(l)


def _count_time(name, i, func):
    start = time()
    r = func(i)
    total = time() - start
    print("%s done in %ss" % (name, total))
    return r


iteration_count = 100000

print('Iterations: {}'.format(iteration_count))
r1 = _count_time("format   ", iteration_count, _with_format)
r2 = _count_time("%s       ", iteration_count, _with_s)
r3 = _count_time("list+join", iteration_count, _with_list)
r4 = _count_time("str cat  ", iteration_count, _with_cat)
r5 = _count_time("f str    ", iteration_count, _with_f_str)

if len(set([r1, r2, r3, r4, r5])) != 1:
    print("Not all results are the same!")

评论

1赞 Lance Kind 8/16/2019
万岁,感谢来自“有时简单的方法就是最好的方法”部门。
0赞 Martlark 11/23/2021
在更高版本的 Python 3.8+ 中,list + join 的速度几乎是 s+= 的两倍
0赞 Karl Knechtel 3/25/2023
这样做的原因(在某种程度上确实如此)是引用实现 - 因为它使用引用计数进行垃圾回收 - 可以很容易地检查字符串是否有任何其他引用;如果它只在一个地方使用,那么就地修改它是安全的。因此,Python 只是“作弊”并在后台实现了一个可变字符串,该字符串大多数时候只是否认突变。当对字符串有其他引用时,必须复制以正确语义;但是在循环中,在第一次循环迭代之后,字符串不再有其他引用(你明白为什么了吗?
1赞 Karl Knechtel 3/25/2023
这类似于列表可以使用逻辑而不创建新对象的方式,即使这样做也是如此。当然,它是一种可变类型,即使它确实有其他引用,也可以修改列表。+=.extend+list+=
1赞 Jan Šimbera 11/21/2021 #8

Python 提供的最接近可变字符串或 StringBuffer 的东西可能是来自标准库模块的 Unicode 类型数组。在只想编辑字符串的一小部分的情况下,它可能很有用:array

modifications = [(2, 3, 'h'), (0, 6, '!')]
n_rows = multiline_string.count('\n')
strarray = array.array('u', multiline_string)
for row, column, character in modifications:
    strarray[row * (n_rows + 1) + column] = character
multiline_string = map_strarray.tounicode()
0赞 Timothy C. Quinn 7/24/2022 #9

这是我的实现:StringBuffer

class StringBuffer:
    def __init__(self, s:str=None):
        self._a=[] if s is None else [s]

    def a(self, v):
        self._a.append(str(v))
        return self

    def al(self, v):
        self._a.append(str(v))
        self._a.append('\n')
        return self

    def ts(self, delim=''):
        return delim.join(self._a)

    def __bool__(self): return True

用法:

sb = StringBuffer('{')
for i, (k, v) in enumerate({'k1':'v1', 'k2': 'v2'}.items()):
    if i > 0: sb.a(', ')
    sb.a('"').a(k).a('": ').a('"').a(v)
sb.a('}')
print(sb.ts('\n'))

这将输出 .{"k1": "v1, "k2": "v2}