Python面试题精选

笔者担任 Python 面试官多年,积累了很多面试题,特整理起来,希望对求职者有所帮助。此外,我们从网上摘录了很多经典面试题,配以详尽的讲解,举一反三。

我们将不定期更新,订阅可以关注我们的公众号: 小菜学编程

面试题

  1. 用一行代码实现整数 1 至 100 之和

网上的答案是通过 range 生成 1100 的整数,然后用 sum 求和:

1
2
3
>>> # 解法一
>>> sum(range(1, 101))
5050

这行代码确实很有美感,但你想过没有:如果是求 110000000000 之和呢?候选人必须认识到这是一个 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

面试官喜欢引申,候选人如果只是刷题记答案而不会分析,肯定是过不了关的。

  1. 如何在一个函数内部修改全局变量

在函数内部用 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. 请描述执行以下程序将输出什么内容?并试着解释其中的原因。
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]

这有点令人丈二和尚摸不着头脑,明明默认参数是一个空列表,为什么第 23 次调用后,列表都比预期中多一些数值呢?这一切得从 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 函数的行为就更符合我们的预期了。

  1. 列出 5 个 Python 标准库

这是一个开发性题目,面试官以考察候选人知识面以及学习深度为目的。必须结合自身情况,选择一些自己比较熟悉的标准库作答,面试官随时可能深入讨论。

保险一点,可以回答一些常用但很浅显的,例如:

  • re ,正则表达式处理
  • datetime ,日期时间处理
  • jsonJSON 数据处理
  • math , 数学计算
  • random , 随机数

想要获得加分,也可以回答一些高级的,例如:

面试官很有很能深入提问,切记:如果自己不是很熟悉,就不要班门弄斧了。

  1. 字典如何删除键

方法一 ,使用 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. 如何合并两个字典
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}
  1. 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 3range 函数也选择返回生成器,可以认为与 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]
  1. 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]
  1. 一句话解释什么样的语言能够用装饰器

函数可以 作为参数传递可以作为返回值返回 的语言,都可以实现装饰器。

  1. Python 内建数据类型有哪些
  • 布尔bool
  • 整数int
  • 浮点float
  • 字符串str
  • 字节序列bytes
  • 元组tuple
  • 列表list
  • 字典dict

面试官可进一步延伸到对象 内部结构 ,相关操作 时间复杂度 等高级知识点。

  1. 请设计正则表达式,提取标签里的内容(中国),注意 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)
['中国']

需要特别注意,类名中不能包含双引号,而标签中的文本不能包含小于号。

  1. 请编写正则表达式,提取以下网页中所有 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 开标签,括号表示 内容提取 。 正则表达式在日常开发中应用场景很多,必须完全掌握。

  1. Python 中有几个名字空间,分别是什么

Python 总共有 4 个名字空间:

  • 局部名字空间 ( locals )
  • 闭包名字空间 ( closures )
  • 全局名字空间 ( globals )
  • 内建名字空间 ( builtin )
  1. 以 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 结构简洁很多。

  1. 举例说明 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
  1. 请处理以下字符串,先将字符去重,再按 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. 请统计以下字符串中每个字符出现的次数
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 些,也是一门学问,很重要的!

  1. lambda 函数实现一个乘法操作函数,计算两个参数的乘积
1
2
3
>>> mul = lambda a, b: a * b
>>> mul(3, 5)
15
  1. 现有字符串 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)
'张三 深圳'
  1. 用 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. 用列表推导的方法求出以下数列中所有奇数并保存到新列表
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]
  1. re 模块中的 compile 函数有什么作用?

re.compile 函数将正则表达式编译成一个对象,以避免重复编译,提高执行效率。

  1. 下列 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'>
  1. 试谈一下 Python 解释器中的全局锁 GIL

  2. **如何理解 func(*args, *kwargs) 中的 *args 和 *kwargs

【小菜学Python】系列文章首发于公众号【小菜学编程】,敬请关注:

【小菜学Python】系列文章首发于公众号【小菜学编程】,敬请关注: