美文网首页
一个关于 += 的谜题

一个关于 += 的谜题

作者: yongxinz | 来源:发表于2022-02-28 12:25 被阅读0次

    原文链接: 一个关于 += 的谜题

    今天在看书过程中发现了一个问题,还挺有意思的,分享给大家。

    下面两个 Python 表达式会产生什么结果?

    t = (1, 2, [3, 4])
    t[2] += [5, 6]
    

    给四个备选答案:

    1. t 变成 (1, 2, [3, 4, 5, 6])
    2. 因为 tuple 不支持对它的元素赋值,所以会抛出 TypeError 异常。
    3. 以上两个都不是。
    4. 以上两个都是对的。

    当时看到这个问题,第一反应就是选 2。因为 tuple 是不可变对象,不支持对它的元素赋值,会报错。

    但事实上,这道题的正解是 4。

    在终端里验证一下:

    Python 3.8.2 (default, Oct  2 2020, 10:45:42)
    [Clang 12.0.0 (clang-1200.0.32.27)] on darwin
    Type "help", "copyright", "credits" or "license" for more information.
    >>>
    >>> t = (1, 2, [3, 4])
    >>> t[2] += [5, 6]
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: 'tuple' object does not support item assignment
    

    结果是没问题的,t 被修改了,但是也报错了。

    还可以在 Python Tutor 上分析一下:

    网站地址: https://pythontutor.com/

    这个网站可以可视化分析 Python 的运行过程和原理。

    执行第一个表达式:

    image

    执行第二个表达式:

    image

    为什么会这样呢?可以从两个方面来解释:

    一、对象类型

    Python 中的对象可以分成两类,可变对象和不可变对象,比如一些内置类型:

    1. 可变对象:list,set,dict。
    2. 不可变对象:int,float,bool,string,tuple。

    举一个例子:

    可变对象:

    >>> a = [1, 2, 3]
    >>> id(a)
    2139167246856
    >>> b = a
    >>> id(b)
    2139167246856
    >>> a[1] = 4
    >>> a
    [1, 4, 3]
    >>> b
    [1, 4, 3]
    >>> id(a)
    2139167246856
    >>> id(b)
    2139167246856
    

    可以看到,改变 a 的同时 b 也跟着变,因为他们始终指向同一个地址。

    不可变对象:

    >>> a = (1, 2, 3)
    >>> id(a)
    2139167074776
    >>> b = a
    >>> a = (4, 5, 6)
    >>> a
    (4, 5, 6)
    >>> b
    (1, 2, 3)
    >>> id(a)
    2139167075928
    >>> id(b)
    2139167074776
    

    可以看到,a 的值改变后,它的地址也发生了变化,而 b 还是原来的地址,并且原地址中的内容也没有发生变化。

    二、字节码

    首先解释一下字节码是什么?

    Python 执行程序时会把源码文件编译成字节码文件,存放在 __pycahe 目录内,文件用 .pyc 结尾。之后如果不再修改源码文件,运行时则直接使用 .pyc 文件编译成机器码,这样不但运行速度快,而且支持多个操作系统。

    字节码,其实就是一种中间代码。

    下面用 dis 模块来看一下表达式 s[a] += b 的执行过程:

    >>> import dis
    >>> dis.dis('s[a] += b')
      1           0 LOAD_NAME                0 (s)
                  2 LOAD_NAME                1 (a)
                  4 DUP_TOP_TWO
                  6 BINARY_SUBSCR
                  8 LOAD_NAME                2 (b)
                 10 INPLACE_ADD
                 12 ROT_THREE
                 14 STORE_SUBSCR
                 16 LOAD_CONST               0 (None)
                 18 RETURN_VALUE
    >>>
    

    通过分析字节码,可以看到其中的关键三步:

    1. 4 DUP_TOP_TWO:将 s[a] 存入 TOS(Top Of Stack)。
    2. 10 INPLACE_ADD:执行 TOS += b,带入到文章开头的表达式,就相当于向 t[2] 中添加元素,因为 t[2] 是 list,可变对象,所以这一操作没有问题。
    3. 14 STORE_SUBSCR:将结果保存回 s[a] = TOS,这相当于将结果重新赋值回 t,由于 t 是 tuple,不可变对象,所以报错。

    虽然这个问题在平时开发中可能并不常见,但通过分析还是有不少知识点可以深挖的。

    简单总结以下三点:

    1. 不要把可变对象放在元组里面。
    2. 增量赋值不是一个原子操作。我们刚才也看到了,它虽然抛出了异常,但还是完成了操作。
    3. 查看 Python 的字节码并不难,而且它对我们了解代码背后的运行机制很有帮助。

    以上就是本文的全部内容,如果觉得还不错的话,欢迎点赞转发,多谢


    推荐阅读:

    • 计算机经典书籍(含下载方式)
    • 技术博客 硬核后端开发技术干货,内容包括 Python、Django、Docker、Go、Redis、ElasticSearch、Kafka、Linux 等。
    • Go 程序员 Go 学习路线图,包括基础专栏,进阶专栏,源码阅读,实战开发,面试刷题,必读书单等一系列资源。
    • 面试题汇总 包括 Python、Go、Redis、MySQL、Kafka、数据结构、算法、编程、网络等各种常考题。

    相关文章

      网友评论

          本文标题:一个关于 += 的谜题

          本文链接:https://www.haomeiwen.com/subject/unrlrrtx.html