☰
Python入门避坑:四道经典选择题测出你的基础是否扎实
2026/10/2 15:04:47 网站建设 项目流程

前两天帮一个刚入门 Python 的朋友做了一次小测验。他刚翻完一本入门书,觉得自己写循环、写函数都没什么问题了。我随手在草稿纸上写了四道选择题,都是最基础的内容:一行切片、一个带默认参数的函数、一行除法取余、一个变量作用域的例子。结果他四题错了三题,而且每道错题他都觉得“答案本来就该是这样啊”。直到我把内存里的对象关系画出来,他才发现自己平时一直在靠 print 试错写代码,并没有真正建立对 Python 执行过程的预判能力。

这也正是我坚持做“每日一练 + 选择题”的原因。日常写代码时,解释器会给出报错提示,这其实是一种“作弊”——它会直接告诉你哪里出了问题;而选择题没有报错,它逼着你在大脑里完整地跑一遍代码,去预判每一步会发生什么。Python 最难学的部分恰恰集中在边界行为上,比如切片越界不报错、默认参数被多个调用共享、负数取余的符号规则、函数作用域遮蔽……这些行为在平时写代码时很难被察觉,却是选择题最好的素材。

这篇文章把我常用的“每日一练”四道经典题完整整理出来,包含题目、选项、答案和逐行解析,并补充了做完之后怎么自查、怎么改题、怎么避坑的实操方法。适合 Python 入门 1 到 3 个月的学习者,也适合准备笔试面试、或者带新人的同学参考。

1. 为什么我用“每日一练+选择题”来检验 Python 水平

1.1 选择题是成本最低的“心智模型体检”

先抛一个观点:如果你只是跟着教程写代码,很容易陷入一种“我懂了”的错觉。教程里的代码大多能一次跑通,你只需要复制、运行、看到结果,然后觉得自己学会了。可一旦把报错提示拿走,让你预测输出,很多问题就暴露了。

选择题的核心价值,就是强迫你建立“心智模型”——也就是说,你的脑子里得有一个会动的 Python 解释器,能逐行执行代码、追踪变量的指向、判断对象的生命周期。这个能力不是靠多写代码就能自然获得的,它需要专门训练。开车的人都知道,仪表盘亮红灯会提示你,但真正的高手在红灯亮之前就能感觉到发动机的异常;写代码也一样,报错是最后一道防线,选择题是在训练你成为那个“提前感觉到问题”的人。

我从大量笔试题目里筛选出了四道高频率出现的经典题,分别切中四个最容易被新手忽略的底层机制。它们的共同点是:代码看着都很短,不到五行的样子,但每一行都是坑。

1.2 四道题覆盖的高频考点地图

题号考察主题最容易踩的坑对应的高频搜索词
第一题字符串与列表切片负步长时 stop 不包含、边界容易算错python 数组切片、python 基础语法
第二题函数定义与默认参数可变默认参数被多次调用共享python 定义函数、python 语法
第三题类型转换、整除与取余int() 不是四舍五入、负数取余的符号python 类型转换、python 语法
第四题变量作用域与函数赋值局部变量遮蔽全局变量,引发 UnboundLocalErrorpython 定义函数、python 基础语法

这四道题不是随便挑的。它们在各大 Python 练习题库、面试题集、计算机二级考试里反复出现,基本属于“入门必过”的四个关卡。这四个考点还正好对应学习者在日常搜索中最常遇到的问题:切片规则、类型转换、函数定义和作用域。很多人搜这些关键词,是因为写代码时遇到了报错,看不懂为什么;而把这四个知识点用选择题的形式打通,从根子上理解机制,写代码时基本就不需要频繁搜索了。

1.3 这套题适合谁,怎么用效果更好

如果你是刚开始学 Python 的小白,建议在学完基础语法后,用这套题做一次自我诊断。8 分钟限时作答,不允许翻书,不允许查资料,也不允许在电脑上跑代码。做完之后先别急着对答案,把每道不确定的题先在本地运行一遍,看看实际输出是多少,再回来看解析。

如果你正准备笔试或面试,这套题的难度可以作为一份锚点。把每道题的解析吃透之后,再去扩展同类知识点,比盲目刷题有效得多。这里解释一个原理:选择题测试的是“再认”,填空和推演测试的是“回忆”,而面试手写代码测试的是“综合应用”。三个层次里,“再认”是门槛最低的,但它能最快筛选出知识盲区。所以哪怕你是老手,做完之后也应该试着把每道题的机制用自己的话讲一遍,这样才能把“看着眼熟”变成“真的会”。

2. 四道经典选择题(含详细解析)

2.1 第一题:负步长切片,边界到底怎么算

先看题目代码:

s = "Python" print(s[4:0:-1])

选项:

  • A.'ohty'
  • B.'nohty'
  • C.'ohtyP'
  • D.'tyho'

正确答案是 A。

解析这题之前,先把索引对应关系写清楚。s = "Python"的长度是 6,索引从 0 开始:

字符Python
正向索引012345
负向索引-6-5-4-3-2-1

len(s)为 6,Python 的切片规则统一是:s[start:stop:step],start 包含,stop 不包含。题目里的写法是s[4:0:-1],start 是 4,stop 是 0,step 是 -1,表示从索引 4 开始向左取,每次步进 -1,一直取到 stop 之前结束。

按这个规则走一遍:

  • 取索引 4,对应字符o
  • 取索引 3,对应字符h
  • 取索引 2,对应字符t
  • 取索引 1,对应字符y
  • 索引 0 是 stop,不包含,所以P不取

最终结果就是'ohty'。

错误选项里最有迷惑性的是 C 和 B。选 C 的人通常把 stop=0 误认为“包含 0”,于是多取了一个P,得到'ohtyP'。实际上如果要包含索引 0,应该写成s[4::-1],省略 stop,表示一直取到序列开头。选 B 的人往往是没搞清楚起始位置就凭感觉选了整个反转结果的前半段,s[::-1]才是完整的反转'nohtyP',而s[-1:-6:-1]则得到'nohty',少了开头的P。

这里有个重要细节:字符串切片在越界时不会报错,Python 会自动把越界的索引截断到边界范围内。日常写代码时,这个特性不容易引起注意,但它是切片规则的一部分。做这类题,我的建议永远是先把索引线段画出来,再按步长走格子,不要心算。心算在负步长的情况下特别容易出错。

2.2 第二题:默认参数是列表时,为什么两次结果不一样

题目代码:

def add_item(item, items=[]): items.append(item) return items print(add_item(1)) print(add_item(2))

选项:

  • A.[1]和[1, 2]
  • B.[1]和[2]
  • C.[1, 2]和[1, 2]
  • D.[1]和[1]

正确答案是 A。

这道题是所有 Python 学习者最容易集体翻车的题之一。很多人会选 B,因为他们以为每次调用add_item时都会重新创建一个新的空列表[],两个调用互不影响。但事实是:默认参数的值在函数定义时只被计算一次,之后每次调用如果不传items参数,用的都是同一个列表对象。

完整执行流程是这样的:

  • 函数第一次调用add_item(1),items指向默认的那个空列表,append(1)之后列表变为[1],函数返回[1],打印出来是[1]。
  • 函数第二次调用add_item(2),因为没有传入items,所以仍然指向同一个列表对象。注意,这个对象已经在第一次调用时被修改成[1]了。继续append(2),列表变成[1, 2],打印出来是[1, 2]。

所以输出是[1]和[1, 2]。

为什么默认参数对象会被共享?因为 Python 定义函数时,def语句本身是一条会被执行的语句。它执行的时候会创建函数对象,并把默认参数作为一个对象保存下来。这个对象一直存活在函数对象上,所有不传该参数的调用都会拿它来用。items.append是原地修改,没有创建新列表,所以修改会被后续调用看到。

这个问题的标准解决方案,是使用None作为哨兵,在函数内部再创建新列表:

def add_item(item, items=None): if items is None: items = [] items.append(item) return items

这个坑看起来只影响默认参数,背后其实关系到 Python 的一个核心认知:函数参数传递既不是传值,也不是传统意义上的传引用,而是“传对象引用”,或者说“赋值传递”。你传进来的是一个对象引用,如果对象是可变类型,函数内部对它的原地修改会直接影响外部。理解了这一点,很多后续的坑都可以提前避开。

我经常把这个现象类比成报名表:默认参数就像印好的固定选项,所有不填的人拿到的是同一张报名表,你在表上写了东西,下一个人拿到的就是被改过的版本。想要每个人拿到干净的表,就得每次发一张新的,也就是用None重新创建。

2.3 第三题:整除、取余与类型转换,负数运算防不胜防

题目代码:

a = 7 // 2 b = int(7 / 2) c = -7 % 3 print(a, b, c)

选项:

  • A.3 3 2
  • B.3 3 -1
  • C.3.5 3 2
  • D.3 3 1

正确答案是 A。

三个变量要拆开看:

第一个7 // 2。//是地板除,也就是向下取整。7 除以 2 的数学结果是 3.5,向下取整得到 3。注意,向下取整不是四舍五入,所以结果不是 4。在正数范围里,地板除和截断看起来一样;一旦遇到负数,两者就分道扬镳了。

第二个int(7 / 2)。7 / 2在 Python 3 里得到的是浮点数3.5,然后int()把这个浮点数转换成整数。int()的规则是“向零截断”,也就是直接去掉小数部分,3.5 变成 3。这里有一个很多新手会踩的坑:int(3.5)等于 3,但round(3.5)也可能等于 3,这是银行家舍入规则导致的;而int(3.6)等于 3,round(3.6)等于 4。所以int()和四舍五入没有关系。

第三个-7 % 3是整道题里最反直觉的部分。Python 的取余运算规则是:余数符号跟随除数方向。具体算的时候,先做地板除:-7 // 3 = -3,因为 -2.33 向下取整是 -3。然后用被除数减去除数乘商:-7 - 3 * (-3) = -7 + 9 = 2。所以结果是 2,不是 -1。

如果是在 C 或者 Java 里,同样的表达式通常得到 -1,因为它们的整数除法是向零截断,余数符号跟随被除数。Python 选择让余数符号跟随除数,是为了保证一个数学等式永远成立:a == (a // b) * b + (a % b)。不信可以验证一下:-7 == 3 * (-3) + 2,两边都成立。

平时写代码时,负数取余最常见于循环索引、分页、时间差计算之类的场景。如果不清楚这个规则,很可能会算出负数索引,导致列表访问越界或者数据错位。顺带记住两个扩展结论:-7 % 4在 Python 里等于 1,因为-7 // 4 = -2,余数-7 - 4 * (-2) = 1;而7 % -3等于 -2,因为除数符号为负,余数也跟随为负。

2.4 第四题:函数内引用再赋值,为什么会报 UnboundLocalError

题目代码:

x = 10 def func(): print(x) x = 20 func()

选项:

  • A. 输出 10
  • B. 输出 20
  • C. 报错NameError: name 'x' is not defined
  • D. 报错UnboundLocalError: local variable 'x' referenced before assignment

正确答案是 D。

这道题几乎每隔几天就会在新手群里出现一次。很多人看到print(x)的时候想:函数外面定义了x = 10,函数里先打印,应该输出 10,然后再把x改成 20。但 Python 的实际行为完全不同。

关键在于:Python 在编译一个函数体的时候,会先扫描整个函数里的赋值语句。只要发现某个变量在函数内被赋值,比如这里的x = 20,它就把这个变量标记为“局部变量”。这个标记是全局生效的,不是从赋值那一行才开始。于是print(x)在执行时,x因为被标记为局部变量但还没绑定任何值,直接报UnboundLocalError。

这里涉及 Python 的作用域查找规则 LEGB,也就是 Local(局部)、Enclosing(嵌套外层)、Global(全局)、Built-in(内置)。函数内一旦存在赋值绑定,外层的同名变量就不会被自动读取,查找过程直接落到局部作用域并失败。

如果确实想修改全局x,常见做法有两种。

第一种,加global声明:

x = 10 def func(): global x print(x) x = 20 func()

这样print(x)会输出 10,然后全局x变成 20。

第二种,不用全局变量,把x作为参数传入,再把结果返回:

x = 10 def func(x): print(x) return 20 x = func(x)

这种设计更推荐,因为全局变量在函数内被随意修改会让程序状态难以追踪。面试时如果写全局变量相关的函数,最好主动说明自己的设计意图。

关于这个考点,还有一个需要区分的经典姊妹题:如果函数内只有print(x),没有x = 20,那么print(x)会正常读取全局变量,输出 10。也就是说,坑只出现在“局部有赋值”和“局部先引用”同时存在的时候。很多人把这两道题混淆,本质上是对“什么时候创建局部变量”的边界还不够清楚。

2.5 做题的通用顺序,先推演再验证

四道题做完之后,我想给一个固定的做题方法论。无论面对什么程序输出类的选择题,我都建议按这个顺序操作。

第一步,先判断这道题在考什么。是作用域?是可变对象引用?是运算符优先级?还是类型转换规则?看穿考点之后,大脑会自动调取对应的知识框架,而不是靠感觉蒙答案。

第二步,用最小数据量手推一遍。比如第四题,把代码缩短成“全局变量x、函数内先打印再赋值”这个最小模型,在脑子里一步步执行。如果推不出来,先把各变量的引用来来回回画清楚。

第三步,不确定的时候画内存图。不用画得很复杂,只需要画变量名、对象、引用箭头三条信息。第二题里只要画出items和那个共享列表之间的箭头,就不会误选 B。

第四步,跑代码验证。验证不是目的,目的是对比“我推演的结果”和“实际输出”之间的差距,差距就是你的知识盲区。

最后一步,也是最容易被忽略的:对完答案后,把每个错误选项为什么错也写一遍。选择题的干扰项往往是出题人特意埋的雷,比如第一题的 C 就是“包含 stop”这个常见误解。把干扰项分析透,比抄十遍正确答案有用得多。

3. 做完题之后,如何把练习变成能力闭环

3.1 不看答案,亲手推一遍才是赚到

很多人的做题习惯是:看到题目,脑子里一闪,觉得“好像是这个”,然后直接翻答案,对完也懒得细看。这样做题效率极低。正确做法是:把每道题的完整执行过程用文字写出来。

以第一题为例,推演过程可以写成:

  • s 是"Python",索引 0 到 5 分别为 P y t h o n。
  • 切片s[4:0:-1],start=4,stop=0,step=-1。
  • 从索引 4 出发,依次取 4、3、2、1,索引 0 不包含。
  • 对应字符为 o、h、t、y,结果是'ohty'。

这个过程看起来很笨,但它强迫你把索引的边界、步长的方向都过一遍。下一次再遇到类似题,你的大脑就能直接调用“左闭右开 + 负步长从右往左”这套规则,而不是从零开始乱猜。人在面对复杂问题时容易偷懒,写推导过程就是在对抗这种偷懒。

第四题的推导也建议写清楚:“函数体内存在对 x 的赋值语句,因此 x 被整体视为局部变量;print(x) 执行时局部 x 尚未绑定,因此抛出 UnboundLocalError。”你能写出这句话,说明你真的理解了作用域机制,而不是只记住了报错信息。

3.2 改一个参数,把每道题变成四道题

我特别推荐一个练习方法:把做过的题改一个参数,变成新题。这比做十道同质化题目更省时间,因为它要求你理解的是机制,而不是记住答案。每个题都可以做三个方向的改造。

第一题可以改成:

print(s[::-1]) # 反转全部,结果为 'nohtyP' print(s[-1:-6:-1]) # 从 n 取到 y,结果为 'nohty' print(s[5:0:-2]) # 索引 5、3、1,结果为 'nhy'

第二题可以改成:

def add_item(item, items=None): if items is None: items = [] items.append(item) return items

改成这个版本之后,两次调用输出会变成[1]和[2]。你还能继续加一个场景:如果调用时传入一个已有的列表,函数会对这个已有列表做原地修改,这就要牵扯到函数外部的列表是否被改变的问题。

第三题可以改成:

print(-7 // 4) # -2 print(-7 % 4) # 1 print(7 % -3) # -2

第四题可以改成:

x = 10 def func(): global x print(x) x = 20 func()

这种改造练习,本质上是在做“最小化的对比实验”。同一个知识点,改一个符号,结果完全变化,这个反差会加深记忆。我自己学新语法点时也用这个套路,而且效果比反复看教程好得多。

3.3 错题本不是抄题,而是记录思维偏差

错题本在编程学习里被很多人忽视了,因为代码题不像英语阅读那样有个标准答案。但我觉得,错题本的价值恰恰在于记录“思维偏差”。下面是我自己在用的记录格式。

题目我的错误答案错误归因正确机制
第一题s[4:0:-1]B(nohty)以为 negative step 会包含 stopstop 不包含,取索引 4 到 1
第二题 默认参数B([1]和[2])以为每次调用都创建新列表默认参数在 def 时创建一次,调用间共享
第三题-7 % 3D(1)按四舍五入思维猜余数余数符号跟随除数,-7//3=-3,余数 2

这个表格的关键不是抄题,而是把“我当时为什么错”写清楚。所有错误的本质只有几类:概念混淆、边界不清、省略细节、想当然。归因越准确,下一次改正的针对性就越强。我经常发现,同一个知识点如果错了一次,记录归因之后基本不会再错;但如果只是把正确答案抄一遍,过两周遇到变体题还是会错。

4. 做题时最容易踩的坑,以及我的排查方法

4.1 “我会写代码,但一做选择题就懵”

这是被问到最多的一句话。会说这句话的人,通常已经在用 Python 写一些简单的脚本了,比如爬虫、数据处理、或者自动化任务。他们的共同特征是:代码靠终端里“输入一执行一看到结果”的循环来推进,先写个大概,然后用 print 一步步确认输出,报错就改。这种风格在写业务代码时效率并不低,但它没有训练出“预判力”。

选择题没有报错提示,也没有 print 调试的机会,它要求你在几秒钟内让代码在脑子里跑完。这不是智商问题,而是练习方式问题。解决方案很简单:强制自己用“盲写-手推-运行”三步法,花两周时间,每天找一个短小的程序,先不运行,在纸上写出预期输出,然后再实际跑一遍。两周之后,预判力会有明显提升。

4.2 “我的运行结果和解析不一样,是环境问题吗”

确实有可能是环境问题,最常见的是 Python 版本差异。Python 2 和 Python 3 在几个关键行为上完全不同,比如/运算:Python 2 里7 / 2是整数除法,结果为 3,Python 3 里是浮点除法,结果为 3.5;print在 Python 2 里是语句,Python 3 里是函数。如果你拿着 Python 3 的代码去旧教程里对答案,会经常怀疑人生。

还有一个容易混淆的地方是round()的舍入规则。Python 3 默认采用银行家舍入,也就是“四舍六入五取偶”,所以round(0.5)结果是 0,round(1.5)结果是 2。很多初学者以为它和数学里的四舍五入一样,于是对不上答案。遇到输出不一致时,先看看自己的 Python 版本:

import sys print(sys.version)

还有一种情况不是版本问题,而是交互式环境的干扰。在 REPL(交互式解释器)里,单独的表达式会自动打印返回值,比如你输入x = [1, 2],有些环境不会打印,但你输入x就会打印[1, 2]。如果题目给的输出是针对脚本文件的,你在交互式环境里看到的多余输出可能会误导判断。建议所有验证都放进.py文件里跑,排除环境差异。

4.3 切片和 range 老是混,一张表说清楚

有的同学每次遇到切片都要在纸上算半天,因为range和切片的规则很像但又不完全一样。其实只要记住一点:切片的 start、stop、step 语义和range完全一致,都是 start 包含、stop 不包含、step 决定方向和步长。

range 写法切片写法产生的元素
range(5)s[:5]索引 0 到 4
range(1, 5)s[1:5]索引 1 到 4
range(0, 5, 2)s[0:5:2]索引 0、2、4
range(4, 0, -1)s[4:0:-1]索引 4、3、2、1

如果觉得 range 也不好记,就用一句话概括:range 生成的是“索引序列”,切片取的是“这些索引对应的元素”,两个知识点共用同一套边界逻辑。把 range 的边界规则练熟了,切片自然就通了。

还有一个容易让人困惑的点:切片越界不会报错,但 range 越界会正常停止。实际上,切片遇到越界索引会静默截断,range 则是按数学区间来生成,本质上仍然是同一套边界逻辑。做题时万一拿不准,可以先转换成 range 写出来,再映射回切片。

4.4 遇到没见过的语法,先查官网而不是瞎猜

做题的时候偶尔会遇到“这个语法我完全没见过”的情况。比如看到functools.lru_cache、@property、*args和**kwargs混在一起,有人会慌。我的建议是:先在题目里判断它考的是“语法糖”还是“核心机制”。大多数选择题的考点还是基础机制,比如可变对象、作用域、类型转换,这些跟你用没用过某个冷门库没有关系。

遇到确实没见过的新语法,优先打开官方文档,或者直接在终端里用help()函数查看说明。不要为了图方便去某个代码博客里复制结论,二手信息容易带错。以*args和**kwargs为例,官方文档里一句话就能说清:*args接收任意数量的位置参数并以元组保存,**kwargs接收任意数量的关键字参数并以字典保存。你只需要在本地跑一个最小实验,就能完全掌握。

这里还隐含了一条学习原则:学习 Python 语法,重点不是背诵每个写法的名字,而是理解为什么需要这种写法,它解决什么问题。选择题里出现的干扰项,基本都来自对这个“为什么”的理解偏差。把坑挖到底层,考试、面试、包括实际写代码,都会从容很多。

这套题我给不少新人用过,有一个很稳定的现象:几乎没有人第一题错,但第二题和第四题往往全军覆没。原因其实很简单,切片规则再绕,你总能通过画图解决;而默认参数的共享和函数作用域的遮蔽,属于“反直觉行为”,如果不是专门踩过坑,光靠想当然一定猜错。我自己的习惯是,每学完一个新知识点就出三道小题,一道正向题、一道边界题、一道故意埋坑的题,隔一周再拿来做一遍。如果全对,说明这个知识点真的“内化”了;如果错了,错的地方刚好就是下一步要补的漏洞。最近我还会把做过的题丢给 AI,让它帮忙生成几个变体,但变体只是素材,真正让我进步的还是自己先手推演这一步,推演的过程谁也替代不了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询