Python中 a,b = b,a 的交换原理是什么?在线求助!

今天被问到
a,b = b,a 是如何实现的
轻蔑的告诉对方这是因为交换了内存地址啊
然而我自己多事,要给别人演示

a = 1
b = 2
id(a)

4304968096

id(b)

4304968064

a,b = b,a
id(a)

4304968064

id(b)

4304968096

目前为止没有任何问题
然后我又解释到:
因为实际上它是这样运行的啊

(a,b)=(b,a)
a

1

b

2

你看,这是生成了两个新的元组在参与运算

id((a,b)),id((b,a))

(4356155464, 4356155464)

诶?! 你等等

(a,b) is (b,a)

False

诶?! 诶?! 你再等等(莫非 tuple 太特殊了)

id([a,b]),id([b,a])

(4356157384, 4356157384)

我 c?! 不行!

id([a,b][0]),id([b,a][0])

(4304968096, 4304968064)

是不同的 id 啊,这个......
那个,你等等啊

各位 V 大! 在线求助啊!!!


Python中 a,b = b,a 的交换原理是什么?在线求助!

23 回复

事到如今,只能怀疑是 id()在作妖了

>>> a = 1
>>> b = 2
>>> id ((a,b))
4526388432
>>> id ((a,b))
4526388504
>>> id ((a,b))
4526388432
>>> id ((a,b))
4526388504

一起等回复


这是Python的并行赋值特性,底层通过创建临时元组实现交换。

# 实际执行过程相当于:
a = 1
b = 2
_temp = (b, a)  # 创建临时元组(2, 1)
a = _temp[0]    # a = 2
b = _temp[1]    # b = 1

关键点:

  1. 右侧表达式b, a先被求值为元组(b的当前值, a的当前值)
  2. 左侧变量按位置接收元组中的值
  3. 整个过程是原子的,不需要中间变量

这比传统三变量交换更简洁高效,因为Python解释器能直接优化这个操作。

一句话总结:利用元组打包和解包的原子操作实现变量交换。

数组不是一回事啊

每次()都创建一个新数组

右边的表达式先求值,再对等号求值;
a = 1, b = 2
b, a = a, b => b, a = 1, 2 => b = 1, a = 2
求值规则是这么个流程,内部的具体实现我就不清楚了。

Python dis 模块值得拥有

id((a,b)) 返回以后 (a,b) tuple 就因为引用计数为 0 被回收了

dis.dis(compile(“a = 0; b = 1; a,b = b,a”, “<string>”, “exec”))


0 LOAD_CONST 0 (0)
2 STORE_NAME 0 (a)
4 LOAD_CONST 1 (1)
6 STORE_NAME 1 (b)
8 LOAD_NAME 1 (b)
10 LOAD_NAME 0 (a)
12 ROT_TWO


官方文档的解释
ROT_TWO()
Swaps the two top-most stack items.

(a, b)每次都会生成新的 tuple 对象,ID 都是不一样的。id((a, b))调用结束后,(a, b)引用计数为 0 被回收。至于你 1 楼中的代码出现 id 一样的情况,应该是 Python 内存管理使用了回收的内存

下面是我在 IPython 中测试的结果:

In [1]: a = 1

In [2]: b = 2

In [3]: id((a, b))
Out[3]: 4512224320

In [4]: id((a, b))
Out[4]: 4510990992

In [5]: id((a, b))
Out[5]: 4510066880

In [6]: id((a, b))
Out[6]: 4510871064

In [10]: t1 = (a, b)

In [11]: t2 = (a, b)

In [12]: t3 = (a, b)

In [13]: t4 = (a, b)

In [14]: id(t1)
Out[14]: 4512123720

In [15]: id(t2)
Out[15]: 4511225328

In [16]: id(t3)
Out[16]: 4510011976

In [17]: id(t4)
Out[17]: 4510782816

你要演示的话应该到 cpython 的源码里打 log,而不是用 id

的答案正解,多谢,学习了。

要计算一个变量的 id 的时候一定要确保这个变量不是被计算出来的。
简单来说就是这个变量一定是有人引用的。只有这样才可以算出来真正的 id。
c = (a,b)
d = (b,a)
这里 id© 就 不等于 id(d) 了。
会出现 id((a,b)) 等于 id((b,a)) 是因为引用计数为 0+内存被回收+缓存池 导致的

我后来也这样试了,确实这样就会 id 不一样
哦~

哦~ 还有这种东西?~


前面被回收,后面又再次引用,所以导致出现了同样的 ID? 看后面 6、7、8、9 楼的回复好像是这样的, 万分感谢!!! 收好铜币哦


大致明白了,我要去找这个库的文档了,但这个大写字母加下划线 →v→ ~ 万分感谢!!! 收好铜币哦


嗯, 把你的解释和 12 楼的结合起来就很清晰了, 还是内存机制导致的不确定, 万分感谢!!! 收好铜币哦

突然想到了 zen of python ,不要想一行代码解决问题 →_→, 万分感谢!!! 收好铜币哦

发现了一个有趣的现象,两个变量交换和四个变量交换使用的不是同一种方法。
# 两个变量的交换
>>> dis.dis(“a=100;b=1000;a,b=b,a”)
1 0 LOAD_CONST 0 (100)
3 STORE_NAME 0 (a)
6 LOAD_CONST 1 (1000)
9 STORE_NAME 1 (b)
12 LOAD_NAME 1 (b)
15 LOAD_NAME 0 (a)
18 ROT_TWO
19 STORE_NAME 0 (a)
22 STORE_NAME 1 (b)
25 LOAD_CONST 2 (None)
28 RETURN_VALUE

# 四个变量的交换
>>> dis.dis(“a=100;b=1000;c=10000;d=10000;a,c,d,b=b,a,c,d”)
1 0 LOAD_CONST 0 (100)
3 STORE_NAME 0 (a)
6 LOAD_CONST 1 (1000)
9 STORE_NAME 1 (b)
12 LOAD_CONST 2 (10000)
15 STORE_NAME 2 ©
18 LOAD_CONST 2 (10000)
21 STORE_NAME 3 (d)
24 LOAD_NAME 1 (b)
27 LOAD_NAME 0 (a)
30 LOAD_NAME 2 ©
33 LOAD_NAME 3 (d)
36 BUILD_TUPLE 4
39 UNPACK_SEQUENCE 4
42 STORE_NAME 0 (a)
45 STORE_NAME 2 ©
48 STORE_NAME 3 (d)
51 STORE_NAME 1 (b)
54 LOAD_CONST 3 (None)
57 RETURN_VALUE

###
两个变量交换的时候,python 没有构建 tuple,但是四个变量交换的时候,python 构建了 tuple。

噗,这是把自己给坑了呀。

#15 那么一个很自然的问题是,三个的情况呢?
#9 IPython 里很多行为不一样的,因为中间多了一层。

我这里的结果很有意思,有几个 tuple 间隔地被重复使用:

>>> a = 1
>>> b = 2
>>> id((a, b))
140111655945608
>>> id((a, b))
140111675289544
>>> id((a, b))
140111655945608
>>> id((a, b))
140111675289544
>>> id((a, b)), id((b, a))
(140111675289544, 140111675289544)
>>> id((a, b))
140111655841608
>>> id((a, b)), id((b, a))
(140111655992648, 140111655992648)
>>> id((a, b))
140111655945608
>>> id((a, b)), id((b, a))
(140111675289544, 140111675289544)
>>> id((a, b)), id((b, a))
(140111655841608, 140111655841608)
>>> id((a, b)), id((b, a))
(140111655945608, 140111655945608)
>>> id((a, b)), id((b, a))
(140111675289544, 140111675289544)

Python 3.6.6

#16 我刚在 Python ( 2.7.10 )的解释器测试,得到的结果和你的类似,tuple 的内存被交替使用。

>>> a = 1
>>> b = 2
>>> id((a, b))
4469425216
>>> id((a, b))
4469425504
>>> id((a, b))
4469425216
>>> id((a, b))
4469425504
>>> id((a, b))
4469425216
>>> id((a, b)), id((b, a))
(4469425504, 4469425504)
>>> id((a, b))
4469425432
>>> id((a, b)), id((b, a))
(4469425432, 4469425432)


然后我进一步测试:

>>> a = 1
>>> b = 2
>>> c = 3
>>> id((a, b, c))
4403338528
>>> id((a, b, c))
4403338528
>>> id((a, b, c))
4403338528
>>> x = (a, b, c)
>>> id(x)
4403338528
>>> id(x)
4403338528
>>> del x
>>> id((a, b, c))
4403338528

我觉得这是 Python 内存管理的优化。

#17 当然是优化啊。Python 从来没有说 id 不会被复用嘛。

三个的时候,倒没有创建元组
这个优化也是动态语言的一种个特点吧


三个变量的交换
import dis
dis.dis(“a=1;b=2;c=3;a,c,b=c,b,a”)

1 0 LOAD_CONST 0 (1)
3 STORE_NAME 0 (a)
6 LOAD_CONST 1 (2)
9 STORE_NAME 1 (b)
12 LOAD_CONST 2 (3)
15 STORE_NAME 2 ©
18 LOAD_NAME 2 ©
21 LOAD_NAME 1 (b)
24 LOAD_NAME 0 (a)
27 ROT_THREE
28 ROT_TWO
29 STORE_NAME 0 (a)
32 STORE_NAME 2 ©
35 STORE_NAME 1 (b)
38 LOAD_CONST 3 (None)
41 RETURN_VALUE
#三个变量的交换使用 ROT_THREE。

但是发现
无论是
dis.dis(“a=1;b=2;(a,b)=(b,a)”)
还是
dis.dis(“a=1;b=2;a,b=b,a”)
字节码命令都是
1 0 LOAD_CONST 0 (1)
3 STORE_NAME 0 (a)
6 LOAD_CONST 1 (2)
9 STORE_NAME 1 (b)
12 LOAD_NAME 1 (b)
15 LOAD_NAME 0 (a)
18 ROT_TWO
19 STORE_NAME 0 (a)
22 STORE_NAME 1 (b)
25 LOAD_CONST 2 (None)
28 RETURN_VALUE

也就是说无论几个元素交换,都是构建 tuple 来实现的?

#21 并没有构建 tuple 啊。

回到顶部