
几周前,我旁听了一些针对中级 Python 岗位筛查面试电话。我不是负责提问的人,而是那个坐在角落做笔记的人。我想弄明白,为什么那些履历看起来不错的人,在前十五分钟里的表现却不尽人意。
原因出乎意料之外,表现不佳的人,并不是因为数据结构或刁钻的算法题失分,他们败在了一些基础问题上。这些问题看起来简单,读起来毫无难度,但当有人要求你当场大声解释时,你却可能突然卡住。
我把这些问题列出来。下面这些问题最能拉开差距。我会说明它们为什么重要,以及一个好的回答应该是什么样子。
在函数中使用默认参数会发生什么?
这其实是这门编程语言的一个老陷阱,直到今天仍然会让许多资深开发者措手不及。
这个问题通常会伪装成下面的代码:
def add(item, bucket=None): if bucket is None: bucket = [] bucket.append(item) return bucket
当面试官问这个问题时,他们真正想知道的是:你是否理解函数是如何工作的。函数定义本身是一条会被执行的语句。函数中的默认值是对象,而这些对象具有一定的生命周期。
有些人只是写出“碰巧能运行”的代码,从来没有认真思考过函数究竟是如何工作的。他们只是写代码。代码确实能运行,但他们不理解函数定义及其默认参数背后的细节。
面试官想知道你是否理解这些函数的重要细节:函数定义是一条会被执行的语句,它的默认参数是具有生命周期的对象。
这个拷贝真的是拷贝吗?
给某人一个嵌套列表,然后让他创建一个副本,并且能够修改副本而不影响原始对象。看看他会怎么做。
original = [[1, 2], [3, 4]]copy = original[:]copy[0][0] = 99print(original) # [[99, 2], [3, 4]]
切片操作确实创建了一个新的外层列表,但内层列表仍然是同一个对象,被两个列表共同引用。
这就是浅拷贝(shallow copy)和深拷贝(deep copy)的区别。大多数人听说过这些术语,却从未真正体会它们带来的后果。
生产环境里一半的调试工作,本质上都是因为两个名字指向了同一个对象,而你却以为它们彼此独立。
一个不错的回答会提到 copy.deepcopy。更重要的信号是:这个人是条件反射般地想到它,还是因为曾经被这个问题狠狠坑过。你甚至能从他的回答听出那些“伤疤”。
不用“惰性(lazy)”这个词来解释生成器
我喜欢这个问题,因为专业术语往往是一种拐杖。
很多人都能背出“生成器是惰性的,而且节省内存”。当你调用 next() 时,函数执行状态会发生什么,很少有人能真正解释清楚。
def counter(): n = 0 while True: yield n n += 1
真正值得说的是:yield 会冻结函数。
它的局部状态、变量 n 的值、循环执行到的位置——所有这些都会被挂起。之后,当你请求下一个值时,这些状态会被原封不动地恢复,函数会从上次停下的地方继续执行。
函数并不是一直运行然后偶尔暂停。更准确地说,它本身就是一个暂停状态的实体,你每次调用时都在恢复它。
真正理解这一点的人,通常也会明白为什么生成器只能遍历一次,以及为什么把一个无限生成器直接转换成列表会耗尽内存并导致程序崩溃。
在建立了正确的心智模型之后,那些术语自然会跟着出现,反过来永远行不通。
全局解释器锁(GIL)到底阻止了你做什么?
多年以来,这都是一个只有固定答案的问题。
但到了 2026 年,情况变得越来越有意思,因为答案正在快速变化。
Python 3.13 发布了一个移除 GIL 的实验版本,Python 3.14 又进一步改进了这项工作。
很多人会说:“GIL 让 Python 很慢。”但这样的说法太笼统了。
GIL 的含义是:同一时刻只能有一个线程执行 Python 字节码。因此,如果你的任务主要消耗 CPU,使用多线程基本帮不上什么忙。
然而,对于涉及等待的任务——例如网络读取或磁盘 I/O——线程依然很好用。因为当线程处于等待状态时,它会释放 GIL。
接下来的问题是:如果你需要真正的并行计算,该怎么办?
最直接的答案是使用多进程,或者使用像 NumPy 这样的库,让它替你处理 GIL 的问题。
另一种选择是自由线程(free-threaded)解释器,这使得过去那些一成不变的建议开始变得不那么绝对了。
为什么 is 不等同于 ==?它什么时候会坑到你?
每个人都知道:
== 比较值(value)
is 比较身份(identity)
但仅仅背出这句话,并不足以通过这个问题。真正奇怪的部分在这里:
a = 256b = 256a is b # Truec = 257d = 257c is d # 通常为 False
在 CPython 中,某些整数会被缓存,比如从 -5 到 256 之间的数字。
这些数字就像始终复用的对象一样。
一旦超出这个范围,这条规则就不再适用了。
真正理解这一点的人,不只是记住了一个知识点。他们理解了 is 检查的是两个对象是否为同一个对象。
他们知道,不应该用 is 来判断两个东西是否具有相同的值,即便在快速测试时看起来能正常工作。
一个简单的经验法则是:
当检查 None 时使用 is
当你想确认两个引用是否真的指向同一个对象时使用 is
其他所有值比较都使用 ==
CPython 对整数的缓存机制,恰恰说明了为什么应该遵循这条规则。
给它加个装饰器,然后告诉我你弄坏了什么
让某人写一个简单的计时装饰器,大多数人都能完成。
然后再问一句:被装饰之后,函数的名字和文档字符串会发生什么?
import timedef timed(fn): def wrapper(*args, **kwargs): start = time.perf_counter() result = fn(*args, **kwargs) print(time.perf_counter() - start) return result return wrapper
这里的问题在于:它悄悄破坏了自己本来应该包装的函数。
加上装饰器之后,函数名会变成 wrapper。原来的文档说明也消失了,因为你已经用另一个函数替换了它。
解决办法很简单:导入一个东西,再加上一行代码即可,那就是 functools.wraps。
知道这么做的人,通常是那些曾经花费大量时间排查问题的人。他们见过无数令人困惑的报错信息,而那些信息里总是写着 wrapper。
这个问题的巧妙之处在于:最简单的实现方式看起来完全没问题。
不会报错,也不会出现明显异常。
问题只会在后面使用工具、调试程序或理解代码行为时暴露出来,然后让你一头雾水。
面试真正想测试的是:能运行(works)和做得正确(done right)之间的区别。
这个列表推导式把什么泄漏出去了?
最后用一个小问题收尾,因为它经常能难住那些自认为已经超越基础知识的人。
funcs = [lambda: i for i in range(3)]print([f() for f in funcs]) # [2, 2, 2]
很多人以为结果会是:
[0, 1, 2]
实际上得到的是:
[2, 2, 2]
原因在于,每个 lambda 捕获的是变量 i 本身,而不是函数创建时 i 的值。等到这些函数真正被调用时,循环早就结束了,而 i 已经停留在最后一个值上。
修复方法是在 lambda 中把 i 设为默认参数:
lambda i=i: i
这样就把当时的值保存下来了。
带有循环变量的闭包是非常隐蔽的 Bug 来源。它们可能潜伏很长时间,然后突然制造大麻烦。当有人问到这个问题时,其实是在考察你是否真正理解闭包与变量的关系。你需要知道:闭包捕获的是变量本身,而不是某个时间点上的变量值。
这些问题究竟在测试什么?
回头看看这份清单,你会发现一个共同模式。这些问题没有一个需要讲算法的教科书。它们考察的都是:代码实际如何运行,而不仅仅是代码表面上看起来如何。
可变默认参数、共享引用、暂停状态的生成器、全局解释器锁(GIL)、身份与相等性、装饰器悄悄改变了什么,以及闭包真正捕获的到底是什么。
这才是真正的筛选标准。
重点不在于问题有多难。面试官真正想知道的是:你是否已经使用这门语言足够久,以至于亲身踩过这些坑。
他们想知道,你只是短暂地用过这门语言,还是真正与它长期相处过。“踩过坑”这种经历是很难伪装出来的,这也是为什么这些看似简单的问题如此有价值。
如果其中任何一个问题让你感到不舒服,其实是件好事。
以上就是“2026年 Python 面试最容易翻车的几个基础问题!”的详细内容,想要了解更多Python教程欢迎持续关注编程学习网。
扫码二维码 获取免费视频学习资料

- 本文固定链接: http://www.phpxs.com/post/14264/
- 转载请注明:转载必须在正文中标注并保留原文链接
- 扫码: 扫上方二维码获取免费视频资料