【发布时间】:2019-10-11 23:47:10
【问题描述】:
所以我经常按照这样的模式编写代码:
_list = list(range(10)) # Or whatever
_list = [some_function(x) for x in _list]
_list = [some_other_function(x) for x in _list]
等
我现在在另一个问题上看到了一条评论,该评论解释了这种方法如何每次都创建一个新列表,并且最好改变现有列表,如下所示:
_list[:] = [some_function(x) for x in _list]
这是我第一次看到这个明确的建议,我想知道这意味着什么:
1) 突变会节省内存吗?据推测,在重新分配后,对“旧”列表的引用将降至零,并且“旧”列表被忽略,但在此之前是否存在延迟,因为我可能使用的内存比我使用时需要的多重新分配而不是改变列表?
2) 使用变异是否有计算成本?我怀疑就地更改某些内容比创建新列表并删除旧列表更昂贵?
为了安全,我写了一个脚本来测试一下:
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 讨论此安全方面或其他最佳实践指南?
谢谢。
编辑:冲突的答案:所以更多的记忆测试 所以到目前为止,我收到了两个相互矛盾的答案。在 cmets 中,jasonharper 写道,等式的右侧不知道左侧,因此内存使用不可能受到左侧出现的内容的影响。然而,在答案中,Masoud 写道:“当使用 [reassignment] 时,会创建两个具有两个不同身份和值的新旧 _list。之后,旧 _list 会被垃圾收集。但是当容器发生变异时,每个单独的值被检索,在 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) -
这是一个非常好的观点@PatrickHaugh
-
_list[:] = [some_function(x) for x in _list]创建一个全新的列表 - 对作业右侧的评估不知道左侧将如何处理它。然后它用新的内容替换现有的列表内容,然后处理新的列表。_list = ...具有完全相同的内存要求,但速度更快,因为它跳过了删除/替换步骤。 -
如果其他内容引用了原始列表,并且您希望更新该引用,您可能仍需要使用
_list[:] = ...。在_list = ...之后,对旧列表的引用仍然是对旧列表的引用。 -
分配给列表切片没有什么“不安全”的,这是一个标准操作。我认为您对性能的担忧是过早的优化。如果你想要相同的列表具有相同的内存地址然后使用切片分配,否则不需要