近期遇到的关于 Python 的面试题

2021-08-19 16:08:56 浏览数 (1)

前段时间去面试了一些招聘 Python 软件开发的公司,看看现在的公司都关注 Python 的那些方面。因为疫情原因,现在面试都是电话或者视频面试,也可以约晚上,不用请假,也不影响白天的工作,面试的成本非常低,收益却很高,面的好的话就意味着涨薪水,面的不好就说明自己掌握的还不够,因此面试是一个很好的学习交流形式,推荐大家每 2 年都去面试一波。

今天来聊一聊我近期遇到的关于 Python 的面试题。

1、说说你对 Python 多线程的理解。

这道题是开放题目,就是考察候选人对 Python 知识了解的广度。

总之,你要提到线程的定义,线程的状态转换,CPython 的 GIL 对多线程的影响,线程同步的几种方式,线程池,多线程的使用场景,甚至你还可以扯一些协程的区别。

这个问题,可以自己思考一下答案,也可以参考文章:Python多线程

2、说说对 python 协程的理解。

这个题目我认为是考察对事件循环的理解。

首先可以聊一聊为什么会有协程,我们知道,在处理 I/O 操作时,使用多线程与普通的单线程相比,效率得到了极大的提高。但是也会遇到问题,比如,多线程运行过程容易被打断,因此有可能出现 race condition 的情况;再如,线程切换本身存在一定的损耗,线程数不能无限增加,因此,如果你的 I/O 操作非常繁重,多线程很有可能满足不了高效率、高质量的需求。为了解决这些问题,协程应运而生。

协程的实现原理,就是事件循环,事件循环 “是一种等待程序分配事件或消息的编程架构”。基本上来说事件循环就是,“当A发生时,执行B”。

简单说,就是在程序中设置两个线程:一个负责程序本身的运行,称为"主线程";另一个负责主线程与其他进程(主要是各种I/O操作)的通信,被称为"Event Loop线程"(可以译为"消息线程")。

每当遇到 I/O 的时候,主线程就让 Event Loop 线程去通知相应的 I/O 程序,然后接着往后运行,所以不存在等待时间。等到 I/O程序完成操作,Event Loop 线程再把结果返回主线程。主线程就调用事先设定的回调函数,完成整个任务。

协程虽然单线程,却实现了多线程并发的效果,也不涉及线程的切换,因此节省资源,更为高效。

从使用体验上来说,多线程编码简单,线程的切换由操作系统控制,而协程编码复杂,代码执行时机的切换由程序员自己控制。

关于线程和协程,前文并发使用多线程还是协程有介绍。

3、Python 中的迭代器和生成器有什么区别,都说生成器是一种特殊的迭代器,请问特殊在哪里?

首先明确迭代器的定义,Python 中一切皆对象,只要一个对象有实现了 __iter__ 方法和 __next__ 方法,那么他就是一个迭代器。

代码语言:javascript复制
class MyListIterator(object):  # 定义迭代器类
    def __init__(self, data):
        self.data = data  # 上边界
        self.now = 0  # 当前迭代值,初始为0

    def __iter__(self):
        return self  # 返回该对象的迭代器类的实例;因为自己就是迭代器,所以返回 self

    def __next__(self):  # 迭代器类必须实现的方法
        while self.now < self.data:
            self.now  = 1
            return self.now - 1  # 返回当前迭代值
        raise StopIteration  # 超出上边界,抛出异常

from collections import Iterator

print(isinstance(MyListIterator(10), Iterator))
# True

迭代器可以通过 next() 函数来遍历,for in 语句将这个过程隐式化,比如上面的对象:

代码语言:javascript复制
mylist = MyListIterator(3)
print(next(mylist)) # 0
print(next(mylist)) # 1
print(next(mylist)) # 2
print(next(mylist)) # StopIteration

用 for 循环也可以

代码语言:javascript复制
mylist = MyListIterator(3)
for i in mylist:
    print(i)

而生成器也是迭代器,只不过是使用 yield 关键字或者 (i for i in range(10)) 这种推导式形式创建出来的迭代器。

代码语言:javascript复制
def fun(n):
    i = 0
    while i < n:
        yield i
        i  = 1

等价于

代码语言:javascript复制
(i for i in range(n))

生成器的特殊之处是生成器比较懒,不会一下次将数据全部加载到内存,而且只能遍历一次。下面的两行代码一个是迭代器,一个是生成器(也是迭代器),可以看出他们的性能差异了吧:

代码语言:javascript复制
In [14]: timeit [i for i in range(1000000)]
49.2 ms ± 723 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [15]: timeit (i for i in range(1000000))
505 ns ± 3.21 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

关于迭代器和生成器,可以参考前文Python迭代器还可以这样玩

4、你知道 GIL 吗,说说你的理解。

可以把这个讲给面试官听Python有可能删除 GIL 吗?

5、Django 就如何防止跨站请求伪造的?

跨站请求伪造的英文 Cross-site request forgery (CSRF),只要你用过 Django,对这个 CSRF 一定不会陌生,因为稍不注意,Django 就会提示你 403 没有权限访问。

简单来说,Django 会生成一个随机的字符串(csrftoken),放在表单的隐藏字段里,然后在提交表单时会将这个 csrftoken 一起提交到后端,后端的中间件django.middleware.csrf.CsrfViewMiddleware会去校验这个字符串跟之前的是否一致,不一致则认为是跨站请求伪造,拒绝访问。

官方文档 CSRF[1]

6、前后端分离的项目,如何解决跨域问题的?

CORS(Cross-origin resource sharing,跨域资源共享)是一个 W3C 标准,定义了在必须访问跨域资源时,浏览器与服务器应该如何沟通。它的核心思想,使用自定义的 HTTP 头部信息让浏览器和后端进行沟通,来决定是否允许跨域请求。

其实有三种解决方案:

  • 后端解决,后面可以配置跨域站点的白名单,或者干脆允许跨域请求。比如 Django 可通过第三方的跨域库 django-cors-headers 添加支持,常用在开发环境。
  • 前端解决,前端可以使用代理实现,常用在开发环境,以 Vue 为例,在 Vue 的配置文件中加入以下代码:
代码语言:javascript复制
proxy: {
    '/api': {
        target: 'http://127.0.0.1:8001',
        changeOrigin: true,
        pathRewrite: {
            '^/api': '/api',//重写,
        }
    },
  • 反向代理实现,Nignx 作为反向代理来解决跨域问题,生产环境通常这样做,比如典型的 nginx 配置:
代码语言:javascript复制
location /static {
    autoindex off;
    alias /Users/aaronbrant/gitee/KeJiTuan/frontEnd/dist/static;
}

location ~/(api|admin) {
    set $Real $proxy_add_x_forwarded_for;
    if ( $Real ~ (d ).(d ).(d ).(d ),(.*) ){
        set $Real $1.$2.$3.$4;
    }


    uwsgi_pass  127.0.0.1:8000;
    uwsgi_param X-Real-IP $Real;
    uwsgi_param X-Forwarded-For $proxy_add_x_forwarded_for;
    uwsgi_param X-Forwarded-Proto $http_x_forwarded_proto;
    uwsgi_param UWSGI_SCRIPT rearEnd.wsgi;
    uwsgi_param UWSGI_CHDIR /Users/aaronbrant/gitee/KeJiTuan/rearEnd;
    include uwsgi_params;
}

具体实施的话,可以看教你玩转Vue和Django的前后端分离

7、Django ORM 的 get 和 filter 方法有什么区别?

这个就很简单了,get 只获取一个对象,对象不存在时抛出异常,filter 获取一组对象,对象不存在时,返回空,不抛出异常。

以下是手撕代码题目:

所谓手撕代码,打开编辑器,开始写代码,没有限制,自己命名函数,自己处理输入输出,如果自己不写一些测试用例,很有可能出现考虑不周的情况。

8、请用两种方式实现单例。

这个其实考察懒汉和饿汉,所谓的懒汉就是用的时候在创建对象,饿汉就是不管用不用先创建了再说,这里分别给出:

方法一,懒汉:

代码语言:javascript复制
# 懒汉式
class Singleton(object):
    __instance = None
    def __init__(self):
        if not self.__instance:
            print('调用__init__, 实例未创建')
        else:
            print('调用__init__,实例已经创建过了:', __instance)

    @classmethod
    def get_instance(cls):
        # 调用get_instance类方法的时候才会生成Singleton实例
        if not cls.__instance:
            cls.__instance = Singleton()
        return cls.__instance

只有在使用的时候才创建对象,因此运行的速度稍快,但线程不安全,多个线程同时访问到 if not cls.__instance: 就有可能创建出多个不同的对象。

方法二,饿汉:

一开始就创建好 Singleton 实例

代码语言:javascript复制
# 饿汉式
class Singleton(object):
    _instance = None
    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            cls._instance = super(Singleton, cls).__new__(cls, *args, **kwargs)
        return cls._instance

线程安全,但是由于要先创建对象再使用,当对象比较大时,比较耗时间。

9、算法题

这类题目基本就是 LeetCode 上的原题,看来面试官也是懒得创新,直接拿原题考一考得了,不过有水平的面试官会拿一道简单题目开始,然后逐渐增加难度,这样更能考察候选人的真实水平。

10、实现 LRU 缓存淘汰算法:

这是老生常谈了,这里直接附上两种实现的代码:

LRU 缓存淘汰算法-双链表 hash 表[2]

当然还可以使用 Python 的有序字典:

LRU 缓存淘汰算法-Python 有序字典[3]

最后的话

技术面试,还是实力最重要,其他的回答技巧基本不起什么作用。如果本文对你有帮助,还请点个在看,感谢支持。至于面试的结果,且听下回分解。

留言讨论

参考资料

[1]

官方文档 CSRF: https://docs.djangoproject.com/zh-hans/3.2/ref/csrf/

[2]

LRU 缓存淘汰算法-双链表 hash 表: https://github.com/somenzz/geekbang/blob/master/algorthms/lru_use_link_table.py

[3]

LRU 缓存淘汰算法-Python 有序字典: https://github.com/somenzz/geekbang/blob/master/algorthms/lru_use_ordered_dict.py

0 人点赞