gpt4 book ai didi

突变和重新分配列表之间的 Python 区别(列表 = 和列表 [:] = )

转载 作者:太空狗 更新时间:2023-10-29 21:48:05 25 4
gpt4 key购买 nike

所以我经常按照这样的模式编写代码:

_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 讨论此安全方面或其他最佳实践指南?

谢谢你。

编辑:相互矛盾的答案:对内存进行更多测试
所以到目前为止我收到了两个相互矛盾的答案。在评论中,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 秒的间隔执行了基于时间的分析并绘制了结果。我想看看是否有内存使用量的瞬时峰值由于垃圾收集(引用计数)而立即消失。但是,唉,我还没有发现这样的尖峰。

谁能解释这些结果?这里到底发生了什么导致内存使用量出现这种轻微但持续增加的情况?

最佳答案

很难规范地回答这个问题,因为实际细节是依赖于实现的,甚至是依赖于类型的。

例如在 CPython 当一个对象达到引用计数为零时,它就会被处理掉并立即释放内存。然而,有些类型有一个额外的“池”,它在你不知道的情况下引用实例。例如,CPython 有一个未使用的“池”list实例。当最后引用一个 list在 Python 代码中删除它 5 月 被添加到这个“空闲列表”而不是释放内存(需要调用一些东西 PyList_ClearFreeList 来回收该内存)。

但是列表不仅仅是列表所需的内存,一个列表包含 对象。即使当列表的内存被回收时,列表中的对象仍然可以保留,例如,在其他地方仍然存在对该对象的引用,或者该类型本身也有一个“空闲列表”。

如果您查看其他实现,例如 PyPy 那么即使没有“池”,当没有人再引用它时,对象也不会立即处理,它只会“最终”处理。

那么这与您可能想知道的示例有何关系。

让我们来看看你的例子:

_list = [some_function(x) for x in _list]

在这一行运行之前,有一个分配给变量 _list 的列表实例。 .然后你创建一个 新名单使用列表理解并将其分配给名称 _list .在此分配之前不久,内存中有两个列表。旧列表和由理解创建的列表。在分配之后,有一个名称引用的列表 _list (新列表)和一个引用计数减 1 的列表。如果旧列表没有在其他任何地方被引用并因此达到 0 的引用计数,它可能会返回到池中,它可能是处置或可能最终处置。旧列表的内容相同。

另一个例子呢:
_list[:] = [some_function(x) for x in _list]

在此行运行之前,再次为名称 _list 分配了一个列表。 .当该行执行时,它还会通过列表理解创建一个新列表。但不是将新列表分配给名称 _list它将用新列表的内容替换旧列表的内容。然而,当它清除旧列表时,它会有 两个保存在内存中的列表。在此分配之后,旧列表仍可通过名称 _list 获得。但是列表理解创建的列表不再被引用,它的引用计数为 0 并且会发生什么取决于它。它可以放入空闲列表的“池”中,可以立即处理,也可以在将来某个未知时间点处理。清除旧列表的原始内容也是如此。

那么区别在哪里:

其实并没有太大的区别。在这两种情况下,Python 都必须将两个列表完全保留在内存中。然而,第一种方法释放对旧列表的引用比第二种方法释放对内存中的中间列表的引用更快,仅仅是因为在复制内容时它必须保持事件状态。

然而,更快地释放引用并不能保证它实际上会导致“更少的内存”,因为它可能会返回到池中,或者实现只会在将来的某个(未知)点释放内存。

内存成本较低的替代方案

您可以链接迭代器/生成器并在需要迭代它们时使用它们(或者您需要实际列表),而不是创建和丢弃列表。

所以,而不是做:
_list = list(range(10)) # Or whatever
_list = [some_function(x) for x in _list]
_list = [some_other_function(x) for x in _list]

你可以这样做:
def generate_values(it):
for x in it:
x = some_function(x)
x = some_other_function(x)
yield x

然后简单地消耗它:
for item in generate_values(range(10)):
print(item)

或者用一个列表来消费它:
list(generate_values(range(10)))

这些不会(除非您将它传递给 list )创建任何列表。生成器是一种状态机,可在请求时一次处理一个元素。

关于突变和重新分配列表之间的 Python 区别(列表 = 和列表 [:] = ),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56308475/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com