3

我尝试通过用第一个元素交换列表和另一个引用列表之间的公共元素来改变列表。实现如下图所示:

>>> 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.  

我的问题是,为什么第一个任务不能改变列表?我想到了,但我的大脑无法解释。

4

2 回答 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)返回01因此存储在原位0

以下确实有效:

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

because now the L.index(3) lookup is done first. However, it's best to store the result of an .index() call in a temporary variable as not calling .index() twice is going to be more efficient in any case.

于 2013-02-10T14:32:40.297 回答
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()不是一个特别快的操作,所以无缘无故地做两次不是一个好主意。

于 2013-02-10T14:24:13.940 回答