因此,我经常按照以下模式编写代码:
_list = list(range(10)) # Or whatever
_list = [some_function(x) for x in _list]
_list = [some_other_function(x) for x in _list]
我现在看到了一个不同的问题,有一条评论解释了这种方法每次都会创建一个新列表,更好的方法是改变现有的列表,像这样:
etc
_list[:] = [some_function(x) for x in _list]
这是我第一次看到这样明确的建议,我想知道其影响:
这种变异是否节省内存?重新分配后,“旧”列表的引用应该会降至零,而“旧”列表将被忽略,但在发生这种情况之前是否存在延迟,在使用重新分配而不是突变列表时可能会使用比所需更多的内存?
使用变异是否存在计算成本?我怀疑,与创建一个新列表并丢弃旧列表相比,改变某些地方的原地操作更加昂贵?
就安全性而言,我编写了一个脚本来测试这个问题:
def some_function(number: int):
return number*10
def main():
_list1 = list(range(10))
_list2 = list(range(10))
a = _list1
b = _list2
_list1 = [some_function(x) for x in _list1]
_list2[:] = [some_function(x) for x in _list2]
print(f"list a: {a}")
print(f"list b: {b}")
if __name__=="__main__":
main()
这将产生以下输出:
list a: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
list b: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90]
变异似乎有一个缺点,即更容易引起副作用。尽管这些可能是可取的。是否有任何PEP讨论这个安全方面或其他最佳实践指南?
谢谢。
编辑:矛盾的答案:因此需要对内存进行更多测试
迄今为止,我已经收到了两个相互矛盾的答案。在评论中,jasonharper写道,等式的右侧不知道左侧的情况,因此内存使用不可能受到左侧出现的影响。然而,在答案中,Masoud写道,“当重新分配时,将创建具有两个不同标识和值的新旧列表。之后,旧的列表被垃圾回收。但是当容器发生变异时,每个单个值都会被检索,CPU中进行更改并逐个更新。所以列表没有被复制。”这似乎表明重新分配存在很大的内存成本。
我决定尝试使用memory-profiler深入挖掘。以下是测试脚本:
from memory_profiler import profile
def normalise_number(number: int):
return number%1000
def change_to_string(number: int):
return "Number as a string: " + str(number) + "something" * number
def average_word_length(string: str):
return len(string)/len(string.split())
@profile(precision=8)
def mutate_list(_list):
_list[:] = [normalise_number(x) for x in _list]
_list[:] = [change_to_string(x) for x in _list]
_list[:] = [average_word_length(x) for x in _list]
@profile(precision=8)
def replace_list(_list):
_list = [normalise_number(x) for x in _list]
_list = [change_to_string(x) for x in _list]
_list = [average_word_length(x) for x in _list]
return _list
def main():
_list1 = list(range(1000))
mutate_list(_list1)
_list2 = list(range(1000))
_list2 = replace_list(_list2)
if __name__ == "__main__":
main()
请注意,我知道例如这个查找平均单词长度的函数并不是特别好写的。只是为了测试而已。
以下是结果:
Line # Mem usage Increment Line Contents
================================================
16 32.17968750 MiB 32.17968750 MiB @profile(precision=8)
17 def mutate_list(_list):
18 32.17968750 MiB 0.00000000 MiB _list[:] = [normalise_number(x) for x in _list]
19 39.01953125 MiB 0.25781250 MiB _list[:] = [change_to_string(x) for x in _list]
20 39.01953125 MiB 0.00000000 MiB _list[:] = [average_word_length(x) for x in _list]
Filename: temp2.py
Line # Mem usage Increment Line Contents
================================================
23 32.42187500 MiB 32.42187500 MiB @profile(precision=8)
24 def replace_list(_list):
25 32.42187500 MiB 0.00000000 MiB _list = [normalise_number(x) for x in _list]
26 39.11328125 MiB 0.25781250 MiB _list = [change_to_string(x) for x in _list]
27 39.11328125 MiB 0.00000000 MiB _list = [average_word_length(x) for x in _list]
28 32.46484375 MiB 0.00000000 MiB return _list
我发现即使我将列表大小增加到100000,重新分配仍然会使用更多的内存,但是只增加了大约1%左右。这使我认为额外的内存成本可能只是某个地方的额外指针,而不是整个列表的成本。
为了进一步测试假设,我以0.00001秒的间隔进行了基于时间的分析,并绘制了结果图。我想看看是否有瞬间的内存使用量峰值,由于垃圾回收(引用计数)而立即消失。但遗憾的是,我没有找到这样的峰值。
有人能解释这些结果吗?在这里究竟发生了什么导致内存使用略微但持续地增加?
_list = (some_function(x) for x in _list)
。 - Patrick Haugh_list[:] = [some_function(x) for x in _list]
会创建一个全新的列表,赋值语句右侧的求值不会知道左侧的操作。然后它会用新内容替换现有列表的内容,并且新列表将被处理掉。_list = ...
的内存需求完全相同,但速度更快,因为它跳过了删除/替换步骤。 - jasonharper_list[:] = ...
。在_list = ...
之后,对旧列表的引用仍然是指向旧列表。请注意不改变原意,让翻译通俗易懂。 - jasonharper