一个小问题引发的血案——python 两个数值互换原理

首先来看两个函数:

def rankList1(li=['w','t'], li_triplets=['t','w','i','u','a','h','p','s']):
    if li_triplets.index(li[1]) < li_triplets.index(li[0]):
        print('exchange num')
        li_triplets[li_triplets.index(li[1])], li_triplets[li_triplets.index(li[0])] = li_triplets[li_triplets.index(li[0])], li_triplets[li_triplets.index(li[1])]
    return li_triplets
    
def rankList2(li=['w','t'], li_triplets=['t','w','i','u','a','h','p','s']):
    if li_triplets.index(li[1]) < li_triplets.index(li[0]):
        print('exchange num')
        li_triplets[li_triplets.index(li[0])], li_triplets[li_triplets.index(li[1])] = li_triplets[li_triplets.index(li[1])], li_triplets[li_triplets.index(li[0])]
    return li_triplets

这两个函数的目标都是将一个大列表中的两个元素按照小列表的顺序进行排序
运行一下看看:

>>> print(rankList1())
exchange num
['t', 'w', 'i', 'u', 'a', 'h', 'p', 's']
>>> print(rankList2())
exchange num
['w', 't', 'i', 'u', 'a', 'h', 'p', 's']

这很奇怪,理论上我们都应该得到 rankList2 的结果,然而 rankList1 并没有改变列表的顺序,函数中唯一有区别的就是这一句:

li_triplets[li_triplets.index(li[1])], li_triplets[li_triplets.index(li[0])] = li_triplets[li_triplets.index(li[0])], li_triplets[li_triplets.index(li[1])]

这有点类似于a,b = b,a,但又有所区别


分析一下a,b = b,a的执行顺序

Python 的变量并不直接存储值,而只是引用一个内存地址,交换变量时,只是交换了引用的地址。

先看下面这段程序:

import dis

def func(a, b):
    a,b = b,a
    print(a, b)
    
a = 10
b = 20
func(a, b)
dis.dis(func)
    一般来说一个Python语句会对应若干字节码指令,Python的字节码是一种类似汇编指令的中间语言,但是一个字节码指令并不是对应一个机器指 令(二进制指令),而是对应一段C代码,而不同的指令的性能不同,所以不能单独通过指令数量来判断代码的性能,而是要通过查看调用比较频繁的指令的代码来 确认一段程序的性能。
    一个Python的程序会有若干代码块组成,例如一个Python文件会是一个代码块,一个类,一个函数都是一个代码块,一个代码块会对应一个运行的上下文环境以及一系列的字节码指令。
  • dis 的作用
      dis 模块主要是用来分析字节码的一个内置模块,经常会用到的方法是 dis.dis([bytesource]),参数为一个代码块,可以得到这个代码块对应的字节码指令序列。
  • 代码输出结果
    一个小问题引发的血案——python 两个数值互换原理

其中只看前面为 4 的结果就行了(在我的编译器里,交换的那一行代码在第 4 行)

可以看出主要是 ROT_TWO 指令的功劳:
查阅 python 文档可以知道有 ROT_TWO (源码 1398 行),ROT_THREE(源码 1406 行), ROT_FOUR 这样的指令,可以直接交换两个变量、三个变量、四个变量的值

因此a,b = b,a首先执行a = b,即将 a 引用的地址的指向改为 b 所引用的地址,然后执行b = a,将 b 引用的地址的指向改为刚才 a 所引用的地址


弄清楚这点以后,我们来分析一下这一句

li_triplets[li_triplets.index(li[1])], li_triplets[li_triplets.index(li[0])] = li_triplets[li_triplets.index(li[0])], li_triplets[li_triplets.index(li[1])]
  1. 执行li_triplets[li_triplets.index(li[1])] = li_triplets[li_triplets.index(li[0])],即li_triplets[0] = li_triplets[1],此时大列表的第一个元素临时变成了 w。
  2. 执行li_triplets[li_triplets.index(li[0])] = li_triplets[li_triplets.index(li[1])],但此时 li[0] 已经变成了 w,因此执行 li_triplets.index(li[0]) 得到的结果是 0,即li_triplets[0] = li_triplets[0],而右侧的 li_triplets[0] 应该还是 t,因此大列表的第一个元素又从 w 变成了 t。

而 rankList2 正好因为索引位置的关系,得到了正确的结果,但写法实际上还是错的。

正确的写法是事先确定好需要交换的变量:

def rankList(li=['w','t'], li_triplets=['t','w','i','u','a','h','p','s']):
    a,b = li_triplets.index(li[1]),li_triplets.index(li[0])
    if a < b:
        print('exchange num')
        li_triplets[a], li_triplets[b] = li_triplets[b], li_triplets[a]
    return li_triplets