Python中的列表变异

3 投票
2 回答
5242 浏览
提问于 2025-04-17 15:25

我尝试通过交换一个列表和另一个参考列表的第一个元素来改变这个列表。下面是我的实现代码:

>>> L = [1,2,3,4,5,6,7,8,9]
>>> A = [3]
>>> L[0], L[L.index(A[0])] = L[L.index(A[0])], L[0] #want to swap 3 with 1  
>>> L 
[1,2,3,4,5,6,7,8,9,] #List L was not mutated  

但是,列表并没有像我预期的那样被改变。不过,当我按照下面的方式修改实现后,它就成功了:

>>> L = [1,2,3,4,5,6,7,8,9]
>>> A = [3]
>>> i = L.index(A[0])
>>> L[0], L[i] = L[i], L[0]
>>> L
[3,2,1,4,5,6,7,8,9,] #Now list mutated as desired even though L[i] and L[L.index(A[0])] evaluate to same value.  

我想问的是,为什么第一次的赋值没有改变列表?我想了想,但就是无法理解这个原因。

2 个回答

12

在Python中,当你进行多个赋值时,右边的表达式会被计算,而左边的赋值目标,如果里面有表达式,会一个一个地被计算。

如果你希望左边的目标先被计算,那样当然可以正常工作。

这个在赋值语句的部分中有说明:

赋值语句会计算表达式列表(记住,这可以是一个单独的表达式,也可以是用逗号分隔的列表,后者会生成一个元组),然后将得到的单一结果对象从左到右赋值给每个目标列表。

还有:

如果目标列表是用逗号分隔的目标:对象必须是一个可迭代的,且其项的数量要和目标列表中的目标数量相同,项会从左到右赋值给对应的目标。

这里的“从左到右”非常重要。L[0]会在L[L.index(3)]之前被赋值。

文档接着详细描述了像L[0]L[L.index(3)]这样的订阅目标会发生什么:

如果目标是一个订阅:引用中的主要表达式会被计算。它应该返回一个可变序列对象(比如列表)或一个映射对象(比如字典)。接下来,订阅表达式会被计算。

再次强调;订阅表达式是单独计算的,由于目标列表是从左到右计算的,这个计算会在对L[0]的赋值之后进行。

你可以通过拆解Python代码来看到这一点:

>>> import dis
>>> def f(L):
...     L[0], L[2] = L[2], L[0]
... 
>>> def g(L):
...     L[0], L[L.index(3)] = L[L.index(3)], L[0]
... 
>>> dis.dis(f)
  2           0 LOAD_FAST                0 (L)   # L[2]
              3 LOAD_CONST               1 (2)
              6 BINARY_SUBSCR       
              7 LOAD_FAST                0 (L)   # L[0]
             10 LOAD_CONST               2 (0)
             13 BINARY_SUBSCR       
             14 ROT_TWO             
             15 LOAD_FAST                0 (L)   # Store in L[0]
             18 LOAD_CONST               2 (0)
             21 STORE_SUBSCR        
             22 LOAD_FAST                0 (L)   # Store in L[2]
             25 LOAD_CONST               1 (2)
             28 STORE_SUBSCR        
             29 LOAD_CONST               0 (None)
             32 RETURN_VALUE        
>>> dis.dis(g)
  2           0 LOAD_FAST                0 (L)   # L[L.index(3)]
              3 LOAD_FAST                0 (L)
              6 LOAD_ATTR                0 (index)
              9 LOAD_CONST               1 (3)
             12 CALL_FUNCTION            1
             15 BINARY_SUBSCR       
             16 LOAD_FAST                0 (L)  #  L[0]
             19 LOAD_CONST               2 (0)
             22 BINARY_SUBSCR       
             23 ROT_TWO             
             24 LOAD_FAST                0 (L)  # Store in L[0]
             27 LOAD_CONST               2 (0)
             30 STORE_SUBSCR        
             31 LOAD_FAST                0 (L)  # Store in L[L.index(3)]
             34 LOAD_FAST                0 (L)
             37 LOAD_ATTR                0 (index)
             40 LOAD_CONST               1 (3)
             43 CALL_FUNCTION            1
             46 STORE_SUBSCR        
             47 LOAD_CONST               0 (None)
             50 RETURN_VALUE        

存储操作首先执行L[0] = 3,所以接下来调用L.index(3)会返回0,因此1又会被存回位置0

下面的代码是可以正常工作的:

L[L.index(3)], L[0] = L[0], L[L.index(3)]

因为现在L.index(3)的查找是先执行的。不过,最好把.index()调用的结果存到一个临时变量中,因为不重复调用.index()在任何情况下都会更高效。

7

问题在于这两者并不相同。第一个例子就像是在做:

>>> L = [1,2,3,4,5,6,7,8,9]
>>> A = [3]
>>> i = L.index(A[0])
>>> L[0] = L[i]
>>> i = L.index(A[0])
>>> L[i] = L[0]

这意味着你先交换了两个元素,然后再找到你刚刚交换的那个元素,再把它换回来。

你感到困惑的原因是,你把元组赋值看成是Python同时做了两件事——但实际上并不是这样,执行是有顺序的,这样就会改变结果。

值得注意的是,即使这样做有效,这也不是一个好的方法。list.index()这个操作本身并不快,所以无缘无故地做两次就不是个好主意。

撰写回答