【问题标题】:Why does the original list change?为什么原来的名单变了?
【发布时间】:2017-07-05 13:18:45
【问题描述】:

运行这个:

a = [[1], [2]]
for i in a:
    i *= 2
print(a)

给予

[[1, 1], [2, 2]]

我希望得到原始列表,就像这里发生的那样:

a = [1, 2]
for i in a:
    i *= 2
print(a)

这给出了:

[1, 2]

为什么要修改第一个示例中的列表?

【问题讨论】:

  • 因为您将嵌套列表对象相乘,这些对象是可变的并且就地更新。第二个示例乘以整数,这些整数是不可变的,因此无法就地更新并返回一个新整数。
  • @MartijnPieters 它们是可变的很清楚,它们是就地更新的需要解释
  • @mpf82 不重复:我需要知道就地更新意味着什么
  • 测试以下内容:for i in a: print(hex(id(i))),注意地址。现在这样做:for i in a: i *= 2; print(hex(id(i)))。比较地址。您将意识到,在元素不可变(整数)的情况下,地址会发生变化,而在元素可变(列表)的情况下,地址是相同的。这就是为什么元素在一种情况下被修改而不在另一种情况下被修改的原因:当返回相同的对象可变时,当不可变时,它是不同地址的副本,因此修改它不会修改原始对象。
  • 列表是可变的;整数不是。列表上的 *= 运算符会改变列表。 int 上的 *= 运算符返回一个新 int(分配给运算符左侧的变量)。

标签: python list augmented-assignment


【解决方案1】:

您正在使用augmented assignment statements。这些对左侧命名的对象进行操作,使该对象有机会就地更新

像 x += 1 这样的增强赋值表达式可以重写为 x = x + 1 以实现类似但不完全相等的效果。在增强版本中,x 只被评估一次。 此外,如果可能,实际操作会在原地执行,这意味着不是创建新对象并将其分配给目标,而是修改旧对象。

(我的粗体强调)。

这是通过让对象实现__i[op]__ 方法来实现的,因为=* 就是__imul__ hook

调用这些方法来实现增强的算术赋值(+=-=*=@=/=//=%=**=<<=>>=&=^=|=)。 这些方法应该尝试就地执行操作(修改self)并返回结果(可以是,但不一定是self)。

在列表上使用*= 会乘以该列表对象并返回相同的列表对象 (self) 以“分配”回相同的名称。

另一方面,整数是不可变对象。整数的算术运算返回 new 整数对象,所以int 对象甚至没有实现__imul__ 钩子;在这种情况下,Python 必须回退到执行 i = i * 3

所以对于第一个例子,代码:

a = [[1], [2]]
for i in a:
    i *= 2

确实是这样做的(为了说明目的,循环展开):

a = [[1], [2]]
i = a[0].__imul__(2)  # a[0] is altered in-place
i = a[1].__imul__(2)  # a[1] is altered in-place

list.__imul__ 方法将更改应用于列表对象本身,并且返回对列表对象的引用。

对于整数,会改为执行:

a = [1, 2]
i = a[0] * 2  # a[0] is not affected
i = a[1] * 2  # a[1] is not affected

所以现在 new 整数对象被分配给i,它独立于a

【讨论】:

    【解决方案2】:

    每个示例的结果不同的原因是列表是可变的但整数不是

    这意味着当你修改一个整数对象时,操作符必须返回一个新的整数对象。但是,由于列表是可变的,因此更改只是添加到已经存在的列表对象中。

    当您在第一个示例的 for 循环中使用 *= 时,Python 会修改已经存在的列表。但是当您将*= 与整数一起使用时,必须返回一个新的整数对象。

    这也可以通过一个简单的例子来观察:

    >>> a = 1
    >>> b = [1]
    >>> 
    >>> id(a)
    1505450256
    >>> id(b)
    52238656
    >>> 
    >>> a *= 1
    >>> b *= 1
    >>> 
    >>> id(a)
    1505450256
    >>> id(b)
    52238656
    >>>
    

    正如你在上面看到的,a 的内存地址在我们就地相乘时发生了变化。所以*= 返回了一个新对象。但是列表的内存没有改变。这意味着*= 就地修改了列表对象。

    【讨论】:

      【解决方案3】:

      在第一种情况下,for 循环中的ilist。所以你告诉 python 嘿,拿ith 列表并重复两次。您基本上重复列表 2 次,这就是 * 运算符对列表所做的操作。
      在第二种情况下,您的 i 是一个值,因此您将 * 应用于一个值,不是列表。

      【讨论】:

      • 我不需要知道这些操作是做什么的。我需要知道为什么要修改原始列表。
      • @dangonfast 那你为什么要问这个问题?也许您需要回答您尝试这样做的更重要的总体原因?
      • @J0hn 我从来没有问过这些操作是做什么的,只是为什么要修改列表。该操作与问题完全无关。
      • 我现在明白你的意思了,我认为你正在寻找的答案可以在 Martijn Pieters 和 dabadaba cmets 中找到。
      猜你喜欢
      • 1970-01-01
      • 2016-06-24
      • 2013-05-13
      • 1970-01-01
      • 1970-01-01
      • 2023-02-14
      • 1970-01-01
      • 1970-01-01
      • 2017-05-25
      相关资源
      最近更新 更多