笔者担任 Python 面试官多年,积累了很多面试题,特整理起来,希望对求职者有所帮助。此外,我们从网上摘录了很多经典面试题,配以详尽的讲解,举一反三。
我们将不定期更新,订阅可以关注我们的公众号: 小菜学编程 。
面试题
- 用一行代码实现整数 1 至 100 之和
网上的答案是通过 range 生成 1 至 100 的整数,然后用 sum 求和:
| 1
2
3
 | >>> # 解法一
>>> sum(range(1, 101))
5050
 | 
 
这行代码确实很有美感,但你想过没有:如果是求 1 至 10000000000 之和呢?候选人必须认识到这是一个 O(N) 算法,真的适合所有场景吗?为什么不用等差数列前 N 项和公式进行计算呢?
| 1
2
3
4
 | >>> # 解法二
>>> n = 100
>>> (n + 1) * n >> 1
5050
 | 
 
采用前 N 项和公式,求和时间复杂度是 O(1) ,孰优孰劣应该很明显了吧。大家可以对比下当 N 很大时,这两种计算方式的表现:
| 1
2
3
4
5
 | >>> n = 100000000
>>> sum(range(1, n+1))
5000000050000000
>>> (n + 1) * n >> 1
5000000050000000
 | 
 
面试官喜欢引申,候选人如果只是刷题记答案而不会分析,肯定是过不了关的。
- 如何在一个函数内部修改全局变量
在函数内部用 global 关键字将变量申明为全局,然后再进行修改:
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
 | >>> a = 1
>>> def func():
...     global a
...     a = 2
...
>>> print(a)
1
>>> func()
>>> print(a)
2
 | 
 
面试官还可能引申到以下概念讨论,必须滚瓜烂熟:
- 变量作用域 ( scope )
- 局部名字空间 ( locals )
- 闭包名字空间 ( globals )
- 全局名字空间 ( enclosing )
- 内建名字空间 ( builtin )
- 请描述执行以下程序将输出什么内容?并试着解释其中的原因。
| 1
2
3
4
5
6
7
 | def add(n, l=[]):
    l.append(n)
    return l
print(add(1))
print(add(2))
print(add(3))
 | 
 
| 1
2
3
 | [1]
[1, 2]
[1, 2, 3]
 | 
 
这有点令人丈二和尚摸不着头脑,明明默认参数是一个空列表,为什么第 2 、 3 次调用后,列表都比预期中多一些数值呢?这一切得从 Python 函数的运行机制说起—— Python 函数默认参数是如何实现的?
Python 函数在创建时便完成了默认参数的初始化,并将默认参数保存在函数对象的 __defaults__ 字段中:

当我们调用 add(1) 时,Python 虚拟机创建一个 栈帧 对象 PyFrameObject ,用于保存函数执行过程中的上下文信息。栈顶对象保存函数局部变量以及一个运行栈,Python 虚拟机负责从函数对象中取出默认参数并设置相关局部变量:

add(1) 执行完毕后,作为函数默认参数的那个 list 对象,就包含了一个元素 1 :

当我们再次调用 add(2) 时,Python 虚拟机还是从函数对象中取出这个 list 对象作为 l 的默认参数。因此,第二个 print 语句输出 [1, 2] 也就不奇怪了。
总结起来,默认参数在函数对象创建时便完成了初始化,并保存在函数对象中。当函数被调用时,Python 从函数对象中取出默认参数,而不是重新初始化。因此,无论 add 函数被调用多少遍,默认参数总是同一个 list 对象。
这与我的直观感觉相悖,因此尽量不要用可变对象作为默认参数,以避免一些潜在的 BUG 。如果实在无法避免,则可以换一种更的严谨写法:
| 1
2
3
4
5
 | def add(n, l=None):
    if l is None:
        l = []
    l.append(n)
    return l
 | 
 
当 add 函数被调用时,如果参数 l 未指定,Python 自动使用默认值 None 。函数内部对参数 l 进行判断,如果它的值是 None ,便将其设为一个新的空列表。这样,add 函数的行为就更符合我们的预期了。
- 列出 5 个 Python 标准库
这是一个开发性题目,面试官以考察候选人知识面以及学习深度为目的。必须结合自身情况,选择一些自己比较熟悉的标准库作答,面试官随时可能深入讨论。
保险一点,可以回答一些常用但很浅显的,例如:
想要获得加分,也可以回答一些高级的,例如:
面试官很有很能深入提问,切记:如果自己不是很熟悉,就不要班门弄斧了。
- 字典如何删除键
方法一 ,使用 del 语句进行删除, del 关键字还可用于删除 变量 、 属性 :
| 1
2
3
4
 | >>> ages = {'tom': 18, 'jim': 20, 'lily': 19}
>>> del ages['jim']
>>> ages
{'tom': 18, 'lily': 19}
 | 
 
方法二 ,调用 pop 方法进行删除,这样可以拿到被删除键对应的值:
| 1
2
3
4
5
6
 | >>> ages = {'tom': 18, 'jim': 20, 'lily': 19}
>>> jims_age = ages.pop('jim')
>>> jims_age
20
>>> ages
{'tom': 18, 'lily': 19}
 | 
 
- 如何合并两个字典
| 1
2
 | >>> info1 = {'name': 'jim', 'age': 18}
>>> info2 = {'name': 'jim', 'score': 95}
 | 
 
方法一 ,调用 dict 对象 update 方法:
| 1
2
3
 | >>> info1.update(info2)
>>> info1
{'name': 'jim', 'age': 18, 'score': 95}
 | 
 
方法二:
| 1
2
3
 | >>> info = {**info1, **info2}
>>> info
{'name': 'jim', 'age': 18, 'score': 95}
 | 
 
- Python 2 和 Python 3 中的 range(100) 的区别
Python 2 中的 range 函数返回一个列表,长度越大消耗内存越多:
| 1
2
 | >>> print(range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
 | 
 
Python 2 中的 xrange 函数与 range 类似,但返回 生成器 :
| 1
2
3
4
5
6
 | >>> r = xrange(10)
>>> ri = iter(r)
>>> next(ri)
0
>>> next(ri)
1
 | 
 
生成器内存消耗固定,与长度无关。因此,循环一般使用 xrange :
| 1
2
3
 | >>> for i in range(10000):
...     pass
...
 | 
 
由于生成器比较高效, Python 3 的 range 函数也选择返回生成器,可以认为与 Python 2 中的 xrange 等价:
| 1
2
3
4
5
6
 | >>> r = range(10)
>>> ri = iter(r)
>>> next(ri)
0
>>> next(ri)
1
 | 
 
当然了,Python 3 中也可以实现与 Python 2 中的 range 函数一样的效果:
| 1
2
 | >>> list(range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
 | 
 
- Python 列表如何去重
| 1
 | >>> l = [7, 3, 0, 3, 0, 8, 4, 9, 3, 8]
 | 
 
先将列表转换成 集合 ( set ),由于集合元素不重复,便实现去重:
| 1
2
 | >>> set(l)
{0, 3, 4, 7, 8, 9}
 | 
 
最后再将集合转化成列表即可:
| 1
2
 | >>> list(set(l))
[0, 3, 4, 7, 8, 9]
 | 
 
- 一句话解释什么样的语言能够用装饰器
函数可以 作为参数传递 、 可以作为返回值返回 的语言,都可以实现装饰器。
- Python 内建数据类型有哪些
- 布尔 , bool
- 整数 , int
- 浮点 , float
- 字符串 , str
- 字节序列 , bytes
- 元组 , tuple
- 列表 , list
- 字典 , dict
面试官可进一步延伸到对象 内部结构 ,相关操作 时间复杂度 等高级知识点。
- 请设计正则表达式,提取标签里的内容(中国),注意 class 名是不确定的:
| 1
 | <div class="tag">中国</div>
 | 
 
这个题目考察根据 html 标签结构,编写正则表达式。参考答案如下:
| 1
2
3
4
 | >>> import re
>>> text = '<div class="tag">中国</div>'
>>> re.findall(r'<div class="[^"]+">([^<]*)</div>', text)
['中国']
 | 
 
需要特别注意,类名中不能包含双引号,而标签中的文本不能包含小于号。
- 请编写正则表达式,提取以下网页中所有 a 标签的 URL
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
 | <html>
  <head>
    <title>小菜学编程</title>
  </head>
  <body>
    <ul>
      <li><a href="https://fasionchan.com/python/">小菜学Python</a></li>
      <li><a href="https://fasionchan.com/network/">小菜学网络</a></li>
      <li><a href="https://fasionchan.com/golang/">Go语言小册</a></li>
    </ul>
  </body>
</html>
 | 
 
这题目考察标准库 re 模块的基本用法,难度不高,根据文本特征写正则即可:
| 1
2
3
 | >>> import re
>>> re.findall(r'<a.*href="([^"]+)".*>', page)
['https://fasionchan.com/python/', 'https://fasionchan.com/network/', 'https://fasionchan.com/golang/']
 | 
 
注意到,参考答案中的正则表达式匹配 a 开标签,括号表示 内容提取 。 正则表达式在日常开发中应用场景很多,必须完全掌握。
- Python 中有几个名字空间,分别是什么
Python 总共有 4 个名字空间:
- 局部名字空间 ( locals )
- 闭包名字空间 ( closures )
- 全局名字空间 ( globals )
- 内建名字空间 ( builtin )
- 以 with 关键字打开并处理文件有什么好处
调用 open 函数打开文件,得到一个文件对象,里面包含打开的文件描述符或文件句柄。我们对文件对象进行读写操作后,必须关闭文件对象,不然就会造成进程句柄泄露。
| 1
2
3
4
 | f = open('data.txt', 'r')
data = f.read()
process(data)
f.close()
 | 
 
由于读写、处理数据时可能出错,一旦程序抛异常,关闭文件那行代码便没机会执行。因此,需要将可能抛异常的代码写成 try 结构,在 finally 中关闭文件:
| 1
2
3
4
5
6
7
8
 | f = open('data.txt', 'r')
try:
    data = f.read()
    process(data)
except:
    # procoss error
finally:
    f.close()
 | 
 
这样一来,不管 try 里面那两行代码会否抛异常,程序最终总会执行 f.close() ,杜绝了文件泄露的可能。 try 结构不够简洁,我们还可以通过 with 关键字实现:
| 1
2
3
 | with open('data.txt', 'r') as f:
    data = f.read()
    process(data)
 | 
 
Python 进入 with 代码块前,自动调用 f.enter ;离开 with 代码块后,自动调用 f.exit ;而 f.exit 则负责关闭自己。因此, with 同样可以保证打开的文件对象最终被关闭,但却比 try 结构简洁很多。
- 举例说明 Python 中断言( assert )的用法
| 1
2
3
4
5
6
7
8
 | >>> value = 8
>>>
>>> assert(value > 5)
>>>
>>> assert(value > 10)
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
AssertionError
 | 
 
- 请处理以下字符串,先将字符去重,再按 ASCII 排序,最后输出结果
| 1
 | >>> s = 'https://github.com/coding-fans/python-book'
 | 
 
首先,借助集合 set 完成字符去重:
| 1
2
 | >>> set(s)
{'k', 'o', 'f', 'y', 'c', 'b', 'i', 't', '/', '-', 'h', 'p', ':', 'd', 'u', '.', 'n', 'm', 'a', 'g', 's'}
 | 
 
然后,调用 sorted 函数对去重结果进行排序:
| 1
2
 | >>> sorted(set(s))
['-', '.', '/', ':', 'a', 'b', 'c', 'd', 'f', 'g', 'h', 'i', 'k', 'm', 'n', 'o', 'p', 's', 't', 'u', 'y']
 | 
 
最后,调用字符串对象 join 方法,将多个字符拼接成完整的字符串并输出:
| 1
2
 | >>> ''.join(sorted(set(s)))
'-./:abcdfghikmnopstuy'
 | 
 
- 请统计以下字符串中每个字符出现的次数
| 1
 | >>> s = 'https://python.fasionchan.com/'
 | 
 
借助 collections 模块中的 Counter ,只须一行代码即可完成统计:
| 1
2
3
4
 | >>> from collections import Counter
>>> counter = Counter(s)
>>> counter
Counter({'h': 3, 't': 3, '/': 3, 'o': 3, 'n': 3, 'p': 2, 's': 2, '.': 2, 'a': 2, 'c': 2, ':': 1, 'y': 1, 'f': 1, 'i': 1, 'm': 1})
 | 
 
此外,使用 collections 模块中的 defaultdict 也可以实现:
| 1
2
3
4
5
6
7
 | >>> from collections import defaultdict
>>> result = defaultdict(int)
>>> for c in s:
...     result[c] += 1
...
>>> result
defaultdict(<class 'int'>, {'h': 3, 't': 3, 'p': 2, 's': 2, ':': 1, '/': 3, 'y': 1, 'o': 3, 'n': 3, '.': 2, 'f': 1, 'a': 2, 'i': 1, 'c': 2, 'm': 1})
 | 
 
defaultdict 需要一个可调用对象作为参数,当你访问一个不存在的 key 时, defaultdict 自动调用该对象生成默认值并进行插入:
| 1
2
3
4
5
6
7
8
9
 | >>> d = defaultdict(int)
>>> d
defaultdict(<class 'int'>, {})
>>> d['no such key']
0
>>> d
defaultdict(<class 'int'>, {'no such key': 0})
>>> int()
0
 | 
 
当然了, 你用一个原始的 dict 字典,同样可以实现:
| 1
2
3
4
5
6
 | >>> d = {}
>>> for c in s:
...     d[c] = d.get(c, 0) + 1
...
>>> d
{'h': 3, 't': 3, 'p': 2, 's': 2, ':': 1, '/': 3, 'y': 1, 'o': 3, 'n': 3, '.': 2, 'f': 1, 'a': 2, 'i': 1, 'c': 2, 'm': 1}
 | 
 
像这样没有标准答案的题目,可以考察候选人的编程水平。这 3 种不同方法,虽然都能解决问题,水平却可分高下。如果你把代码写得如此憋足,就算完全正确,面试官内心肯定还是嫌弃的:
| 1
2
3
4
5
6
7
8
9
 | >>> d = {}
>>> for c in s:
...     if c in d:
...         d[c] += 1
...     else:
...         d[c] = 1
...
>>> d
{'h': 3, 't': 3, 'p': 2, 's': 2, ':': 1, '/': 3, 'y': 1, 'o': 3, 'n': 3, '.': 2, 'f': 1, 'a': 2, 'i': 1, 'c': 2, 'm': 1}
 | 
 
将代码写得更 Pythonic 些,也是一门学问,很重要的!
- 用 lambda 函数实现一个乘法操作函数,计算两个参数的乘积
| 1
2
3
 | >>> mul = lambda a, b: a * b
>>> mul(3, 5)
15
 | 
 
- 现有字符串 s ,每个单词中间是空格,请过滤英文和数字,最终输出 “张三 深圳”
| 1
 | s = 'not 404 found 张三 99 深圳'
 | 
 
首先,我们以空格为分隔符,将字符串分割成若干单词:
| 1
2
3
 | >>> words = s.split(' ')
>>> words
['not', '404', 'found', '张三', '99', '深圳']
 | 
 
然后,我们用正则把只包含英文和数字的单词提取出来,并保存到集合 words :
| 1
2
3
 | >>> alnums = set(re.findall('[0-9a-zA-Z]+', s))
>>> alnums
{'not', 'found', '404', '99'}
 | 
 
接着,我们将只包含单词和字母的单词过滤掉:
| 1
2
3
 | >>> result = [word for word in words if word not in alnums]
>>> result
['张三', '深圳']
 | 
 
最后,将剩下单词拼接起来:
| 1
2
 | >>> ' '.join(result)
'张三 深圳'
 | 
 
- 用 filter 函数求出以下数列中所有奇数并保存到新列表
| 1
 | numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
 | 
 
首先,我们定义一个函数 odd ,判定给定数值 n 是否为奇数:
| 1
2
 | def odd(n):
    return n % 2 == 1
 | 
 
然后,我们以 odd 为判定函数过滤数列 numbers ,并将结果保存到新列表:
| 1
2
 | >>> list(filter(odd, numbers))
[1, 3, 5, 7, 9]
 | 
 
- 用列表推导的方法求出以下数列中所有奇数并保存到新列表
| 1
 | numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
 | 
 
| 1
2
 | >>> [x for x in numbers if x % 2 == 1]
[1, 3, 5, 7, 9]
 | 
 
- re 模块中的 compile 函数有什么作用?
re.compile 函数将正则表达式编译成一个对象,以避免重复编译,提高执行效率。
- 下列 3 个变量分别是什么数据类型?
| 1
2
3
 | a = (1,)
b = (1)
c = ("1")
 | 
 
- a 是一个 元组 tuple ,元组里包含一个元素,即整数 1 ;
- b 是一个 整数 int , (1)等价于1;
- c 是一个 字符串 str , ("1")等价于"1";
| 1
2
3
4
5
6
 | >>> type(a)
<class 'tuple'>
>>> type(b)
<class 'int'>
>>> type(c)
<class 'str'>
 | 
 
- 
试谈一下 Python 解释器中的全局锁 GIL 
- 
**如何理解 func(*args, *kwargs) 中的 *args 和 *kwargs 
【小菜学Python】系列文章首发于公众号【小菜学编程】,敬请关注:
