从字节码的角度看 python 变量交换
从字节码的角度看 python 变量交换
背景
从一道算法题开始:
反转链表
class ListNode:
def __init__(self, v) -> None:
self.val = v
self.next = None
def add_next(self, v):
new_node = ListNode(v)
self.next = new_node
return new_node
def print(self):
cur = self
while cur:
print(cur.val,end='->')
cur = cur.next
print()
def reverse(head):
pre = None
cur = head
while cur:
cur.next, cur, pre = pre, cur.next, cur
# 用下面一行代替后,会报错.
# cur, cur.next, pre = cur.next, pre, cur
return pre
head = ListNode(1)
head.add_next(2).add_next(3).add_next(4)
reverse(head).print()
这样写是没问题的, 正常输出:
4->3->2->1->
但是如果按照注释用下面一行代替上面, 程序程序就崩了.
AttributeError: 'NoneType' object has no attribute 'next'
分析
想知道发生了什么, 需要看字节码是如何执行的.
import dis
dis.dis(reverse)
2 0 LOAD_CONST 0 (None)
2 STORE_FAST 1 (pre)
# Pushes a reference to the local co_varnames[var_num] onto the stack
3 4 LOAD_FAST 0 (head)
# Stores TOS into the local co_varnames[var_num].
# 从这里可以看出 cur变量对应 co_varnames[2]
6 STORE_FAST 2 (cur)
4 8 SETUP_LOOP 28 (to 38)
>> 10 LOAD_FAST 2 (cur)
12 POP_JUMP_IF_FALSE 36
# 将等号右边的三个值依次加载到栈上
5 14 LOAD_FAST 1 (pre)
16 LOAD_FAST 2 (cur)
18 LOAD_ATTR 0 (next)
20 LOAD_FAST 2 (cur)
# 调整栈顶三个值的位置
# Lifts second and third stack item one position up, moves top down to position three.
22 ROT_THREE
# Swaps the two top-most stack items.
24 ROT_TWO
# 重新赋值. 可以推测出, 如果交换01,02两个步骤,将会导致非预期行为, 因为cur已经改变了.
# 01 给 co_varnames[2],也就是cur变量的next属性赋值
26 LOAD_FAST 2 (cur)
28 STORE_ATTR 0 (next)
# 02 给 co_varnames[2],也就是cur变量赋值
30 STORE_FAST 2 (cur)
# 02 给 co_varnames[1],也就是pre变量赋值
32 STORE_FAST 1 (pre)
34 JUMP_ABSOLUTE 10
>> 36 POP_BLOCK
7 >> 38 LOAD_FAST 1 (pre)
40 RETURN_VALUE
如何避免非预期情况发生
从字节码和注释可以看到, 发生非预期行为是因为等号左边的值是依次赋值的, 所以前面的变量改变后后面的变量就再使用它就会出现问题. 解决方案就是调整变量顺序.
例如:
# ok
cur.next, cur= pre, cur.next
# not ok
cur, cur.next= cur.next, pre
最后再来个例子, 附上两两交换链表节点的代码:
def swapPairs(head: ListNode) -> ListNode:
sentry = ListNode(0)
sentry.next = head
cur = sentry
while cur and cur.next and cur.next.next:
cur.next.next.next, cur.next.next, cur.next, cur = \
cur.next, cur.next.next.next, cur.next.next, cur.next
return sentry.next