为什么,在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
当添加的字符串的引用计数为1时,CPython对字符串添加进行了优化。
这是因为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
答案 0 :(得分:9)
在基于列表的方法中,列表中索引0的字符串被采用并修改,然后被放回到索引0的列表中。
对于这个短暂的时刻,解释器仍然在列表中具有旧版本的字符串,并且不能执行就地修改
如果您查看Python's source,那么您将看到不支持修改列表元素。因此,必须从列表中检索对象(在这种情况下为字符串),修改然后放回
换句话说,list
类型与str
运算符的+=
类型支持完全无关。
并考虑以下代码:
l = ['abc', 'def']
def nasty():
global l
l[0] = 'ghi'
l[1] = 'jkl'
return 'mno'
l[0] += nasty()
l
的值为['abcmno', 'jkl']
,证明'abc'
已从列表中删除,然后nasty()
执行修改列表的内容,字符串{{1并且'abc'
已连接,结果已分配给'mno'
。如果在访问l[0]
之前评估了nasty()
以对其进行修改,那么结果将为l[0]
。
答案 1 :(得分:6)
那么为什么列表版本会创建两个引用?
在l[0] += ' '
中,一个参考位于l[0]
。暂时创建一个引用以执行+=
。
以下是两个更简单的函数来显示效果:
>>> def f():
... l = ['']
... l[0] += ' '
...
>>> def g():
... s = ''
... s += ' '
...
反汇编它们
>>> from dis import dis
>>> dis(f)
2 0 LOAD_CONST 1 ('')
3 BUILD_LIST 1
6 STORE_FAST 0 (l)
3 9 LOAD_FAST 0 (l)
12 LOAD_CONST 2 (0)
15 DUP_TOPX 2
18 BINARY_SUBSCR
19 LOAD_CONST 3 (' ')
22 INPLACE_ADD
23 ROT_THREE
24 STORE_SUBSCR
25 LOAD_CONST 0 (None)
28 RETURN_VALUE
>>> dis(g)
2 0 LOAD_CONST 1 ('')
3 STORE_FAST 0 (s)
3 6 LOAD_FAST 0 (s)
9 LOAD_CONST 2 (' ')
12 INPLACE_ADD
13 STORE_FAST 0 (s)
16 LOAD_CONST 0 (None)
19 RETURN_VALUE
在f
中,BINARY_SUBSCR
(切片)指令将l[0]
放在VM堆栈的顶部。 DUP_TOPX
复制堆栈中的顶部 n 项。这两个函数(参见ceval.c
)都会增加引用计数; DUP_TOPX
(Py3中的DUP_TOP_TWO
)直接执行此操作,而BINARY_SUBSCR
使用PyObject_GetItem
。因此,字符串的引用计数现在至少为三。
g
没有这个问题。当使用LOAD_FAST
推送项目时,它会创建一个额外的引用,给出引用计数为2,即VM堆栈上项目的最小数量,以便它可以进行优化。