前段时间去面试了一些招聘 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__
方法,那么他就是一个迭代器。
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))
这种推导式形式创建出来的迭代器。
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 的配置文件中加入以下代码:
proxy: {
'/api': {
target: 'http://127.0.0.1:8001',
changeOrigin: true,
pathRewrite: {
'^/api': '/api',//重写,
}
},
- 反向代理实现,Nignx 作为反向代理来解决跨域问题,生产环境通常这样做,比如典型的 nginx 配置:
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