Python 面试题:10 道分主题详解

不是又一份「一百道题」清单。十道题,按面试官真正要探的五个方面分组,每组先说清这类题在考什么——然后是能推演的代码、答案,以及他们接下来一定会追问的那一句。

多数 Python 面试题清单只给答案,不告诉你在考什么。所以有人能背出「元组不可变」却照样丢分:面试官要的是那个后果——不可变换来可哈希,可哈希才是元组能当字典键、而列表不能的原因。

下面这些是反复出现的题,聚成五块:内置类型到底保证了什么、默认参数与闭包如何捕获值、同一性何时不等于相等性、执行模型对线程和内存做了什么、以及类级状态如何在实例之间漏出去。其中两道——可变默认参数与循环里的 lambda——是同一个机制的两个侧面,这正是面试官爱把它们配成一对的原因。

先自己把代码推一遍再展开解析。每条解析都点出通常紧跟着来的那个追问,因为真实面试里,第一个答案只换来被问更难一题的资格。

五个方面,十道题

内置类型,以及各自会在哪儿翻车

2 道题

面试官很少想听教科书那句「元组不可变」。他们想听的是后果:不可变才带来可哈希,可哈希才使它能当字典的键、能放进集合。先说机制、再给一个真正用得上的场景,两句话就说完了别人绕一整段还没说清的事。

通常怎么问「list 和 tuple 有什么区别?」·「为什么 list 不能当字典的键?」

第 1 题

除了「一个可变一个不可变」,list 与 tuple 在实践中真正的区别是什么?

  1. Atuple 永远更快,所以处处优先用它
  2. Btuple 可哈希(前提是内部元素也可哈希),因而能当字典键、能进集合;list 不行
  3. Clist 能装混合类型,tuple 不能
  4. Dtuple 不能嵌套进其他容器
看答案与解析

正确答案B. tuple 可哈希(前提是内部元素也可哈希),因而能当字典键、能进集合;list 不行

🐱 不可变是原因,可哈希才是你真正用得上的结果。d[(1, 2)] = 'x' 可以,d[[1, 2]] = 'x' 会抛 TypeError: unhashable type。还有一个能把好答案拉开差距的补充:元组只有在内部元素全都可哈希时才可哈希——hash((1, [2])) 照样报错。两者都能装混合类型;速度差异小到不足以成为选型理由。

第 2 题

这段代码打印什么?为什么?

a = [1, 2, 3]
b = a[:]
c = a
a.append(4)
print(len(b), len(c))
  1. A4 4
  2. B3 4
  3. C3 3
  4. D4 3
看答案与解析

正确答案B. 3 4

🐱 b = a[:] 创建了一个新列表(浅拷贝);c = a 只是给同一个对象再绑了个名字。通过 a 追加元素,c 看得见、b 看不见,所以答案是 3 4。追问几乎必来:切片拷贝是的——如果列表里装的是子列表,b 里的子列表仍然和 a 里是同一批对象,这时才需要 copy.deepcopy

函数、默认参数与闭包

3 道题

这一组是面试官区分「用过 Python」和「读过 Python」的地方。两个坑反复出现:默认参数在定义时求值一次(所以可变默认值会被所有调用共享),闭包按引用捕获变量而非按值(所以循环里造出来的 lambda 看到的是循环变量的最终值)。能把这两个机制讲清、并说出各自的修法,这一组的问题基本就覆盖了。

通常怎么问def f(x=[]) 有什么问题?」·「讲讲装饰器」·「为什么这一串 lambda 打印的是同一个数?」

第 3 题

这段代码打印什么?

def add(item, target=[]):
    target.append(item)
    return target

print(add(1))
print(add(2))
  1. A先 [1],后 [2]
  2. B先 [1],后 [1, 2]
  3. C先 [1],后 []
  4. D抛出 TypeError
看答案与解析

正确答案B. 先 [1],后 [1, 2]

🐱 那个默认的 [] 只在执行 def 那一行时创建一次,不是每次调用都新建。于是两次调用改的是同一个列表,得到 [1][1, 2]。面试官想听的修法:默认值写 None,进函数体内再建(if target is None: target = [])。注意这个坑只咬可变默认值;def f(x=0) 没问题,因为整数无法原地修改。

第 4 题

这段代码打印什么?

fns = [lambda: i for i in range(3)]
print([f() for f in fns])
  1. A[0, 1, 2]
  2. B[2, 2, 2]
  3. C[3, 3, 3]
  4. D抛出 NameError
看答案与解析

正确答案B. [2, 2, 2]

🐱 每个 lambda 闭包捕获的是变量 i 本身,而不是创建时刻的值。等到任何一个 lambda 真正被调用时,推导式早已结束、i 是 2,于是三个都返回 2。标准修法是在定义时把值绑住:lambda i=i: i——默认参数会立刻求值,正是上一题那条「默认值在定义时求值一次」的规则,这次被反过来利用了。

第 5 题

用一句面试官会认可的话解释:装饰器是什么?

  1. A一种能改变 Python 编译方式的注释
  2. B一个接收函数、返回替代品的可调用对象,用 @ 语法应用
  3. C运行时强制执行的类型注解
  4. D让函数在独立线程中运行的手段
看答案与解析

正确答案B. 一个接收函数、返回替代品的可调用对象,用 @ 语法应用

🐱 def f 上面写 @log,等价于 f = log(f)——没有比这更玄的东西。把这个等价关系说出口,后续追问就都好答了:多个装饰器自下而上生效(离 def 最近的先包),以及包装函数应带 functools.wraps 以保住原函数名和文档字符串。只会背「给函数增加功能」的人,通常卡在叠加顺序那一问上。

同一性、相等性与拷贝

1 道题

is 比较同一性(内存中是不是同一个对象),== 比较值——这句人人都会说。分数拉开在后半句:你在小整数或短字符串上见过的那些反直觉的 is 结果,来自 CPython 把它们驻留了,那是实现细节,因此绝不能写依赖它的代码。一句「那是 CPython 的实现细节,不是语言保证」,能把这道题收得漂亮。

通常怎么问is== 有什么区别?」·「为什么 256 is 256257 is 257 表现不同?」

第 6 题

什么时候该用 is 而不是 ==

  1. A比较数字时都该用,因为更快
  2. B与 None、True、False 这些单例比较时
  3. C比较字符串时用,可避免编码问题
  4. D永远不用;is 已废弃
看答案与解析

正确答案B. 与 None、True、False 这些单例比较时

🐱 当你真正想表达「就是同一个对象」时,is 才是对的工具:if x is None 是判空的惯用写法,因为 None 是单例,而自定义类完全可以重写 __eq__x == None 说谎。比较值就用 ==。在数字或字符串上用 is,在交互式解释器里看着能用(因为驻留),一旦碰到缓存范围之外的值就崩——这正是本题要抓的 bug。

执行模型:GIL 与生成器

2 道题

GIL 这道题真正在问的是:你知不知道它拖累的是哪类活。一句话足够:全局解释器锁意味着同一时刻只有一个线程在执行 Python 字节码,所以多线程加速不了 CPU 密集型任务——但对 I/O 密集型有用,因为等待网络或磁盘时锁会被释放。接着说出逃生门:CPU 密集要并行就上 multiprocessing(或原生扩展)。生成器则是内存侧的对应物:一次产出一个元素,而不是先把整个列表建出来。

通常怎么问「GIL 是什么?它怎么影响你的代码?」·「列表推导式还是生成器?为什么?」

第 7 题

你的脚本大部分时间都在等 HTTP 响应。用 Python 多线程能加速吗?

  1. A不能——GIL 阻止了一切并发
  2. B能——等待 I/O 时 GIL 会被释放,多个线程的等待因此重叠起来
  3. C只有在运行时关掉 GIL 才行
  4. D只能用多进程;线程从来没用
看答案与解析

正确答案B. 能——等待 I/O 时 GIL 会被释放,多个线程的等待因此重叠起来

🐱 这正是本题要考的分野。GIL 串行化的是字节码执行,不是等待:线程阻塞在 socket 上时会释放锁,其他线程照跑。所以 I/O 密集型任务用线程(或 asyncio)能很好地并行。选项 D 是典型的矫枉过正——听到「GIL 让线程没用」就套到所有场景,包括线程恰恰是对的工具的这一种。

第 8 题

你要把一个一千万行日志文件里的字节数求和。哪种写法更好?为什么?

# A
total = sum([int(line) for line in f])

# B
total = sum(int(line) for line in f)
  1. AA——列表推导式总是更快
  2. BB——生成器表达式避免了在内存里建出一千万元素的列表
  3. C两者等价;Python 会把 A 优化成 B
  4. DB 在 sum() 里是语法错误
看答案与解析

正确答案B. B——生成器表达式避免了在内存里建出一千万元素的列表

🐱 A 先把所有值都实体化进一个列表再求和——一千万个整数同时占着内存。B 一次只产出一个值,内存占用是平的。可以直接说出的通则:需要保留或多次使用元素时才建列表;只打算迭代一次时用生成器——而 sum 恰恰只迭代一次。Python 不会替你把 A 改写成 B。

类:共享状态的坑

1 道题

类属性这道题之所以被问,是因为一个具体的 bug:可变类属性被所有实例共享,于是一个对象上的 append 会出现在所有对象上。它是「可变默认参数」的面向对象版表亲;面试官爱考它,因为背下了定义的人照样会栽在代码版本上。至于双下方法,能落地的短答案是:__repr__ 给开发者看、应当无歧义;__str__ 给用户看、应当易读——只定义一个的话就定义 __repr__,因为 str() 会回退到它。

通常怎么问「类属性和实例属性的区别?」·「__str____repr__ 有什么不同?」

第 9 题

这段代码打印什么?

class Team:
    members = []

    def add(self, name):
        self.members.append(name)

a, b = Team(), Team()
a.add('Ann')
print(len(b.members))
  1. A0
  2. B1
  3. C抛出 AttributeError
  4. D2
看答案与解析

正确答案B. 1

🐱 members 是类属性——所有实例共享同一个列表a.add('Ann') 改的就是那个共享列表,所以 b 也看得见,答案是 1。修法是把每个实例自己的状态建在 __init__ 里:def __init__(self): self.members = []。有个值得一提的细节:重新绑定self.members = [...])会创建实例属性、不影响其他对象——真正漏出去的是通过共享引用做的原地修改

继续练

Python 面试题 — 常见问题

Python 面试最常问哪些题?

初级到中级面试里反复出现的是:list 与 tuple(以及可哈希如何由不可变推出)、可变默认参数、is==、GIL 对线程的影响、列表推导式与生成器、装饰器、浅拷贝与深拷贝。本页全部覆盖。

答到多深合适?

先给机制、再给一个后果,然后停。「默认参数在定义时求值一次,所以可变默认值会被多次调用共享,因此默认值要写 None」——这就是完整答案。再往下铺,只会在你把分数落袋之前招来更难的追问。

256 is 256 为真,这个能依赖吗?

不能。小整数缓存与字符串驻留都是 CPython 的实现细节,不是语言保证。在面试里明确说出这一点,通常比记住确切的缓存区间更值钱。

这些题只针对 Python 3 吗?

是。本页全部按 CPython 上的 Python 3.x 陈述——除非面试方另行说明,这也是面试默认的前提。

这些是真实面试题吗?

它们是围绕面试反复考查的概念编写的原创题,不是任何公司面试流程的实录。这一点有实际意义:你练的是机制,而不是背某一家的题单。