Python 面试题精选

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

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

面试题

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

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

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

    这行代码确实很有美感,但你想过没有:如果是求 110000000000 之和呢? 候选人必须认识到这是一个 O(N) 算法,真的适合所有场景吗?为什么不用等差数列前 N 项和公式进行计算呢?

    >>> # 解法二
    >>> n = 100
    >>> (n + 1) * n >> 1
    5050
    

    采用前 N 项和公式,求和时间复杂度是 O(1) ,孰优孰劣应该很明显了吧。 大家可以对比下当 N 很大时,这两种计算方式的表现:

    >>> n = 100000000
    >>> sum(range(1, n+1))
    5000000050000000
    >>> (n + 1) * n >> 1
    5000000050000000
    

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

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

    在函数内部用 global 关键字将变量申明为全局,然后再进行修改:

    >>> a = 1
    >>> def func():
    ...     global a
    ...     a = 2
    ...
    >>> print(a)
    1
    >>> func()
    >>> print(a)
    2
    

    面试官还可能引申到以下概念讨论,必须滚瓜烂熟:

    • 变量作用域 ( scope )

    • 局部名字空间 ( locals )

    • 闭包名字空间 ( globals )

    • 全局名字空间 ( enclosing )

    • 内建名字空间 ( builtin )

  3. 请描述执行以下程序将输出什么内容?并试着解释其中的原因。

    def add(n, l=[]):
        l.append(n)
        return l
    
    print(add(1))
    print(add(2))
    print(add(3))
    
    [1]
    [1, 2]
    [1, 2, 3]
    

    这有点令人丈二和尚摸不着头脑,明明默认参数是一个空列表,为什么第 23 次调用后,列表都比预期中多一些数值呢? 这一切得从 Python 函数的运行机制说起—— Python 函数默认参数是如何实现的?

    Python 函数在创建时便完成了默认参数的初始化,并将默认参数保存在函数对象的 __defaults__ 字段中:

    ../_images/34198eef7cccba1b3878e0612e74e069.svg

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

    ../_images/9a315ef9197c08acd30998ad52399e58.svg

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

    ../_images/28ec6edc3663497123eeae450ef729e9.svg

    当我们再次调用 add(2) 时,Python 虚拟机还是从函数对象中取出这个 list 对象作为 l 的默认参数。 因此,第二个 print 语句输出 [1, 2] 也就不奇怪了。

    总结起来,默认参数在函数对象创建时便完成了初始化,并保存在函数对象中。 当函数被调用时,Python 从函数对象中取出默认参数,而不是重新初始化。 因此,无论 add 函数被调用多少遍,默认参数总是同一个 list 对象。

    这与我的直观感觉相悖,因此尽量不要用可变对象作为默认参数,以避免一些潜在的 BUG 。 如果实在无法避免,则可以换一种更的严谨写法:

    def add(n, l=None):
        if l is None:
            l = []
        l.append(n)
        return l
    

    add 函数被调用时,如果参数 l 未指定,Python 自动使用默认值 None 。 函数内部对参数 l 进行判断,如果它的值是 None ,便将其设为一个新的空列表。 这样,add 函数的行为就更符合我们的预期了。

  4. 列出 5 个 Python 标准库

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

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

    • re ,正则表达式处理

    • datetime ,日期时间处理

    • jsonJSON 数据处理

    • math , 数学计算

    • random , 随机数

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

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

  5. 字典如何删除键

    • 方法一,使用 del 语句进行删除, del 关键字还可用于删除 变量属性

      >>> ages = {'tom': 18, 'jim': 20, 'lily': 19}
      >>> del ages['jim']
      >>> ages
      {'tom': 18, 'lily': 19}
      
    • 方法二,调用 pop 方法进行删除,这样可以拿到被删除键对应的值:

      >>> ages = {'tom': 18, 'jim': 20, 'lily': 19}
      >>> jims_age = ages.pop('jim')
      >>> jims_age
      20
      >>> ages
      {'tom': 18, 'lily': 19}
      
  6. 如何合并两个字典

    >>> info1 = {'name': 'jim', 'age': 18}
    >>> info2 = {'name': 'jim', 'score': 95}
    
    • 方法一,调用 dict 对象 update 方法:

      >>> info1.update(info2)
      >>> info1
      {'name': 'jim', 'age': 18, 'score': 95}
      
    • 方法二:

      >>> info = {**info1, **info2}
      >>> info
      {'name': 'jim', 'age': 18, 'score': 95}
      
  7. Python 2 和 Python 3 中的 range(100) 的区别

    Python 2 中的 range 函数返回一个列表,长度越大消耗内存越多:

    >>> print(range(10))
    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    

    Python 2 中的 xrange 函数与 range 类似,但返回 生成器

    >>> r = xrange(10)
    >>> ri = iter(r)
    >>> next(ri)
    0
    >>> next(ri)
    1
    

    生成器内存消耗固定,与长度无关。因此,循环一般使用 xrange

    >>> for i in range(10000):
    ...     pass
    ...
    

    由于生成器比较高效, Python 3range 函数也选择返回生成器,可以认为与 Python 2 中的 xrange 等价:

    >>> r = range(10)
    >>> ri = iter(r)
    >>> next(ri)
    0
    >>> next(ri)
    1
    

    当然了,Python 3 中也可以实现与 Python 2 中的 range 函数一样的效果:

    >>> list(range(10))
    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    
  8. Python 列表如何去重

    >>> l = [7, 3, 0, 3, 0, 8, 4, 9, 3, 8]
    

    先将列表转换成 集合 ( set ),由于集合元素不重复,便实现去重:

    >>> set(l)
    {0, 3, 4, 7, 8, 9}
    

    最后再将集合转化成列表即可:

    >>> list(set(l))
    [0, 3, 4, 7, 8, 9]
    
  9. 一句话解释什么样的语言能够用装饰器

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

  10. Python 内建数据类型有哪些

    • 布尔bool

    • 整数int

    • 浮点float

    • 字符串str

    • 字节序列bytes

    • 元组tuple

    • 列表list

    • 字典dict

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

  11. 请设计正则表达式,提取标签里的内容(中国),注意 class 名是不确定的:

    <div class="tag">中国</div>
    

    这个题目考察根据 html 标签结构,编写正则表达式。参考答案如下:

    >>> import re
    >>> text = '<div class="tag">中国</div>'
    >>> re.findall(r'<div class="[^"]+">([^<]*)</div>', text)
    ['中国']
    

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

  12. 请编写正则表达式,提取以下网页中所有 a 标签的 URL

    <html>
      <head>
        <title>小菜学编程</title>
      </head>
      <body>
        <ul>
          <li><a href="https://python.fasionchan.com">Python语言小册</a></li>
          <li><a href="https://linux.fasionchan.com">学习Linux</a></li>
          <li><a href="https://network.fasionchan.com">Linux网络编程</a></li>
          <li><a href="https://golang.fasionchan.com">Go语言小册</a></li>
          <li><a href="https://nodejs.fasionchan.com">Node.js小册</a></li>
        </ul>
      </body>
    </html>
    

    这题目考察标准库 re 模块的基本用法,难度不高,根据文本特征写正则即可:

    >>> import re
    >>> re.findall(r'<a.*href="([^"]+)".*>', page)
    ['https://python.fasionchan.com', 'https://linux.fasionchan.com', 'https://network.fasionchan.com', 'https://golang.fasionchan.com', 'https://nodejs.fasionchan.com']
    

    注意到,参考答案中的正则表达式匹配 a 开标签,括号表示 内容提取 。 正则表达式在日常开发中应用场景很多,必须完全掌握。

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

    Python 总共有 4 个名字空间:

    • 局部名字空间 ( locals )

    • 闭包名字空间 ( closures )

    • 全局名字空间 ( globals )

    • 内建名字空间 ( builtin )

  14. 以 with 关键字打开并处理文件有什么好处

    调用 open 函数打开文件,得到一个文件对象,里面包含打开的文件描述符或文件句柄。 我们对文件对象进行读写操作后,必须关闭文件对象,不然就会造成进程句柄泄露。

    f = open('data.txt', 'r')
    data = f.read()
    process(data)
    f.close()
    

    由于读写、处理数据时可能出错,一旦程序抛异常,关闭文件那行代码便没机会执行。 因此,需要将可能抛异常的代码写成 try 结构,在 finally 中关闭文件:

    f = open('data.txt', 'r')
    try:
        data = f.read()
        process(data)
    except:
        # procoss error
    finally:
        f.close()
    

    这样一来,不管 try 里面那两行代码会否抛异常,程序最终总会执行 f.close() ,杜绝了文件泄露的可能。 try 结构不够简洁,我们还可以通过 with 关键字实现:

    with open('data.txt', 'r') as f:
        data = f.read()
        process(data)
    

    Python 进入 with 代码块前,自动调用 f.__enter__ ;离开 with 代码块后,自动调用 f.__exit__ ;而 f.__exit__ 则负责关闭自己。 因此, with 同样可以保证打开的文件对象最终被关闭,但却比 try 结构简洁很多。

  15. 举例说明 Python 中断言( assert )的用法

    >>> value = 8
    >>>
    >>> assert(value > 5)
    >>>
    >>> assert(value > 10)
    Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
    AssertionError
    
  16. 请处理以下字符串,先将字符去重,再按 ASCII 排序,最后输出结果

    >>> s = 'https://github.com/coding-fans/python-book'
    

    首先,借助集合 set 完成字符去重:

    >>> set(s)
    {'k', 'o', 'f', 'y', 'c', 'b', 'i', 't', '/', '-', 'h', 'p', ':', 'd', 'u', '.', 'n', 'm', 'a', 'g', 's'}
    

    然后,调用 sorted 函数对去重结果进行排序:

    >>> sorted(set(s))
    ['-', '.', '/', ':', 'a', 'b', 'c', 'd', 'f', 'g', 'h', 'i', 'k', 'm', 'n', 'o', 'p', 's', 't', 'u', 'y']
    

    最后,调用字符串对象 join 方法,将多个字符拼接成完整的字符串并输出:

    >>> ''.join(sorted(set(s)))
    '-./:abcdfghikmnopstuy'
    
  17. 请统计以下字符串中每个字符出现的次数

    >>> s = 'https://python.fasionchan.com/'
    

    借助 collections 模块中的 Counter ,只须一行代码即可完成统计:

    >>> 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 也可以实现:

    >>> 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 自动调用该对象生成默认值并进行插入:

    >>> d = defaultdict(int)
    >>> d
    defaultdict(<class 'int'>, {})
    >>> d['no such key']
    0
    >>> d
    defaultdict(<class 'int'>, {'no such key': 0})
    >>> int()
    0
    

    当然了, 你用一个原始的 dict 字典,同样可以实现:

    >>> 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 种不同方法,虽然都能解决问题,水平却可分高下。 如果你把代码写得如此憋足,就算完全正确,面试官内心肯定还是嫌弃的:

    >>> 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 些,也是一门学问,很重要的!

  18. lambda 函数实现一个乘法操作函数,计算两个参数的乘积

    >>> mul = lambda a, b: a * b
    >>> mul(3, 5)
    15
    
  19. 现有字符串 s ,每个单词中间是空格,请过滤英文和数字,最终输出 “张三 深圳”

    s = 'not 404 found 张三 99 深圳'
    

    首先,我们以空格为分隔符,将字符串分割成若干单词:

    >>> words = s.split(' ')
    >>> words
    ['not', '404', 'found', '张三', '99', '深圳']
    

    然后,我们用正则把只包含英文和数字的单词提取出来,并保存到集合 words

    >>> alnums = set(re.findall('[0-9a-zA-Z]+', s))
    >>> alnums
    {'not', 'found', '404', '99'}
    

    接着,我们将只包含单词和字母的单词过滤掉:

    >>> result = [word for word in words if word not in alnums]
    >>> result
    ['张三', '深圳']
    

    最后,将剩下单词拼接起来:

    >>> ' '.join(result)
    '张三 深圳'
    
  20. 用 filter 函数求出以下数列中所有奇数并保存到新列表

    numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    

    首先,我们定义一个函数 odd ,判定给定数值 n 是否为奇数:

    def odd(n):
        return n % 2 == 1
    

    然后,我们以 odd 为判定函数过滤数列 numbers ,并将结果保存到新列表:

    >>> list(filter(odd, numbers))
    [1, 3, 5, 7, 9]
    
  21. 用列表推导的方法求出以下数列中所有奇数并保存到新列表

    numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    
    >>> [x for x in numbers if x % 2 == 1]
    [1, 3, 5, 7, 9]
    
  22. re 模块中的 compile 函数有什么作用?

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

  23. 下列 3 个变量分别是什么数据类型?

    a = (1,)
    b = (1)
    c = ("1")
    
    • a 是一个 元组 tuple ,元组里包含一个元素,即整数 1 ;

    • b 是一个 整数 int(1) 等价于 1

    • c 是一个 字符串 str("1") 等价于 "1"

    >>> type(a)
    <class 'tuple'>
    >>> type(b)
    <class 'int'>
    >>> type(c)
    <class 'str'>
    
  24. 试谈一下 Python 解释器中的全局锁 GIL

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

附录

订阅更新,获取更多学习资料,请关注我们的 微信公众号

微信搜索:小菜学编程