不奇妙的python "+=" 操作

缘起今年参加北京PyCon之后,给公司python爱好者的一次分享。分享邹义鹏的《Python 隐藏的玄机》的时候,大家对“元组中的列表”这一页的“+=”操作产生疑问。
在此输入图片描述

其实,这个在北京分会中也是大家争论的重点。我当时认为,这个操作修改了tuple的元素。但标号22这一行又明确地说明了,操作是成功的。这到底是怎么回事呢?

第一个从脑子里冒出来的想法是,这个操作的字节码是什么?于是我将PDF上的代码写成了一个函数:


def func():
    a = ([], [])
    a[0].append(1)
    a[0].extend([2])
    a[0] += [3]

import dis
dis.dis(func)

编译出来的字节码是这样的:

<!-- lang: shell -->
2           0 BUILD_LIST               0          
            3 BUILD_LIST               0
            6 BUILD_TUPLE              2
            9 STORE_FAST               0 (a)

3          12 LOAD_FAST                0 (a)
           15 LOAD_CONST               1 (0)
           18 BINARY_SUBSCR       
           19 LOAD_ATTR                0 (append)
           22 LOAD_CONST               2 (1)
           25 CALL_FUNCTION            1
           28 POP_TOP             

4          29 LOAD_FAST                0 (a)
           32 LOAD_CONST               1 (0)
           35 BINARY_SUBSCR       
           36 LOAD_ATTR                1 (extend)
           39 LOAD_CONST               3 (2)
           42 BUILD_LIST               1
           45 CALL_FUNCTION            1
           48 POP_TOP
5          49 LOAD_FAST                0 (a)  
           52 LOAD_CONST               1 (0)
           55 DUP_TOPX                 2
           58 BINARY_SUBSCR       
           59 LOAD_CONST               4 (3)
           62 BUILD_LIST                
           65 INPLACE_ADD         
           66 ROT_THREE           
           67 STORE_SUBSCR        
           68 LOAD_CONST               0 (None)
           71 RETURN_VALUE

其中,第一列是python代码行号,第二列是字节码的起始位置,圆括号是操作的值。
我们看到,一行python代码,编译成字节码后有数条指令。“+=“操作有9条指令(最后两条为函数的返回值)。我们就详细解释一下这9条指令。在解释的过程中,大家不妨画一下栈的状态,这样更容易看出问题所在。

  1. LOAD_FAST 0 (a)
    这条指令把局部变量a的值放入栈。可以顺带说一下载入全局变量的指令:LOAD_GLOBAL
  2. LOAD_CONST 1 (0)
    这条指令从保存常量的地方取出,放入栈中。
  3. DUP_TOPX 2
    这条指令复制出栈中最顶部的两个对象,也就是a0,然后放入栈中。
  4. BINARY_SUBSCR
    实现TOS = TOS1[TOS].的操作,TOS代表栈顶元素,TOS1代表栈顶下一个元素,以此类推。按现在的数据,就是取出a0,执行a[0],把结果放回栈中。现在栈的情况是 a, 0, a[0]。右边是栈顶。
  5. LOAD_CONST 4 (3)
    载入常量3,放入栈顶。现在栈的情况是 a, 0, a[0], 3
  6. BUILD_LIST
    取出栈顶元素,组成list对象,即[3]。 现在栈的情况是 a, 0, a[0], [3]
  7. INPLACE_ADD
    这条指令实现"TOS = TOS1 + TOS”,即a[0] + [3],结果就是a[0]变成了[1,2,3]。这是符合两个list执行+=操作的。现在栈的情况是 a, 0, a[0]
  8. ROT_THREE
    ROT_THREE是一条交换指令。类似的还有两条:ROT_TWOROT_FOURROT_THREE将栈顶对象往后移动两个位置。即a[0], a, 0
  9. STORE_SUBSCR
    前面实现了+=中的+的操作。这条指令则实现赋值。执行TOS1[TOS] = TOS2。即a[0] = a[0]。这条python语句看似没有问题,实则问题大大滴!对于可变对象,没问题。对于不可变对象,像tuple,string,肯定是错的。因为这些对象根本不支持改变它的元素。如果a是list,a[0] = a[0] 没有问题;如果a是tuple,就是不能执行的。

所以,问题的本质没我们想像得那么复杂。就像python解释器给我们的提示,不能改变tuple对象的元素。换句话就是,不能改变不可变对象的元素。

你可能感兴趣的:(python,异常,Tuple,陷阱,+=)