【问题标题】:Modifying Python lists via slices and for-loops?通过切片和 for 循环修改 Python 列表?
【发布时间】:2018-07-18 01:46:34
【问题描述】:

我试图通过切片和 for 循环修改列表中的值,但遇到了一些非常有趣的行为。如果有人可以在这里解释内部发生的事情,我将不胜感激。

>>> x = [1,2,3,4,5]
>>> x[:2] = [6,7] #slices can be modified
>>> x
[6, 7, 3, 4, 5]
>>> x[:2][0] = 8 #indices of slices cannot be modified
>>> x
[6, 7, 3, 4, 5]
>>> x[:2][:1] = [8] #slices of slices cannot be modified
>>> x
[6, 7, 3, 4, 5]
>>> for z in x: #this version of a for-loop cannot modify lists
...    z += 1
... 
>>> x
[6, 7, 3, 4, 5]
>>> for i in range(len(x)): #this version of a for-loop can modify lists
...    x[i] += 1
... 
>>> x
[7, 8, 4, 5, 6]
>>> y = x[:2] #if I assign a slice to a var, it can be modified...
>>> y[0] = 1
>>> y
[1, 8]
>>> x #...but it has no impact on the original list
[7, 8, 4, 5, 6]

【问题讨论】:

  • 什么,究竟是你不明白?你期待什么?
  • 但本质上,切片会创建副本,但 切片分配 会改变底层列表。由于int对象是不可变的,z += 1只是简单的给变量z赋值一个新的列表,所以它等价于z = z + 1
  • 切片和切片分配是有区别的。前者创建一个副本,后者进行特殊的__setitem__ 调用。 See here.
  • 哇,我从没想过 Python 中的赋值如此复杂。感谢@Norrius 的链接,内容丰富。

标签: python python-3.x list for-loop slice


【解决方案1】:

让我们一一分解你的 cmets:

1.) x[:2] = [6, 7] 切片可以修改:

See these answers here。它从list 对象和assigning the slice 调用__setitem__ 方法。每次引用 x[:2] 时,都会创建一个新的切片对象(您可以简单地执行 id(x[:2]) ,很明显,它不会是相同的 id)。

2.) 切片索引不能修改:

这不是真的。无法修改它,因为您是在slice 实例上执行分配,而不是list,因此它不会触发__setitem__list 上执行。此外,int 是不可变的,因此无论哪种方式都无法更改。

3.) 切片切片不能修改:

见上文。同样的原因 - 您正在分配给切片的实例,而不是直接修改 list

4.) 这个版本的 for 循环不能修改列表:

这里引用的zx 元素中的实际对象。如果您使用 id(z) 运行 for 循环,您会注意到它们与 id(6), id(7), id(3), id(4), id(5) 相同。即使list 包含所有5 个相同的引用,当您执行z = ... 时,您只是将新值分配给对象z,而不是存储在list 中的对象。如果你想修改list,你需要通过索引来分配它,同样的原因你不能指望1 = 6会把x变成[6, 2, 3, 4, 5]

5.) 这个版本的 for 循环可以修改列表:

见我上面的回答。现在您直接在 list 上执行项目分配,而不是它的表示。

6.) 如果我将切片分配给 var,则可以对其进行修改:

如果您到目前为止一直在关注,您会发现现在您将x[:2] 的实例分配给对象y,现在它是list。故事如下——你在y上按索引执行项目分配,当然会更新。

7.) ...但它对原始列表没有影响:

当然。 xy 是两个不同的对象。 id(x) != id(y),因此对x 执行的任何操作都不会影响y。但是,如果您分配了y = x,然后更改为y,那么是的,x 也会受到影响。

要扩展一点for z in x:,假设您有一个class foo(),并将其两个实例分配给列表f

f1 = foo()
f2 = foo()
f = [f1, f2]
f
# [<__main__.foo at 0x15f4b898>, <__main__.foo at 0x15f4d3c8>]

请注意,有问题的引用是实际的 foo 实例,而不是对象 f1f2。所以即使我做了以下事情:

f1 = 'hello'
f
#  [<__main__.foo at 0x15f4b898>, <__main__.foo at 0x15f4d3c8>]

f 仍然保持不变,因为 foo 实例保持不变,即使对象 f1 现在被分配了不同的值。出于同样的原因,每当您对for z in x: 中的z 进行更改时,您只会影响z 对象,但在您按索引更新x 之前,列表中的任何内容都不会更改。

如果对象有属性或者是可变的,你可以直接在循环中更新被引用的对象:

x = ['foo']
y = ['foo']
lst = [x,y]
lst
# [['foo'], ['foo']]
for z in lst:
    z.append('bar')

lst
# [['foo', 'bar'], ['foo', 'bar']]
x.append('something')
lst
# [['foo', 'bar', 'something'], ['foo', 'bar']]

这是因为您直接更新引用中的对象,而不是分配给对象z。但是,如果您将xy 分配给一个新对象,lst 将不会受到影响。

【讨论】:

  • 关于最后一点,“无论何时您对 for z in x: 中的 z 进行更改,您只会影响对象 z,但在您更新 x 之前,列表中的任何内容都不会更改按索引”,如果 z 是可变对象,这仍然是真的吗?例如,如果 x 是一个列表列表,并且我执行了for z in x: z.append(1),那应该修改 x 中的所有 z,因为列表是可变的,不像整数,对吧?
  • 不。 x 中的对象直接引用对象本身。请参阅我的上一个f = [f1, f2] 示例。您可以看到被引用的对象是实际的foo 实例,而不是f1f2。因此,即使您将 f1f2 分配给其他对象,f 也不会更改,因为 foo 实例不会更改。不过,如果您真的更新了 foo 实例,情况就不同了。
  • 例如,如果你在foo中有一个类属性name,如果你分配了f1.name = "hello",那么f中的第一个foo实例也会用类属性体现改变。
【解决方案2】:

这里没有什么奇怪的事情发生。您从列表中获得的任何切片都是包含原始列表副本的新对象。元组也是如此。

当你遍历你的列表时,你会得到迭代产生的对象。由于 ints 在 Python 中是不可变的,因此您无法更改 int 对象的状态。每次添加两个ints 时,都会创建一个新的int 对象。因此,您的“for-loop [which] 无法修改列表的版本”并没有真正尝试修改任何内容,因为它不会将添加的结果分配回列表。

也许您现在可以猜到为什么您的第二种方法不同了。它使用一种特殊的切片语法,它不会真正创建列表的切片,并允许您分配给列表 (documentation)。添加操作创建的新创建的对象通过此方法存储在列表中。

为了理解您的最后一个(也是您的第一个)示例,重要的是要知道切片会创建(至少对于列表和元组,从技术上讲,您可以在自己的类中覆盖它)列表的部分副本。正如您已经发现的那样,对这个新对象的任何更改都不会更改原始列表中的任何内容。

【讨论】:

    猜你喜欢
    • 2018-10-18
    • 2018-12-01
    • 2020-05-22
    • 1970-01-01
    • 2018-12-29
    • 1970-01-01
    • 1970-01-01
    • 2019-12-01
    • 2017-07-09
    相关资源
    最近更新 更多