问题
为什么在CPython中,
def add_string(n):
s = ''
for _ in range(n):
s += ' '
需要花费线性时间,但是
def add_string_in_list(n):
l = ['']
for _ in range(n):
l[0] += ' '
需要二次时间?
证明:
Timer(partial(add_string, 1000000)).timeit(1)
#>>> 0.1848409200028982
Timer(partial(add_string, 10000000)).timeit(1)
#>>> 1.1123797750042286
Timer(partial(add_string_in_list, 10000)).timeit(1)
#>>> 0.0033865350123960525
Timer(partial(add_string_in_list, 100000)).timeit(1)
#>>> 0.25131178900483064
我的理解
CPython在字符串引用计数为1时有一个优化,用于字符串相加。
这是因为Python中的字符串是不可变的,因此通常不能进行编辑。如果存在对字符串的多个引用,并且对其进行了更改,所有引用都将看到更改后的字符串。显然,这并不是我们想要的,因此不能使用多个引用进行更改操作。
但是,如果只有一个引用指向该字符串,则更改该值只会更改该引用所需的字符串。您可以通过以下方式测试这可能是原因:
from timeit import Timer
from functools import partial
def add_string_two_references(n):
s = ''
for _ in range(n):
s2 = s
s += ' '
Timer(partial(add_string_two_references, 20000)).timeit(1)
#>>> 0.032532954995986074
Timer(partial(add_string_two_references, 200000)).timeit(1)
#>>> 1.0898985149979126
我不确定为什么这个因素只有30倍,而不是预期的100倍,但我认为这是开销问题。
我不知道的事情
那么为什么列表版本会创建两个引用呢?这是否是阻止优化的原因?
您可以检查它是否对普通对象进行了任何区别处理:
class Counter:
def __iadd__(self, other):
print(sys.getrefcount(self))
s = Counter()
s += None
#>>> 6
class Counter:
def __iadd__(self, other):
print(sys.getrefcount(self))
l = [Counter()]
l[0] += None
#>>> 6
x = 'a'; y = x; x = 'b'
还是x = 'a'; y = x; x += 'b'
,y
都仍然是 'a'。 - ElmoVanKielmo+=
是一种特殊的部分优化情况,但建议使用.join()
,因为它在所有实现中都得到保证的优化,并且将受到开发人员的青睐。 - Davidmh