【问题标题】:If Python slice copy the reference, why can't I use it to modify the original list?如果 Python 切片复制引用,为什么我不能用它来修改原始列表?
【发布时间】:2020-05-03 19:11:50
【问题描述】:

我知道Slicing lists does not generate copies of the objects in the list; it just copies the references to them.

但如果是这样,那为什么这行不通呢?

l = [1, 2, 3]

# Attempting to modify the element at index 1
l[0:2][-1] = 10

# but the attempt fails. The original list is unchanged
l
> [1, 2, 3]

l[0:2][-1] 不应该指向原始列表索引 1 处的元素吗?

【问题讨论】:

  • 不能修改元素本身;它是不可变的。分配给列表的一个元素会用另一个引用替换一个引用。
  • 另外,“复制列表”(浅拷贝)和“复制列表的对象”(深拷贝)是有区别的。

标签: python list reference


【解决方案1】:

您是对的,切片不会复制列表中的项目。但是,它确实会创建一个新的列表对象。

您的评论表明存在误解:

# Attempting to modify the element at index 1
l[0:2][-1] = 10

这不是对元素的修改,而是对列表的修改。换句话说,它实际上是“更改列表,使索引 1 现在指向数字 10”。由于您的切片创建了一个新列表,因此您只是将该新列表更改为指向其他对象。

在您对 oldrinb 的回答的评论中,您说:

为什么l[0:1] 和l[0:1][0] 不同?他们不应该都引用同一个对象,即l的第一项吗?

除了l[0:1] 是一个列表而l[0:1][0] 是单个元素这一事实之外,这里再次存在同样的误解。假设some_list 是一个列表,索引ix 处的对象是obj。这个:

some_list[ix] = blah

。 . .是对some_list 的操作。不涉及对象obj。这可能会令人困惑,因为这意味着 some_list[ix] 的语义略有不同,具体取决于它位于分配的哪一侧。如果你这样做了

blah = some_list[ix] + 2

。 . .then 你确实在对列表内的对象进行操作(即,它与obj + 2 相同)。但是当索引操作在赋值的左边时,它根本就不再涉及到被包含的对象,只涉及到列表本身。

当您分配给列表索引时,您正在修改 列表,而不是其中的对象。因此,在您的示例中,l[0] 与l[0:2][0] 相同,但这并不重要;因为您的索引是一个分配目标,它正在修改列表并且不在乎那里已经有什么对象。

【讨论】:

  • 接受这个答案,因为它是我误解的核心。谢谢!
【解决方案2】:

对list 进行切片会返回一个新浅复制的list 对象。虽然您是正确的,它不会深度复制原始列表的项目,但结果是与原始列表不同的全新 list。

见Python 3 tutorial:

所有切片操作都会返回一个包含请求元素的新列表。这意味着以下切片返回列表的浅表副本:

>>> squares = [1, 4, 9, 16, 25]
>>> squares[:]
[1, 4, 9, 16, 25]

考虑

>>> squares[:] is squares
False

【讨论】:

  • 即使切片是一个新列表,它仍然包含对旧对象的引用。 (这就是我认为“浅拷贝”的意思。)这是有道理的,因为l = [1,2,3]; l[0:1] = [10] 确实修改了列表。但是,l[0:1][0] = [10] 不会修改该列表。为什么l[0:1] 和l[0:1][0] 不同?他们不应该都引用同一个对象,即l的第一项吗?
  • @Heisenberg l[0:1] = [10] 不是从slice 构造一个新的list——它是使用slice 分配给现有的list。另一方面,l[0:1][0] = 10首先使用slice构造一个新的list,然后然后使用一个简单的索引(0 )。
  • 区别在于l[0:1] = [10] 等价于l.__setitem__(slice(0, 1), [10]) 而l[0:1][0] = 10 类似于l.__getitem__(slice(0, 1)).__setitem__(0, 10)
  • 首先,您在l 上调用__setitem__,从而改变了原始列表;然而,在第二个中,您将新创建的 list 与 l.__getitem__ 进行变异。为了更清楚地了解发生了什么,请考虑引入一个中间变量,如下所示:l2 = l[0:1]; l2[0] = 10。之后检查l2 你会注意到l2 == [10] 和l 没有改变
  • @Heisenberg:我添加了一个可以澄清问题的答案。要点是,当您分配给列表索引时,您不会影响列表中的对象;您只是在修改列表本身(因此您正在修改切片创建的新列表)。
【解决方案3】:

为了更好地解释它,假设你已经写了

l = [1, 2, 3]
k = l[0:2]
k[-1] = 10

我希望你能同意这是等价的。

现在让我们分解各个语句:

l = [1, 2, 3]

这将创建以下对象和引用:

id  object
--  --------
0   <int 1>
1   <int 2>
2   <int 3>
3   <list A>
name  →  id
----     --
l     →  3
l[0]  →  0
l[1]  →  1
l[2]  →  2

k = l[0:2]

这将创建一个新列表 &lt;list B&gt;,其中包含 l 中包含的引用的副本:

标识对象 ---------- 0 1 2 3 4 姓名→身份证 ---- -- l → 3 l[0] → 0 l[1] → 1 l[2] → 2 k → 4 k[0] → 0(l[0] 的副本) k[1] → 1(l[1] 的副本)

k[-1] = 10

首先,索引 -1 解析为索引 1(因为 k 的长度为 2),所以这等价于 k[1] = 10。这个赋值意味着对象和引用会这样更新:

标识对象 ---------- 0 1 2 3 4 5 姓名→身份证 ---- -- l → 3 l[0] → 0 l[1] → 1 l[2] → 2 k → 4 k[0] → 0 k[1] → 5

注意l 和l[0] 到l[2] 不受此影响。 QED。

【讨论】:

    猜你喜欢
    • 2015-07-22
    • 1970-01-01
    • 2021-10-26
    • 1970-01-01
    • 1970-01-01
    • 2019-01-09
    • 1970-01-01
    • 1970-01-01
    • 2021-10-28
    相关资源
    最近更新 更多