python unpacking_Python在unpacking上的一个小陷阱

在Python中,交换两个变量的值很方便:

a, b = b, a

同样的,对于列表也简单直接:

a[i], a[j] = a[j], a[i]

至此都很trivial。但是请看下面这个交换:

j = 0

m = [1, 3, 5]

j, m[j] = m[j], 99

结果违背了(我的)直觉:

print(j) # 1

print(m) # [1, 99, 5]

并非如我预期的m[0]被写为99,而是m[1]被写了。如果我们交换该语句两边对应的顺序:

m[j], j = 99, m[j]

print(j) # 1

print(m) # [99, 3, 5] # different result

个人认为这是个略坑爹的结果:unpacking左右边子表达式的顺序是影响结果的,尤其是子表达式间有依赖的时候(比如这里m[j]是依赖j的)。究其原因,我们可以编译字节码看看:

>>> import dis

>>> dis.dis("j, m[j] = m[j], 99")

1 0 LOAD_NAME 0 (m)

3 LOAD_NAME 1 (j)

6 BINARY_SUBSCR

7 LOAD_CONST 0 (99)

10 ROT_TWO

11 STORE_NAME 1 (j)

14 LOAD_NAME 0 (m)

17 LOAD_NAME 1 (j)

20 STORE_SUBSCR

21 LOAD_CONST 1 (None)

24 RETURN_VALUE

第11位置的STORE_NAME和第17位置的LOAD_NAME指令表明,在执行unpacking绑定时,j的值是实时存取的,所以m[j]载入的是刚被改写的新的j值,而且子表达式的绑定顺序是从左至右。

相比之下,等号右侧的整个结构值则不是实时的,在执行unpacking之前已入栈预存起来。

(注:第10位置的ROT_TWO用于颠倒等号右侧已入栈元素的顺序,从第11位置开始,每次STORE会弹出一个栈顶元素。这样就实现了等号左右侧同时对应的从左至右的绑定顺序。若右侧元素超出三个,则ROT_TWO会被更通用的UNPACK_SEQUENCE替换掉。)

虽然本文这种情形不太常见,但是在实现一些算法题细节的时候,偷懒可能会被坑。所以童鞋们以后在遇到

i, a[i] = 1, 2

# not equivalent to

a[i], i = 2, 1

的情形时要稍加注意,以免掉坑难以查错。

==========================================

@王赟 Maigo 问是什么情况下遇到这个问题的,其实是来源于一道代码题:给定两个长度为

的数组

是由一组巨大元素构成的未排序数组,

是由

中元素的索引构成的数组,且

中元素顺序是按索引

元素排序的结果。

即:

现在欲对

进行inplace排序,求O(N)时间和O(1)空间的方法。

解法为:

def sort_with(a, m):

for p in range(len(a)):

j = p

while m[j] > p:

a[j], a[m[j]] = a[m[j]], a[j]

j, m[j] = m[j], -1 # WRONG

标记WRONG的这一行试图做这个:

实际效果却是:

从而暴露了本文所提这个问题。严格正确的写法应该拆开几个赋值语句:

def sort_with(a, m):

for p in range(len(a)):

j = p

while m[j] > p:

a[j], a[m[j]] = a[m[j]], a[j]

j_old = j

j = m[j_old]

m[j_old] = -1

还有一个“恰好正确”的写法,可以把三行并作一行(不建议这样写):

def sort_with(a, m):

for p in range(len(a)):

j = p

while m[j] > p:

a[j], a[m[j]] = a[m[j]], a[j]

m[j], j = -1, m[j] # coincidentally correct

这个特性也暴露了mutability和unpacking蕴含语义不太明确的问题,故以此文为记。

====================================

关于unpacking,这里再给一个略tricky的例子:

a = 1

b = 5

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

print(a)

print(b)

问输出结果是多少?

事实上,Python将右侧先整体求值并缓存结果,然后遍历左侧进行绑定。即第三行代码相当于:

a = 9

b = 1 # (a == 1) value before evaluating RHS

a = 5 # (b == 5) value before evaluating RHS

我们可以看dis的结果:

>>> dis.dis('a, (b, a) = 9, (a, b)')

1 0 LOAD_CONST 0 (9)

3 LOAD_NAME 0 (a)

6 LOAD_NAME 1 (b)

9 BUILD_TUPLE 2

12 ROT_TWO

13 STORE_NAME 0 (a)

16 UNPACK_SEQUENCE 2

19 STORE_NAME 1 (b)

22 STORE_NAME 0 (a)

25 LOAD_CONST 1 (None)

28 RETURN_VALUE

前四个指令即构造等号右侧的结构,第六个指令开始遍历左侧进行绑定。这里第16位置的UNPACK_SEQUENCE指令右侧的2是指令参数,表示接下需要两次STORE(或递归的UNPACK_SEQUENCE)。本指令会把栈内的元素全部弹出到另一个临时栈,再从临时栈取。

比如这里右手边入栈的那个tuple是(a, b),入栈后是a的旧值在栈里,b的旧值在栈顶,通过UNPACK到临时栈反序后,接下来的STORE则先取出的是a的旧值,后b的旧值。

希望对感兴趣的童鞋有所帮助~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值