【问题标题】:Why does mutating a list in a tuple raise an exception but mutate it anyway? [duplicate]为什么在元组中改变列表会引发异常但无论如何都会改变它? [复制]
【发布时间】:2020-10-18 04:43:26
【问题描述】:

我不确定我是否完全理解以下 mini sn-p 中发生的事情(在 Py v3.6.7 上)。如果有人能向我解释如何在 Python 抛出错误的情况下成功地改变列表,那就太好了。

我知道我们可以改变列表并更新它,但错误是什么?就像我的印象是,如果有错误,那么x 应该保持不变。

x = ([1, 2], )
x[0] += [3,4] # ------ (1)

第 (1) 行抛出的 Traceback 是

> TypeError: 'tuple' object doesn't support item assignment.. 

我了解错误的含义,但我无法了解它的上下文。

但是现在如果我尝试打印变量 x 的值,Python 会说,

print(x) # returns ([1, 2, 3, 4])

据我所知,在 Python 允许列表的突变发生之后发生了异常,然后希望它尝试重新分配它。我认为它吹到了那里,因为元组是不可变的。

有人能解释一下幕后发生了什么吗?

编辑 - 1 来自 ipython 控制台的错误作为图像;

【问题讨论】:

  • @Aaron 那么这意味着我们不应该使用 += 吗?就地操作 wrt 列表是否有某种权衡?
  • x[0].extend([3,4]) 确实按预期工作......
  • 简单的就地分配x += y 总是受欢迎的。使用切片/索引/属性时,需要考虑可变性。
  • 感谢所有答案!它们都是有用且独特的,但只能接受一个:(;谢谢大家!

标签: python python-3.x tuples immutability


【解决方案1】:

现有答案是正确的,但我认为文档实际上可以对此提供一些额外的说明:

来自in-place operators documentation

语句 x += y 等价于 x = operator.iadd(x, y)

所以当我们写

x[0] += [3, 4]

相当于

x[0] = operator.iadd(x[0], [3, 4])

iadd是使用extend实现的,在列表的情况下,所以我们看到这个操作实际上做了2件事:

  • 扩展列表
  • 重新分配给索引 0

如文档后面所述:

请注意,当调用就地方法时,计算和赋值是在两个单独的步骤中执行的。

第一次操作没问题

第二个操作是不可能的,因为x是一个元组。

但为什么要重新分配?

在这种情况下,这似乎令人费解,人们可能想知道为什么+=operator 等同于x = operator.iadd(x, y),而不是简单的operator.iadd(x, y)

这不适用于不可变类型,例如 int 和 str。因此,虽然 iadd 对于列表实现为 return x.extend(y),但对于整数,它实现为 return x + y

再次来自文档:

对于字符串、数字和元组等不可变目标,会计算更新后的值,但不会将其分配回输入变量

【讨论】:

    【解决方案2】:

    这里发生了一些事情。

    += 并不总是+ 然后是=

    +=+ 如果需要可以有不同的实现。

    看看这个例子。

    In [13]: class Foo: 
        ...:     def __init__(self, x=0): 
        ...:         self.x = x 
        ...:     def __add__(self, other): 
        ...:         print('+ operator used') 
        ...:         return Foo(self.x + other.x) 
        ...:     def __iadd__(self, other): 
        ...:         print('+= operator used') 
        ...:         self.x += other.x 
        ...:         return self 
        ...:     def __repr__(self): 
        ...:         return f'Foo(x={self.x})' 
        ...:                                                                        
    
    In [14]: f1 = Foo(10)                                                           
    
    In [15]: f2 = Foo(20)                                                           
    
    In [16]: f3 = f1 + f2                                                           
    + operator used
    
    In [17]: f3                                                                     
    Out[17]: Foo(x=30)
    
    In [18]: f1                                                                     
    Out[18]: Foo(x=10)
    
    In [19]: f2                                                                     
    Out[19]: Foo(x=20)
    
    In [20]: f1 += f2                                                               
    += operator used
    
    In [21]: f1                                                                     
    Out[21]: Foo(x=30)
    

    同样,列表类对++= 有单独的实现。

    使用+=实际上是在后台执行extend操作。

    In [24]: l = [1, 2, 3, 4]                                                       
    
    In [25]: l                                                                      
    Out[25]: [1, 2, 3, 4]
    
    In [26]: id(l)                                                                  
    Out[26]: 140009508733504
    
    In [27]: l += [5, 6, 7]                                                         
    
    In [28]: l                                                                      
    Out[28]: [1, 2, 3, 4, 5, 6, 7]
    
    In [29]: id(l)                                                                  
    Out[29]: 140009508733504
    

    使用+ 创建一个新列表。

    In [31]: l                                                                      
    Out[31]: [1, 2, 3]
    
    In [32]: id(l)                                                                  
    Out[32]: 140009508718080
    
    In [33]: l = l + [4, 5, 6]                                                      
    
    In [34]: l                                                                      
    Out[34]: [1, 2, 3, 4, 5, 6]
    
    In [35]: id(l)                                                                  
    Out[35]: 140009506500096
    

    我们现在来回答你的问题。

    In [36]: t = ([1, 2], [3, 4])                                                   
    
    In [37]: t[0] += [10, 20]                                                       
    ---------------------------------------------------------------------------
    TypeError                                 Traceback (most recent call last)
    <ipython-input-37-5d9a81f4e947> in <module>
    ----> 1 t[0] += [10, 20]
    
    TypeError: 'tuple' object does not support item assignment
    
    In [38]: t                                                                      
    Out[38]: ([1, 2, 10, 20], [3, 4])
    

    + 运算符在这里首先执行,这意味着列表被更新(扩展)。这是允许的,因为对列表的引用(存储在元组中的值)不会改变,所以这很好。

    = 然后尝试更新 tuple 内的引用,这是不允许的,因为元组是不可变的。

    但实际列表被+ 改变了。

    Python 未能更新对元组内列表的引用,但由于它会更新为相同的引用,因此我们作为用户看不到更改。

    因此,+ 被执行,= 无法执行。 +tuple 中已经引用的list 进行变异,因此我们在列表中看到了变异。

    【讨论】:

      【解决方案3】:

      我的直觉是 x[0] += [3, 4] 行首先修改列表本身,因此 [1, 2] 变为 [1, 2, 3, 4]然后它尝试调整抛出 TypeError 的元组的内容,但元组总是指向同一个列表,因此它的内容(就指针而言)不会被修改,而指向 的对象会被修改。

      我们可以这样验证:

      a_list = [1, 2, 3]
      a_tuple = (a_list,)
      print(a_tuple)
      >>> ([1, 2, 3],)
      
      a_list.append(4)
      print(a_tuple)
      >>> ([1, 2, 3, 4], )
      

      尽管存储在“不可变”元组中,但这不会引发错误并会对其进行适当的修改。

      【讨论】:

      • x[0] = x[0] + [3,4] 不起作用所以这似乎与+=有关
      • @Aaron 提出了一个很好的观点。我的评论的其余部分似乎是错误的:+=t 是两个操作数上+ 的简写,然后赋值给左操作数。解释器对存储在x[0] 中的可变数组执行+ 连接,并且在分配步骤尝试改变不可变元组之前不会引发错误。
      • 我相信你的直觉;所以我们应该避免使用+=?并且总是更喜欢extend/append
      • 因为__iadd__ 改变了列表,__add__ 创建了一个新列表。
      • @Aditya 这真的取决于你在做什么。断章取义,很难给出一个明确的答案。也许x 可能是您的列表,如果您需要将x 保留为元组,则可能首选extend/append。就我个人而言,我经常将+= 用于列表,因为我喜欢它的易读性,所以我真的不能告诉你总是 扩展。
      猜你喜欢
      • 1970-01-01
      • 2012-05-10
      • 2019-10-23
      • 2020-05-02
      • 1970-01-01
      • 1970-01-01
      • 2015-10-14
      • 2015-11-22
      相关资源
      最近更新 更多