我怎么知道Python是否在内存中为:`for item in nums[1:]`创建了一个新的子列表呢



我不是在问这个问题的答案,而是我自己是如何得到答案的。

原始问题:

以下代码是否导致Python在内存中创建一个新的大小列表(len(nums)-1),然后进行迭代

for item in nums[1:]:
# do stuff with item

原始答案

这里提出了一个类似的问题,Srinivas Reddy Thatipathy有一个子注释说,创建了一个新的子列表。但是,关于他是如何得出这个答案的,没有提供任何细节,我认为这与我想要的非常不同

问题:

我怎么能自己找到问题的答案呢

我以前也有过类似的问题。例如,我了解到,如果我做my_function(nums[1:]),我不会通过";切片";而是一个全新的、不同的子列表!我只是通过测试传递到my_function的原始列表是否在函数后进行了修改(事实并非如此)来发现这一点但我看不出有什么直接的方法来判断Python是否正在为for循环示例制作新的子列表请帮我知道怎么做。

旁注

顺便说一句,这是我使用的当前解决方案,来自最初的stackoverflow post解决方案:

for indx, item in enumerate(nums):
if indx == 0:
continue 
# do stuff w items 

通常,了解是否有新的数据块或只是对现有数据块的新引用的简单方法是通过一个引用修改数据,然后查看它是否也通过另一个引用进行了修改。(这听起来像是你的"艰难之路",但我建议将其作为一种通用技术。)一些伪代码看起来像:

function areSameRef(thing1, thing2){
thing1.modify()
return thing1.equals(thing2) //make sure this is not just a referential equality check
}

这种情况很少会失败,而且基本上需要幕后优化,即数据不会立即克隆,而是只有在修改时才会克隆。在这种情况下,基本数据是相同的这一事实对你来说是隐藏的,在大多数情况下,你应该相信隐藏的人知道他们在做什么。例外情况是他们做错了,或者你有一些复杂的性能问题。为此,您可能需要转向更特定于语言的调试或分析工具。(详见下文)

也要小心部分数据可能被共享的情况——例如,查找缺点列表和尾部共享。在这种情况下,如果你做了这样的事情:

function foo(list1, list2){
list1.append(someElement)
return list1.length == list2.length
}

将返回false-元素只添加到第一个列表中,但类似

function bar(list1, list2){
list1.set(someIndex, someElement)
return list1.get(someIndex)==list2.get(someIndex)
}

将返回true(尽管在实践中,以这种方式创建的列表通常没有允许可变的接口。)

我在第二部分没有看到任何问题,但是的,你的结论对我来说是有效的

编辑:更多关于实际内存使用情况的信息

正如您所指出的,在某些情况下,这种测试不起作用,因为您实际上没有两个引用,如for i in [nums 1:]的情况。在这种情况下,我会说求助于探查器,但你不能真正相信结果。

原因归结为编译器/解释器的工作方式,以及它们在语言规范中履行的合同一般规则是,解释器可以以任何不会改变结果,但可能会改变内存或时间性能的方式重新安排和修改代码的执行因此,如果代码的状态和所有I/O都相同,则foo(5)不可能在一个解释器实现/执行中返回6,在另一个解释器执行中返回7,但它们占用的时间和内存量非常不同是有效的。

这很重要,因为解释器和编译器所做的很多工作都是幕后优化;只要结果是一样的,他们会尽量让代码以尽可能快的速度运行,并且占用尽可能小的内存。然而,只有当它能够证明这些变化不会改变结果时,它才能这样做。

这意味着,如果你写一个简单的测试用例,解释器可能会在幕后对其进行优化,以最大限度地减少内存使用,并给你一个结果——";没有创建新的列表"但是,如果你试图相信真实代码中的结果,那么真实代码可能太复杂,编译器无法判断优化是否安全,而且可能会失败。它还可以取决于特定的解释器版本、环境变量或可用的硬件资源。

这里有一个例子:

def foo(x : int):
l = range(9999)
return 5
def bar(x:int):
l = range(9999)
if (x + 1 != (x*2+2)/2):
return l[x]
else:
return 5

我不能保证这适用于任何特定的语言,但我通常希望foobar有很多不同的内存用法。在foo中,任何创建良好的解释器都应该能够判断出l在超出范围之前从未被引用,因此可以自由跳过实际分配任何内存的操作,这是一个安全的操作。在bar中(除非我算术失败),l也永远不会被使用——但要知道这一点,需要对if语句的条件进行一些推理。这需要一个更聪明的解释器来识别,所以即使这两个代码片段在逻辑上看起来相同,它们在幕后的表现也可能截然不同。

EDIT:正如我所指出的,考虑到语言的动态特性,Python可能无法优化这两种语言;CCD_ 14函数和CCD_。如果没有python优化领域的专业知识,我就不能说他们做了什么或不做什么。无论如何,我把这篇文章留在这里是为了启发优化的一般概念,但把我的错误作为"关于优化的推理是困难的";。

话虽如此:FWIW,我强烈怀疑python解释器足够聪明,可以识别出for i in nums[1:]实际上不应该分配新内存,而只是在一个切片上迭代。在我看来,对于一个非常常见的用例来说,这是一个相对简单、安全和有价值的转换,所以我希望(高度优化的)python解释器能够处理它

EDIT2:最后(固执己见),我在Python中对此没有在几乎任何其他语言中那么自信,因为Python语法非常灵活,允许很多奇怪的事情。这使得python解释器(或人类)更难自信地说任何话,因为";合法python代码";太大了。这也是为什么我更喜欢像Rust这样更严格的语言的一个重要原因,它迫使程序员在行内着色,但会产生更可预测的行为。

EDIT3:作为最后一句话,通常对于这样的事情,最好相信执行环境正在处理这些低级优化。十有八九,在某些东西真正坏掉之前,不要试图解决这种性能问题。

关于列表切片是如何工作的,从语言引用Sequence Types——list、tuple、range,我们知道

s[i:j]-s从i到j的切片定义为具有索引k的项目使得i<=k<j.

因此,切片创建了一个新序列,但我们不知道该序列是否是一个列表,也不知道是否有某种巧妙的方法可以让同一列表对象以某种方式表示这两个序列。对于python语言规范来说,这并不奇怪,在该规范中,列表被描述为序列的一般讨论的一部分,并且该规范从未真正尝试涵盖对象实现的所有细节。

这是因为最终,像nums[1:]这样的东西实际上只是nums.__getitem__(slice(1, None))的语法糖,这意味着列表可以自己决定切片的含义。您需要找到实现的源代码。参见listobject.c 中的list_subscript函数

但我们可以试验。看一下对声明的陈述,

for_stmt::=";对于";target_list";在";starred_list":"一套["其他":"套房"]starred_list表达式求值一次;它应该产生一个可迭代的对象。

因此,nums[1:]是一个必须生成可迭代对象的表达式,我们可以将该对象分配给一个中间变量。

nums = [1 ,2, 3]
tmp = nums[1:]
for item in tmp:
pass
tmp[0] = "new stuff"
assert id(nums) != id(tmp), "List slice creates a new object"
assert type(tmp) == type(nums), "List slice creates a new list"
assert 999 not in nums, "List slice doesn't affect original"

运行该程序,如果两个断言错误都没有出现,那么您就知道创建了一个新列表。

其他类似序列的对象的工作方式可能完全不同。例如,在numpy数组中,两个数组对象可能确实引用了相同的内存。在本例中,将引发最终断言,因为切片是同一数组中的另一个视图。是的,这会让你整晚都睡不着。

import numpy as np
nums = np.array([1,2,3])
tmp = nums[1:]
for item in tmp:
pass
tmp[0] = 999
assert id(nums) != id(tmp), "array slice creates a new object"
assert type(tmp) == type(nums), "array slice creates a new list"
assert 999 not in nums, "array slice doesn't affect original"

您可以使用新的Walrus操作符:=来捕获Python为切片创建的临时对象。一点调查表明它们不是同一个物体。

import sys
print(sys.version)
a = list(range(1000))
for i in (b := a[1:]):
b[0] = 906
print(b is a)
print(a[:10])
print(b[:10])
print(sys.getsizeof(a))
print(sys.getsizeof(b))

生成以下输出:

3.11.0 (main, Nov  4 2022, 00:14:47) [GCC 7.5.0]
False
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[906, 2, 3, 4, 5, 6, 7, 8, 9, 10]
8056
8048

您可以在Godbolt编译器资源管理器中查看编译器生成的代码。

最新更新