• 流畅的Python读书笔记(四)序列:序列的运算及陷阱


    活动地址:CSDN21天学习挑战赛

    流畅的Python读书笔记(四)序列:序列的运算及陷阱

    本篇笔记记录了序列的+*+=*=运算的使用以及细节。着重介绍了关于+=的一个谜题:t=(1,2,[3, 4]); t[2] += [50, 60],这条python语句会抛出异常,但是能够成功执行。

    +*运算

    Python程序员默认序列是支持+*操作的。这两种运算都是非常简单的,所以不会过多介绍。

    首先明确一点,对于+*这类运算符,作用于序列时,都不会修改原序列,而是会新创建序列。即序列a,ba + ba * b,这两个表达式都不会修改a,b

    +运算

    • 用途:拼接序列。

    • 示例:

    a = [1, 2]
    b = [i for i in range(10)]
    c = a + b
    print(c)
    #结果为:
    #[1, 2, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    *运算

    • 用途:复制序列并将复制的元素拼接

    • 示例:

    a = [1, 2]
    a2 = a * 2
    print('a2=', a2)
    b = [3, 4]
    b3 = b * 3
    print('b3=', b3)
    #结果为:
    #a2= [1, 2, 1, 2]
    #b3= [3, 4, 3, 4, 3, 4]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这里提一下关于*运算作用于序列上时的陷阱。

    *序列运算的陷阱

    有时,想要复制序列并拼接,最简单的方法就是使用*

    >>> a = [1, 2]
    >>> a = a * 2
    >>> a
    [1, 2, 1, 2]
    >>> a[3] = 100  #修改一个值
    >>> a
    [1, 2, 1, 100]  #只有一个值被改动
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    以上代码简单易懂。但是如果我们操作的序列中的元素本身又是序列类型呢?

    >>> a = [[1, 2], [3, 4]]  #a是由两个列表构成的列表,简称为嵌套列表
    >>> a = a * 2  #复制并拼接
    >>> a
    [[1, 2], [3, 4], [1, 2], [3, 4]]
    >>> a[0][1] = 100  #修改一个值
    >>> a
    [[1, 100], [3, 4], [1, 100], [3, 4]]  #有两个发生了改动,不符合预期
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    上述代码不符合预期。本来只是想改动一个值,但是却改动了两个。

    根本原因是:列表是引用数据类型。

    注:下面说明时,地址和引用没有区分开来,实际上二者在不同语言中有较大区别。但是地址和引用的作用有点类似,下面使用地址说明,更便于理解。实际实现过程,需要看Python源码。在流畅的Python书中,关于本章的内容,还只有介绍如何使用,相关实现在书后面的章节再做介绍。

    同时,对于有过其他编程语言(比如C/C++)学习经历的读者,可以参看文末的两篇关于Python中引用的文章,能够加深理解。

    比如:a = [1, 2]这句Python语句中,变量a中存储的并不是1,2这两个值,而是存放[1,2]的引用或者说是指向存储[1,2]的内存空间的地址。

    所以,对于a = [[1, 2], [3, 4]],变量a指向的空间中实际存储的是两个地址,抽象一下,a = [地址值1,地址值2],那么在复制时,a = a * 2,Python会拷贝a指向的空间中的值,然后拼接到a的后面。所以,现在a 等于 [地址值1,地址值2, 地址值1, 地址值2]a[0],a[2]指向相同的空间,因此当修改a[0]指向的空间时,a[2]指向的空间也就被修改了。

    建立由列表构成的列表

    根据以上介绍,我们知道了,对于嵌套列表,使用*运算,会到达预料之外的效果。那么如何构建嵌套列表呢?或者说如何构建嵌套序列呢?

    书中推荐的方法是:使用列表推导式。

    >>> x = [[i] for i in range(10)]
    >>> x
    [[0], [1], [2], [3], [4], [5], [6], [7], [8], [9]]
    >>> for i in range(10):\
    ... x[i][0] += 1
    ...
    >>> x
    [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10]]
    # 每个元素都加了1,说明列表x中每个列表元素都是互不相关的
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    序列的增量赋值:增强运算符+=``*=

    增量赋值运算符 += 和 *= 的表现取决于它们的第一个操作对象。

    由于+=*=的运算过程类似,只介绍+=

    += 背后的特殊方法是 __iadd__(用于“就地加法”)。但是如果一个类没有实现这个方法的话,Python 会退一步调用 __add__

    也就是说,如果运算对象实现了__iadd__,那么就调用该方法,这样该运算就转换成了对象的方法调用,对象会就地修改。如果运算对象没有实现__iadd__的话,a += b这个表达式的效果就变成了a = a + b,这时,就是调用__add__方法了,先计算a+b,然后将结果赋值给a。对于*=运算,其背后的特殊方法为__imul__,调用过程与+=基本一致,可以类推。

    下面给出书中的例子,加深印象:

    >>> l = [1, 2, 3]
    >>> id(l)
    4311953800>>> l *= 2
    >>> l
    [1, 2, 3, 1, 2, 3]
    >>> id(l)
    4311953800>>> t = (1, 2, 3)
    >>> id(t)
    4312681568>>> t *= 2
    >>> id(t)
    4301348296
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    ① 刚开始时列表的 ID。

    ② 运用增量乘法后,列表的 ID 没变,新元素追加到列表上。

    ③ 元组最开始的 ID。

    ④ 运用增量乘法后,新的元组被创建。

    对不可变序列进行重复拼接操作的话,效率会很低,因为每次都有一个新对象,而解释器需要把原来对象中的元素先复制到新的对象里,然后再追加新的元素。但是str 是一个例外,因为对字符串做 += 实在是太普遍了,所以 CPython 对它做了优化。为 str 初始化内存的时候,程序会为它留出额外的可扩展空间,因此进行增量操作的时候,并不会涉及复制原有字符串到新位置这类操作。

    不可变序列中含有可变序列——+=谜团

    先看看下面这段代码,预测其运行结果:

    >>> t = (1, 2, [30, 40])
    >>> t[2] += [50, 60]
    
    • 1
    • 2

    如果你有了自己的预测,可以输入以上代码进行验证。

    最后的结果是,解释器抛出异常,但是t被修改。即:

    t 变成 (1, 2, [30, 40, 50, 60]) tuple 不支持对它的元素赋值,所以会抛出TypeError异常。

    结果:

    >>> t = (1, 2, [30, 40]) 
    >>> t[2] += [50, 60] 
    Traceback (most recent call last): 
     File "", line 1, in <module> 
    TypeError: 'tuple' object does not support item assignment 
    >>> t 
    (1, 2, [30, 40, 50, 60])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    透过字节码分析代码运行逻辑

    这里分析一下表达式s[a] += b的执行过程,对上面那个例子会有更好的理解。

    这里笔者水平有限,暂时看不懂Python字节码。下面的例子源自书中。

    >>> 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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    ① 将 s[a] 的值存入 TOS(Top Of Stack,栈的顶端)。

    ② 计算 TOS += b。这一步能够完成,是因为 TOS 指向的是一个可变对象,也就是t[2]

    s[a] = TOS 赋值。这一步失败,是因为s是不可变的元组。

    小结

    1. 不要把可变对象放在元组里面。

    2. 使用*作用于序列时,需要确保原序列中的元素不是引用类型。

    3. 应该通过列表推导式来构建嵌套序列

    4. 在有需要时,可以通过dis库来查看代码的字节码

    参考资料

  • 相关阅读:
    HALCON:Programming With HALCON/.NET
    LeetCode-895. 最大频率栈以及HashMap的存值取值操作
    【面试必刷TOP101】链表相加 & 单链表的排序
    [附源码]java毕业设计网上学车预约系统
    「喜迎华诞」手把手教你用微信小程序给头像带上小旗帜
    Abp Vnext 动态(静态)API客户端源码解析
    DeFi 永不消亡?
    C++第三方库【httplib】断点续传
    黑客(网络安全)技术速成自学
    微信小程序| 做一款可以计算亲戚关系的计算器
  • 原文地址:https://blog.csdn.net/m0_52339560/article/details/126412920