1. 先从一份摸底测验说起
带过几批新人之后,我发现一个规律:很多人Python写了几个月,列表推导、装饰器、面向对象都聊得头头是道,但一碰到操作符相关的问题就露馅。
不是不会用+ - * /,而是用得不透。比如-7 // 2到底等于多少,2 ** 3 ** 2到底是先算哪个,[] == []和[] is []有什么区别,True or 1/0会不会崩——这些问题看起来基础,但真能答对的人不多。
所以后来我给团队做入职摸底,第一关就是操作符测验。这份测验一共10道题,覆盖算术操作符、比较操作符、逻辑操作符、位操作符、赋值操作符、成员操作符和身份操作符。题目不难,但每道都在考“你到底是背了语法,还是真的理解Python的运行机制”。
这篇文章把我用的这套题目、参考答案和逐题拆解都整理出来。想自测基础的人可以跟着做一遍,带新人的可以直接拿去用。建议你先别急着往下翻,把题做完再对答案,不然错过很多有意思的细节。
1.1 测验题目
# 题目1:整除与取模 print(7 // 2) print(-7 // 2) print(7 % 2) print(-7 % 2) # 题目2:幂运算 print(2 ** 3 ** 2) # 题目3:链式比较 if 1 < 2 == 2: print("True") else: print("False") # 题目4:逻辑短路 print(True or 1 / 0) print(False and 1 / 0) # 题目5:位运算 print(3 & 5) print(3 | 5) print(3 ^ 5) print(4 << 1) print(4 >> 1) # 题目6:赋值与引用 a = [1, 2, 3] b = a c = a[:] a.append(4) print(b) print(c) print(a is b) print(a is c) # 题目7:比较与身份 print([] == []) print([] is []) # 题目8:复合赋值 x = 5 y = 5 x += 3 print(x) print(y) # 题目9:成员运算符 print("a" in "abc") print(2 not in [1, 3]) # 题目10:逻辑运算符的返回值 print(0 or 42) print(1 and 99)1.2 快速参考答案
| 题号 | 输出 |
|---|---|
| 1 | 3, -4, 1, 1 |
| 2 | 512 |
| 3 | True |
| 4 | True, False |
| 5 | 1, 7, 6, 8, 2 |
| 6 | [1, 2, 3, 4], [1, 2, 3], True, False |
| 7 | True, False |
| 8 | 8, 5 |
| 9 | True, True |
| 10 | 42, 99 |
如果你全部答对了,那操作符的基础已经很扎实,可以直接跳到文中“易错点与踩坑记录”部分,看看那些真正容易写错的地方。如果错了几道,那接下来的逐题拆解值得仔细看一遍。
2. 逐题拆解与背后原理
这一节是全文的核心。我会把每道题涉及的Python机制和设计原因都讲清楚,不发散到用不到的东西上。
2.1 算术操作符:整除和取模的“向下取整”逻辑
第一题看似简单,7 // 2等于3,这没有争议。真正坑人的是负数版本的整除和取模。
-7 // 2在Python里等于-4,而不是很多人口算的 -3。原因在于Python的//是“向下取整”(floor division),也就是结果永远小于或等于真实的商。7除以2的真实商是3.5,向下取整是3;-7除以2的真实商是-3.5,向下取整是-4。向零取整才是-3,但Python不是这么做的。
取模运算%也遵循同样的方向。-7 % 2等于1,而不是很多人以为的 -1。这里有个硬核公式:a % b的结果满足a = (a // b) * b + r,其中r就是余数。把a = -7、b = 2、a // b = -4代入,得到-7 = (-4) * 2 + r,所以r = 1。
这个设计的实际意义在哪儿?最典型的场景是循环索引。假设你有一个长度为7的列表,想从索引 -1 往前循环取元素,用index % 7做取模运算,结果永远是0到6之间的非负数,不需要额外处理负数情况。很多算法题里用取模处理循环队列或哈希表冲突,如果碰到负数,就会感受到Python这个设计有多好用。
浮点数也有同样的问题:7.0 // 2.0得到3.0,-7.0 // 2.0得到-4.0。所以别以为只有整数才有这个坑。
2.2 幂运算:右结合才是关键
2 ** 3 ** 2的结果是512,不是64。
原因很简单:幂运算符**是右结合的,意思是2 ** 3 ** 2等价于2 ** (3 ** 2),先算3 ** 2 = 9,再算2 ** 9 = 512。而大多数算术运算符(+、-、*、/)都是左结合的,你习惯了从左往右算,一碰到**就容易掉坑里。
还有一个隐藏更深的坑:-2 ** 2的结果是-4,不是4。因为**的优先级高于一元负号-,所以它等价于-(2 ** 2)。如果你想表达(-2) ** 2,必须手动加括号。
这两条规则放在一起,实际操作中最稳妥的做法就是:幂运算一律加括号。不管是自己写还是给别人看,都把结合顺序写清楚。我在项目代码评审时见过-x ** 0.5这种表达式,写的人自以为是在求“负数的平方根”,实际上Python算的是“对x开方再取负”,结果完全不一样。
2.3 比较操作符:链式比较的隐藏逻辑
1 < 2 == 2的结果是True,这在Python中是合法的。
很多从C或Java转过来的人第一次看到这种写法会愣住,因为大多数语言的==和<优先级不同,写在一起会编译报错。但Python把比较做成了“链式”,1 < 2 == 2等价于(1 < 2) and (2 == 2),而且2只求值一次。
这就是Python比较操作符的隐藏逻辑:所有比较操作符(<、<=、>、>=、==、!=、is、in)都可以连成链。这个语法糖在实际编码中非常有用,比如检查一个数是否在区间内:
if 10 <= score <= 100: print("成绩有效")不用写成if score >= 10 and score <= 100,可读性高很多。
不过链式比较有个容易踩的坑:如果中间某个比较返回的不是布尔值,而是数组或自定义对象,就会出问题。比如写0 < np.array([1, 2, 3]) < 5会报错,因为NumPy数组的<比较结果也是数组,不能直接作为布尔判断使用。这个我放在后面“常见问题”里细说。
2.4 逻辑操作符:短路求值与返回值
第四题True or 1 / 0和False and 1 / 0都不会报错,分别输出True和False。
这就是逻辑操作符的短路求值(short-circuit evaluation):or只要左边为真,右边根本不会执行;and只要左边为假,右边也不会执行。所以1 / 0虽然是个会抛异常表达式,但只要不执行,程序就不会崩。
这个特性在真实代码里非常重要。最常见的是判空再访问属性:
if user and user.get("name"): print(user["name"])如果user是None,左边直接短路,右边不会执行,自然也不会报'NoneType' object is not subscriptable的错。
另一个容易被忽视的点是:Python的and和or返回的并不一定是布尔值,而是参与运算的那个对象本身。0 or 42返回42,1 and 99返回99。这个特性能写出非常简洁的代码:
name = user_input or "默认值"但也是一把双刃剑。很多人不知道or返回原值,在条件判断上就出过幺蛾子,比如if 0 or [] or ""这三个都为假,判断没问题。但若写成if value or 0,当value为0时,整个表达式结果是0,还是假,看起来逻辑没问题,可一旦你把返回值赋给一个变量并且期望是布尔值,就容易出bug。我的建议是:返回值特性可以用在赋值场景,但做条件判断时,最好还是老老实实转成布尔值。
2.5 位操作符:从二进制视角看数据
第五题考的是位运算。3 & 5等于1,3 | 5等于7,3 ^ 5等于6,4 << 1等于8,4 >> 1等于2。
人脑理解位运算,最直接的方式是把数字写成二进制。3 的二进制是011,5 的二进制是101。按位与&:每一位都做 AND,011 & 101 = 001,也就是1。按位或|:011 | 101 = 111,也就是7。按位异或^:相同为0、不同为1,011 ^ 101 = 110,也就是6。
左移<<相当于把这个数的二进制位整体往左移一位,低位补0,高位丢弃。在十进制层面看,就是乘2。右移同理,相当于整除2。4 << 1是8,4 >> 1是2。
实际项目中位运算最常用的场景有两个。第一个是权限系统:用一个整数的不同位表示不同权限,通过&判断用户是否有某项权限。第二个是状态标记:比如一个消息有多个属性(已读、置顶、删除),可以用一个字节表示,不需要存多个布尔字段。这在网络协议或存储优化里非常常见。
但这里必须提醒一句:普通业务代码里,能用可读性更好的写法就用更好的写法,不要为了炫技到处位运算。我之前见过有人用位运算实现字符串拼接,代码完全没法维护。
2.6 赋值与引用:浅拷贝和深拷贝的第一道坎
第六题是新手最容易错的。a = [1, 2, 3],之后b = a只是让b指向同一个列表对象,而c = a[:]则是创建了一个新的列表,把元素逐个复制过去。所以a.append(4)之后,b也跟着变成[1, 2, 3, 4],c还是[1, 2, 3]。
a is b是 True,a is c是 False。这题的考点是:=赋值不会复制对象,只是绑定名字;is比较的是内存地址。
这个知识点看似基础,但它直接关系到“变量赋值到底改的是哪个对象”这个核心问题。很多人用面向对象做项目时,不小心把同一个对象赋给多个变量,最后改了一个,所有地方都变了,找半天不知道原因。
更进一步的坑是切片拷贝只能做一层。如果列表里的元素本身是可变对象,比如a = [[1, 2], [3, 4]],那么c = a[:]对里层的[1, 2]还是同一个引用。修改c[0][0] = 99,a也会变。要真正复制嵌套结构,得用copy.deepcopy()。
这个“第一道坎”其实也是理解后续很多东西的基础:为什么函数传参有时候会改掉外部列表,为什么dict和list作为默认参数会有坑,都和这题有关。
2.7 身份操作符:== 和 is 绝不能混用
第七题:[] == []是True,[] is []是False。
==比较的是值,两个空列表内容一样,所以相等。is比较的是对象身份(内存地址),两个空列表是两个不同的对象,所以不相等。
一个常见的误区是:is用于判定“两者是不是同一个东西”,==用于判定“两者的值是否相等”。生活里类比一下:==是两本内容一样的书,is是同一本书的两个名字。
实际编码中最常见的问题是:判断变量是否为None,应该用if x is None,不要用if x == None。原因有二:一是None在Python中是一个单例对象,is比较效率高且语义准确;二是某些自定义类型重写了__eq__,== None的结果可能不可预期。同理,判断布尔值也不要写if x == True,直接if x就好。
这里还有个小知识点:Python的==对于字符串和整数有“驻留”优化机制。比如"hello" == "hello"是 True,但"hello" is "hello"在大多数情况下也是 True,因为解释器复用了同一个字符串对象。但这对长字符串或动态拼接出来的字符串不成立,所以永远不要依赖这种“碰巧”。哪怕在小整数范围1 is 1返回 True,也不代表1000 is 1000一定返回 True。一切以is比较对象身份为准。
2.8 复合赋值:不可变对象和可变对象的差异
第八题的x = 5,然后y = 5,执行x += 3之后,x变成8,y仍然是5。这看起来没啥好说的,但如果不小心把5换成列表,情况就完全不一样了。
a = [1, 2, 3] b = a a += [4] print(b) # [1, 2, 3, 4]同样是用+=,对不可变对象(整数、字符串、元组)来说,x += 3等价于x = x + 3,创建了新对象,原来的y不受影响。但对可变对象(列表、字典)来说,a += [4]等价于a.extend([4]),是原地修改,所有指向这个对象的变量都会同步变化。
这一点极其容易踩坑,特别是在函数参数和全局变量交互的时候。比如:
def add_item(item, container=[]): container.append(item) return container这种写法就是经典的“可变默认参数”坑。默认参数container=[]只在函数定义时创建一次,多次调用并不会生成新的空列表,而是不断往同一个列表里追加。这也是操作符与对象语义交叉的典型案例。
区分“原地修改”和“重新绑定”是Python进阶的重要节点。我的检查方法很简单:如果你不确定一个操作是原地还是新建,就在操作前后打印id(obj)看看内存地址有没有变。
2.9 成员操作符:in 的底层逻辑
第九题太简单,"a" in "abc"是 True,2 not in [1, 3]也是 True。
但in的底层逻辑值得多讲两句。对于列表,in本质是遍历然后挨个比较,时间复杂度是 O(n)。对于集合,in的底层是哈希查找,时间复杂度是 O(1)。对于字典,in判断的是键是否存在。所以当你需要频繁判断“某个元素是否在大量数据中”,把列表转成集合的收益非常可观。
另外,in不只用于判断,它还能和for循环配合,做迭代。它的实现原理是调用对象的__contains__方法。也就是说,自定义类可以重写__contains__,让in的行为符合你的业务逻辑。比如:
class MyRange: def __init__(self, start, end): self.start = start self.end = end def __contains__(self, item): return self.start <= item < self.end print(5 in MyRange(1, 10)) # True这种高级玩法在写领域模型时挺有用的。理解in的底层逻辑,能帮你从“会用”变成“用得对”。
2.10 逻辑操作符的返回值:从表达式到值的转换
第十题已经在前面的 2.4 里提到了,0 or 42返回42,1 and 99返回99。
再深入一点:or的操作逻辑是“从左往右找第一个真值,找到就返回它,找不到就返回最后一个值”;and是“从左往右找第一个假值,找到就返回它,找不到就返回最后一个值”。所以:
print(None or 0 or "" or "ok") # "ok" print("a" and "b" and "c") # "c" print(1 and 0 and 2) # 0这个特性最实用的地方是“默认值”赋值。比如从字典里取配置,如果键不存在就用默认值:
level = config.get("level") or 5如果config.get("level")返回None,就会用5兜底。但要注意:如果合法值本身是0、空字符串、空列表这些“假值”,这个写法会把它们也替换成默认值。所以要用默认值时,我更推荐config.get("level", 5),或者在明确知道取值不会为“假值”时才用or语法。
3. 易错点与踩坑记录
做完了测验,下面这些是我从实际项目和代码评审里攒下来的高频坑。每一个都是我亲眼见过有人写错过,甚至我自己也踩过,写出来给大家提个醒。
3.1 操作符优先级:别让代码的表达力毁在括号上
Python的操作符优先级和数学上的直觉大部分一致,但有两个反直觉的点:**右结合、一元负号优先级低于**。前面已经提过,这里再补充一个真实场景。
有一次同事写了个计算体脂率的公式,代码里有这么一行:
result = -1 * bmi ** 2 + 25.5 * bmi - 3他的本意是(-1 * bmi) ** 2,但Python实际计算的是-1 * (bmi ** 2),因为**的优先级高于*和负号。最后算出来的结果偏差极大,排查了很久才发现是括号的问题。
我后来定了一条代码规范:只要表达式超过一个运算符,涉及幂运算、位运算或混合比较,就必须加括号,禁止依赖优先级。这不是因为我记不住优先级,而是因为看代码的人不一定记得住,而且在时间长了以后,连写代码的人自己也会忘。
3.2 列表乘法与嵌套引用
[[]] * 3看起来像创建了一个包含3个空列表的列表,实际上它创建的是“同一个空列表的3个引用”。如果你执行:
matrix = [[]] * 3 matrix[0].append(1) print(matrix) # [[1], [1], [1]]三个元素一起变了,因为它们是同一个对象。这和第六题的b = a是同一类问题:*操作符对列表做的是“复制引用”,不是“复制对象”。
这个坑在初始化多维数组时特别常见。正确写法是:
matrix = [[] for _ in range(3)]用列表推导式,每轮循环都会创建新的空列表。
3.3 浮点数比较:0.1 + 0.2 不等于 0.3
严格来说这不是操作符的问题,而是“==比较浮点数”的经典坑。0.1 + 0.2 == 0.3在Python里返回 False,因为浮点数在二进制下无法精确表示0.1和0.2,最终结果是0.30000000000000004。
业务里最常见的场景是金额计算。比如算商品总价、折扣、税费,如果直接用浮点数加加减减,很容易出现精度问题。解决方案有两种:一是用Decimal类型,二是比较时设定误差范围:
import math print(math.isclose(0.1 + 0.2, 0.3)) # Truemath.isclose是Python 3.5之后提供的函数,默认用相对误差和绝对误差来判断两个浮点数是否“足够接近”,推荐在科学计算和数据分析里比较浮点数时使用。
3.4 判断空值的正确姿势
新手写“判断列表是否为空”时,常见的做法是if len(my_list) > 0或者if my_list != []。这两种写法都对,但更Pythonic、更稳妥的写法是直接if my_list:。
Python中空列表、空字典、空字符串、0、None、False都是“假值”,所以if my_list:在列表为空时自然走else分支。这个写法逻辑简洁,也更贴近“数据是否存在”的语义。
但要注意的是,“假值”判断在特定业务里可能不适用。比如你要判断一个用户的积分数是否为0,直接用if score:会把0当假,弹回提示“没有积分”,但用户可能确实有0积分。这种情况要显式写成if score is not None:。选择哪一种,取决于你想表达的是“非空”还是“有值”。
3.5 运算符重载的意外行为
Python允许自定义类型通过重写__add__、__eq__、__lt__等魔法方法来改变操作符行为。这很强大,但也容易制造“惊喜”。
最典型的是NumPy和Pandas。np.array([1, 2]) == np.array([1, 3])返回的不是布尔值,而是一个布尔数组[True, False]。如果你把这个结果直接放在if里,会报ValueError: The truth value of an array with more than one element is ambiguous。这是因为Python在执行if时会对结果调用bool(),而数组的__bool__无法确定到底该转成哪个布尔值。
写数据处理代码的时候,一定要习惯这种“逐元素比较”的语义。需要整体判断两个数组是否相等,用(arr1 == arr2).all();判断是否存在相等元素,用.any()。自定义类时,也要慎重考虑是否重写操作符,尤其是比较操作符,因为一旦重写,现有的==、<、排序、去重逻辑都可能改变行为。
3.6 字符串拼接:+ 和 join 的效率差异
字符串是不可变对象,所以s = s + "a"每次都会创建新字符串。在循环里做大量拼接时,用+会产生很多临时对象,性能极差。正确的做法是用列表收集,最后''.join(list)。
# 低效 s = "" for word in words: s += word # 高效 s = ''.join(words)这个差异在小数据量时感受不到,但处理几千几万个字符串时,运行时间能差出几十倍。操作符不仅仅是“能不能用”的问题,还有“合不合适”的问题。
4. 把这个测验扩展成更完整的练习
做完一次测验、看完答案,其实只是第一步。操作符这个东西,看懂了不表示会用了,必须通过反复练习和项目实践才能真正内化。下面说说可以怎么继续深化。
4.1 设计一套分级自测题
我把题目分成三个等级,你可以拿这个思路自己出题:
第一级是“识别型”,直接问某个表达式的结果,比如5 // 2、2 ** 10是什么。这个等级考查的是记忆和对基本语法的熟悉程度。
第二级是“理解型”,把表达式放在一个小场景里,比如问“对一个负数做整除取模,结果是什么方向”“为什么[] == []是 True 但[] is []是 False”。这个等级考查的是对运行机制的理解。
第三级是“应用型”,给出实际业务场景,比如“统计一个列表中每个元素出现的次数,可以用什么数据结构”“判断用户权限时用位运算还是布尔组合”。这个等级考查的是知识迁移能力。
我给新人做培训时,操作符测验只是第一关。之后我会让他们把错题整理成一个“坑位文档”,每道错题写明:错误答案、正确答案、原理、避免方法。最后用三到五个实际项目里的代码片段让他们做代码评审,找出其中的操作符隐患。效果比单纯背语法好得多。
4.2 operator 模块:超出符号的操作符
Python的operator模块把操作符对应的功能做成了函数,比如operator.add(a, b)等价于a + b,operator.itemgetter(1)可以根据索引取元素。这在需要把操作符作为参数传递时非常实用。
最常见的应用是sorted排序:
from operator import itemgetter students = [("张三", 88), ("李四", 92), ("王五", 80)] sorted(students, key=itemgetter(1))这里key=itemgetter(1)表示按元组的第二个元素排序。如果自己写lambda x: x[1],也可以,但itemgetter的性能更好,代码也更简洁。
还有一个attrgetter,可以按对象的属性排序。如果你在写ORM或者处理大量数据对象,这两个函数几乎必不可少。
4.3 把操作符放进真实项目里练
如果只做一两道题就完事,效果很有限。我建议用一个小的数据处理任务来练手:读一个CSV文件,里面有姓名、分数、排名,然后计算总分、平均分、及格率,排序,筛选出低于平均分的名单。这个过程会用到算术操作符(计算)、比较操作符(筛选)、逻辑操作符(多条件)、成员操作符(判断是否在名单里)、位操作符(可能用不到但可以故意加一个状态标记)。
这种“项目式练习”的好处是:你会在真实语境里理解操作符到底是干嘛的,而不是孤立地背语法。写完第一版之后,再回头想想有没有更Pythonic的写法,比如用sum()而不是手写循环加+,用all()和any()简化多条件判断。
5. 常见问题与排查技巧实录
最后整理一份我在带新人和自己做项目时反复遇到的问题速查表。这些问题很多不是操作符本身的问题,而是操作符和类型、数据结构、函数交互时出现的问题。
| 报错信息 | 出现原因 | 解决办法 |
|---|---|---|
TypeError: unsupported operand type(s) for +: 'int' and 'str' | 用+拼接整数和字符串 | 用str()转换;或使用f"{age}"格式化 |
TypeError: 'int' object is not callable | 把变量名覆盖了函数名,然后当函数调用 | 检查变量命名,避免用sum、list等内建名做变量 |
ValueError: The truth value of an array with more than one element is ambiguous | 把NumPy数组的逐元素比较结果直接放在if里 | 用.all()或.any()聚合 |
UnboundLocalError: local variable 'count' referenced before assignment | 函数内对全局变量使用了+=,被解释为局部变量 | 在函数内显式声明global count,或改用可变对象 |
IndexError: list index out of range | 用索引访问不存在的元素 | 用len()先检查长度;或使用切片避免越界 |
SyntaxError: invalid character in identifier | 代码里混入了中文符号或全角空格 | 用编辑器的高亮和lint工具检查不可见字符 |
下面针对几个高频问题做一点展开说明。
5.1TypeError与类型转换
这个问题在刚入门时最常遇到。Python的+既可以做数值加法,也可以做字符串拼接,但不能在两种类型之间混用。input()返回的一律是字符串,所以从控制台读到的数字必须手动转换。
age = int(input("请输入年龄: ")) next_year_age = age + 1类型转换不仅仅是数字和字符串之间的转换。列表和元组互转、字典的键值对转列表、字符串转字节串,这些都用得上构造器函数。熟练掌握这些转换,能让操作符在不同类型间安全使用。
5.2+=引发的局部变量困惑
a += 1在函数里会触发一个很隐蔽的规则:如果a在函数内部被赋值,Python会把它当成局部变量,除非你显式声明global a。
count = 0 def increment(): global count count += 1这里如果去掉global count,运行时会报UnboundLocalError。原因不是+=本身有问题,而是 Python“赋值即局部”的规则在起作用。我用一个简单的方法避免这种问题:尽量不在函数里直接修改外部变量,而是把外部变量作为参数传入,把计算结果作为返回值传出来。这样代码更清晰,也不会触发局部/全局的坑。
5.3 除以零与其他运行时错误
ZeroDivisionError出现在分母为0时。分母可能是变量,运行到一半才知道是0。可以提前用判断:
if divisor != 0: result = dividend / divisor else: result = float("inf")位运算里还有个类似问题:右移的位数不能为负数,否则会报ValueError。逻辑上右移负数位相当于左移,但Python不允许这种隐式转换,需要你显式改成左移。
5.4 操作符链与短路带来的性能优势
前面提到短路求值能避免异常,其实它还能提升性能。比如要判断一个列表是否非空且第一个元素是否满足条件:
if my_list and my_list[0] > 0: ...如果my_list为空,右边根本不会执行,不会出现索引越界。这种一行代码就把“先判断再访问”的安全性和“条件与”的逻辑结合在一起,是Python很优雅的地方。
使用all()和any()也能享受短路:
if any(x > 100 for x in scores): print("有高分")只要找到一个符合条件的元素,后面就不会再迭代了。数据量大时,这个性能差异非常明显。
写在最后的个人体会
操作符看起来是Python里最基础的内容,但恰恰是这些最基础的地方,决定了代码的健壮性和可读性。
我记得带过一个实习生,做测验的时候在-7 // 2上栽了跟头。他当时很不服气,说“反正平时都用正数,谁会管负数整除的方向”。结果第二周他在写分页逻辑时,把起始页算成了负数,用整除处理偏移量,页面数据全乱了。他翻回测验题目,才明白当初为什么要考这个负号。
后来我把这段话写进团队的入职文档:操作符测试不是为了考倒谁,而是为了让你在真正写业务代码之前,先把那些会在半夜三点折磨你的问题解决掉。
如果让我只给一条建议,那就是:写代码时,永远把操作符优先级用括号表达清楚。这不但能避免计算顺序的错误,也是对你代码读者的一种尊重。
这份测验你可以直接复制去跑,也可以像我一样拿它来考考身边的人。如果你发现某个问题连自己都说不清原理,那恭喜你,找到一个需要补的盲点。Python的学习就是这样不断“测验-发现-补全”的过程,把每个盲点都堵上,你写代码的底气就会越来越足。