流畅的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]

*运算

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

  • 示例:

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]

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

*序列运算的陷阱

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

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

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

>>> 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]]  #有两个发生了改动,不符合预期

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

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

注:下面说明时,地址和引用没有区分开来,实际上二者在不同语言中有较大区别。但是地址和引用的作用有点类似,下面使用地址说明,更便于理解。实际实现过程,需要看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中每个列表元素都是互不相关的

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

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

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

+= 背后的特殊方法是 __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

① 刚开始时列表的 ID。

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

③ 元组最开始的 ID。

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

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

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

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

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

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

最后的结果是,解释器抛出异常,但是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 "<stdin>", line 1, in <module> 
TypeError: 'tuple' object does not support item assignment 
>>> t 
(1, 2, [30, 40, 50, 60])

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

这里分析一下表达式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

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

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

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

小结

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

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

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

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

参考资料

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我理解了你的问题。这是一个关于 C 语言实现算术表达式求值的问题,需要实现算符优先法对算术表达式求值的过程,并输出运算符栈、运算数栈、输入字符和主要操作的变化过程。下面是一个实现的提示: 1. 首先,需要定义一个运算符栈和一个运算数栈,可以使用数组来实现。运算符栈用于存储运算符,运算数栈用于存储运算数。 2. 读入表达式的字符序列时,需要进行运算符和运算数的识别处理,并且进行相应的运算。可以使用 while 循环来实现。 3. 在识别出运算数时,需要将其字符序列形式转换成整数形式。可以使用 atoi 函数来实现。 4. 在程序的适当位置,可以通过输出语句来输出运算符栈、运算数栈、输入字符和主要操作的内容,以便更好地理解算法的执行过程。 下面提供一个简单的 C 语言代码实现供参考: ```c #include <stdio.h> #include <stdlib.h> #define STACK_SIZE 100 typedef struct { int data[STACK_SIZE]; int top; } Stack; void initStack(Stack *s) { s->top = -1; } int isEmpty(Stack *s) { return s->top == -1; } int isFull(Stack *s) { return s->top == STACK_SIZE - 1; } void push(Stack *s, int x) { if (isFull(s)) { printf("Error: stack is full.\n"); exit(1); } s->data[++s->top] = x; } int pop(Stack *s) { if (isEmpty(s)) { printf("Error: stack is empty.\n"); exit(1); } return s->data[s->top--]; } int getTop(Stack *s) { if (isEmpty(s)) { printf("Error: stack is empty.\n"); exit(1); } return s->data[s->top]; } int getPriority(char op) { switch (op) { case '+': case '-': return 1; case '*': case '/': return 2; case '(': return 0; default: return -1; } } int main() { Stack opStack, numStack; char ch, lastOp; int num, a, b; initStack(&opStack); initStack(&numStack); printf("Please input the expression: "); while ((ch = getchar()) != '\n') { if (ch >= '0' && ch <= '9') { num = ch - '0'; while ((ch = getchar()) >= '0' && ch <= '9') { num = num * 10 + ch - '0'; } push(&numStack, num); printf("Push %d into numStack.\n", num); } if (ch == '+' || ch == '-' || ch == '*' || ch == '/') { while (!isEmpty(&opStack) && getPriority(ch) <= getPriority(getTop(&opStack))) { b = pop(&numStack); a = pop(&numStack); lastOp = pop(&opStack); switch (lastOp) { case '+': push(&numStack, a + b); printf("Calculate %d + %d = %d.\n", a, b, a + b); break; case '-': push(&numStack, a - b); printf("Calculate %d - %d = %d.\n", a, b, a - b); break; case '*': push(&numStack, a * b); printf("Calculate %d * %d = %d.\n", a, b, a * b); break; case '/': push(&numStack, a / b); printf("Calculate %d / %d = %d.\n", a, b, a / b); break; } } push(&opStack, ch); printf("Push %c into opStack.\n", ch); } if (ch == '(') { push(&opStack, ch); printf("Push %c into opStack.\n", ch); } if (ch == ')') { while ((lastOp = pop(&opStack)) != '(') { b = pop(&numStack); a = pop(&numStack); switch (lastOp) { case '+': push(&numStack, a + b); printf("Calculate %d + %d = %d.\n", a, b, a + b); break; case '-': push(&numStack, a - b); printf("Calculate %d - %d = %d.\n", a, b, a - b); break; case '*': push(&numStack, a * b); printf("Calculate %d * %d = %d.\n", a, b, a * b); break; case '/': push(&numStack, a / b); printf("Calculate %d / %d = %d.\n", a, b, a / b); break; } } printf("Pop ( from opStack.\n"); } } while (!isEmpty(&opStack)) { b = pop(&numStack); a = pop(&numStack); lastOp = pop(&opStack); switch (lastOp) { case '+': push(&numStack, a + b); printf("Calculate %d + %d = %d.\n", a, b, a + b); break; case '-': push(&numStack, a - b); printf("Calculate %d - %d = %d.\n", a, b, a - b); break; case '*': push(&numStack, a * b); printf("Calculate %d * %d = %d.\n", a, b, a * b); break; case '/': push(&numStack, a / b); printf("Calculate %d / %d = %d.\n", a, b, a / b); break; } } printf("The result is: %d\n", pop(&numStack)); return 0; } ``` 上述代码实现了算符优先法对算术表达式求值的过程,并输出了运算符栈、运算数栈、输入字符和主要操作的变化过程。在输入表达式后,会依次输出每一步的操作,以便更好地理解算法的执行过程。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值