从字节码的角度看 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
posted @ 2024-03-28 18:25  Aloe_n  阅读(7)  评论(0编辑  收藏  举报