ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python入门避坑:四道经典选择题测出你的基础是否扎实

Python入门避坑:四道经典选择题测出你的基础是否扎实 前两天帮一个刚入门 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.ohtyB.nohtyC.ohtyPD.tyho正确答案是 A。解析这题之前先把索引对应关系写清楚。s Python的长度是 6索引从 0 开始字符Python正向索引012345负向索引-6-5-4-3-2-1len(s)为 6Python 的切片规则统一是s[start:stop:step]start 包含stop 不包含。题目里的写法是s[4:0:-1]start 是 4stop 是 0step 是 -1表示从索引 4 开始向左取每次步进 -1一直取到 stop 之前结束。按这个规则走一遍取索引 4对应字符o取索引 3对应字符h取索引 2对应字符t取索引 1对应字符y索引 0 是 stop不包含所以P不取最终结果就是ohty。错误选项里最有迷惑性的是 C 和 B。选 C 的人通常把 stop0 误认为“包含 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, itemsNone): 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 2B.3 3 -1C.3.5 3 2D.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)等于 3round(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. 输出 10B. 输出 20C. 报错NameError: name x is not definedD. 报错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]start4stop0step-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, itemsNone): 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]Bnohty以为 negative step 会包含 stopstop 不包含取索引 4 到 1第二题 默认参数B[1]和[2]以为每次调用都创建新列表默认参数在 def 时创建一次调用间共享第三题-7 % 3D1按四舍五入思维猜余数余数符号跟随除数-7//3-3余数 2这个表格的关键不是抄题而是把“我当时为什么错”写清楚。所有错误的本质只有几类概念混淆、边界不清、省略细节、想当然。归因越准确下一次改正的针对性就越强。我经常发现同一个知识点如果错了一次记录归因之后基本不会再错但如果只是把正确答案抄一遍过两周遇到变体题还是会错。4. 做题时最容易踩的坑以及我的排查方法4.1 “我会写代码但一做选择题就懵”这是被问到最多的一句话。会说这句话的人通常已经在用 Python 写一些简单的脚本了比如爬虫、数据处理、或者自动化任务。他们的共同特征是代码靠终端里“输入一执行一看到结果”的循环来推进先写个大概然后用 print 一步步确认输出报错就改。这种风格在写业务代码时效率并不低但它没有训练出“预判力”。选择题没有报错提示也没有 print 调试的机会它要求你在几秒钟内让代码在脑子里跑完。这不是智商问题而是练习方式问题。解决方案很简单强制自己用“盲写-手推-运行”三步法花两周时间每天找一个短小的程序先不运行在纸上写出预期输出然后再实际跑一遍。两周之后预判力会有明显提升。4.2 “我的运行结果和解析不一样是环境问题吗”确实有可能是环境问题最常见的是 Python 版本差异。Python 2 和 Python 3 在几个关键行为上完全不同比如/运算Python 2 里7 / 2是整数除法结果为 3Python 3 里是浮点除法结果为 3.5print在 Python 2 里是语句Python 3 里是函数。如果你拿着 Python 3 的代码去旧教程里对答案会经常怀疑人生。还有一个容易混淆的地方是round()的舍入规则。Python 3 默认采用银行家舍入也就是“四舍六入五取偶”所以round(0.5)结果是 0round(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 到 4range(1, 5)s[1:5]索引 1 到 4range(0, 5, 2)s[0:5:2]索引 0、2、4range(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让它帮忙生成几个变体但变体只是素材真正让我进步的还是自己先手推演这一步推演的过程谁也替代不了。
返回列表