近期遇到的關(guān)于 Python 的面試題
前段時間去面試了一些招聘 Python 軟件開發(fā)的公司,看看現(xiàn)在的公司都關(guān)注 Python 的那些方面。因為疫情原因,現(xiàn)在面試都是電話或者視頻面試,也可以約晚上,不用請假,也不影響白天的工作,面試的成本非常低,收益卻很高,面的好的話就意味著漲薪水,面的不好就說明自己掌握的還不夠,因此面試是一個很好的學(xué)習(xí)交流形式,推薦大家每 2 年都去面試一波。
今天來聊一聊我近期遇到的關(guān)于 Python 的面試題。
1、說說你對 Python 多線程的理解。
這道題是開放題目,就是考察候選人對 Python 知識了解的廣度。
總之,你要提到線程的定義,線程的狀態(tài)轉(zhuǎn)換,CPython 的 GIL 對多線程的影響,線程同步的幾種方式,線程池,多線程的使用場景,甚至你還可以扯一些協(xié)程的區(qū)別。
這個問題,可以自己思考一下答案,也可以參考文章:Python多線程。
2、說說對 python 協(xié)程的理解。
這個題目我認(rèn)為是考察對事件循環(huán)的理解。
首先可以聊一聊為什么會有協(xié)程,我們知道,在處理 I/O 操作時,使用多線程與普通的單線程相比,效率得到了極大的提高。但是也會遇到問題,比如,多線程運(yùn)行過程容易被打斷,因此有可能出現(xiàn) race condition 的情況;再如,線程切換本身存在一定的損耗,線程數(shù)不能無限增加,因此,如果你的 I/O 操作非常繁重,多線程很有可能滿足不了高效率、高質(zhì)量的需求。為了解決這些問題,協(xié)程應(yīng)運(yùn)而生。
協(xié)程的實現(xiàn)原理,就是事件循環(huán),事件循環(huán) “是一種等待程序分配事件或消息的編程架構(gòu)”。基本上來說事件循環(huán)就是,“當(dāng)A發(fā)生時,執(zhí)行B”。
簡單說,就是在程序中設(shè)置兩個線程:一個負(fù)責(zé)程序本身的運(yùn)行,稱為"主線程";另一個負(fù)責(zé)主線程與其他進(jìn)程(主要是各種I/O操作)的通信,被稱為"Event Loop線程"(可以譯為"消息線程")。
每當(dāng)遇到 I/O 的時候,主線程就讓 Event Loop 線程去通知相應(yīng)的 I/O 程序,然后接著往后運(yùn)行,所以不存在等待時間。等到 I/O程序完成操作,Event Loop 線程再把結(jié)果返回主線程。主線程就調(diào)用事先設(shè)定的回調(diào)函數(shù),完成整個任務(wù)。
協(xié)程雖然單線程,卻實現(xiàn)了多線程并發(fā)的效果,也不涉及線程的切換,因此節(jié)省資源,更為高效。
從使用體驗上來說,多線程編碼簡單,線程的切換由操作系統(tǒng)控制,而協(xié)程編碼復(fù)雜,代碼執(zhí)行時機(jī)的切換由程序員自己控制。
關(guān)于線程和協(xié)程,前文并發(fā)使用多線程還是協(xié)程有介紹。
3、Python 中的迭代器和生成器有什么區(qū)別,都說生成器是一種特殊的迭代器,請問特殊在哪里?
首先明確迭代器的定義,Python 中一切皆對象,只要一個對象有實現(xiàn)了 __iter__ 方法和 __next__ 方法,那么他就是一個迭代器。
class MyListIterator(object): # 定義迭代器類
def __init__(self, data):
self.data = data # 上邊界
self.now = 0 # 當(dāng)前迭代值,初始為0
def __iter__(self):
return self # 返回該對象的迭代器類的實例;因為自己就是迭代器,所以返回 self
def __next__(self): # 迭代器類必須實現(xiàn)的方法
while self.now < self.data:
self.now += 1
return self.now - 1 # 返回當(dāng)前迭代值
raise StopIteration # 超出上邊界,拋出異常
from collections import Iterator
print(isinstance(MyListIterator(10), Iterator))
# True
迭代器可以通過 next() 函數(shù)來遍歷,for in 語句將這個過程隱式化,比如上面的對象:
mylist = MyListIterator(3)
print(next(mylist)) # 0
print(next(mylist)) # 1
print(next(mylist)) # 2
print(next(mylist)) # StopIteration
用 for 循環(huán)也可以
mylist = MyListIterator(3)
for i in mylist:
print(i)
而生成器也是迭代器,只不過是使用 yield 關(guān)鍵字或者 (i for i in range(10)) 這種推導(dǎo)式形式創(chuàng)建出來的迭代器。
def fun(n):
i = 0
while i < n:
yield i
i += 1
等價于
(i for i in range(n))
生成器的特殊之處是生成器比較懶,不會一下次將數(shù)據(jù)全部加載到內(nèi)存,而且只能遍歷一次。下面的兩行代碼一個是迭代器,一個是生成器(也是迭代器),可以看出他們的性能差異了吧:
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)
關(guān)于迭代器和生成器,可以參考前文Python迭代器還可以這樣玩
4、你知道 GIL 嗎,說說你的理解。
可以把這個講給面試官聽Python有可能刪除 GIL 嗎?
5、Django 就如何防止跨站請求偽造的?
跨站請求偽造的英文 Cross-site request forgery (CSRF),只要你用過 Django,對這個 CSRF 一定不會陌生,因為稍不注意,Django 就會提示你 403 沒有權(quán)限訪問。
簡單來說,Django 會生成一個隨機(jī)的字符串(csrftoken),放在表單的隱藏字段里,然后在提交表單時會將這個 csrftoken 一起提交到后端,后端的中間件django.middleware.csrf.CsrfViewMiddleware會去校驗這個字符串跟之前的是否一致,不一致則認(rèn)為是跨站請求偽造,拒絕訪問。
官方文檔 CSRF[1]
6、前后端分離的項目,如何解決跨域問題的?
CORS(Cross-origin resource sharing,跨域資源共享)是一個 W3C 標(biāo)準(zhǔn),定義了在必須訪問跨域資源時,瀏覽器與服務(wù)器應(yīng)該如何溝通。它的核心思想,使用自定義的 HTTP 頭部信息讓瀏覽器和后端進(jìn)行溝通,來決定是否允許跨域請求。
其實有三種解決方案:
后端解決,后面可以配置跨域站點的白名單,或者干脆允許跨域請求。比如 Django 可通過第三方的跨域庫 django-cors-headers 添加支持,常用在開發(fā)環(huán)境。
前端解決,前端可以使用代理實現(xiàn),常用在開發(fā)環(huán)境,以 Vue 為例,在 Vue 的配置文件中加入以下代碼:
proxy: {
'/api': {
target: 'http://127.0.0.1:8001',
changeOrigin: true,
pathRewrite: {
'^/api': '/api',//重寫,
}
},
反向代理實現(xiàn),Nignx 作為反向代理來解決跨域問題,生產(chǎn)環(huán)境通常這樣做,比如典型的 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;
}
具體實施的話,可以看教你玩轉(zhuǎn)Vue和Django的前后端分離
7、Django ORM 的 get 和 filter 方法有什么區(qū)別?
這個就很簡單了,get 只獲取一個對象,對象不存在時拋出異常,filter 獲取一組對象,對象不存在時,返回空,不拋出異常。
以下是手撕代碼題目:
所謂手撕代碼,打開編輯器,開始寫代碼,沒有限制,自己命名函數(shù),自己處理輸入輸出,如果自己不寫一些測試用例,很有可能出現(xiàn)考慮不周的情況。
8、請用兩種方式實現(xiàn)單例。
這個其實考察懶漢和餓漢,所謂的懶漢就是用的時候在創(chuàng)建對象,餓漢就是不管用不用先創(chuàng)建了再說,這里分別給出:
方法一,懶漢:
# 懶漢式
class Singleton(object):
__instance = None
def __init__(self):
if not self.__instance:
print('調(diào)用__init__, 實例未創(chuàng)建')
else:
print('調(diào)用__init__,實例已經(jīng)創(chuàng)建過了:', __instance)
@classmethod
def get_instance(cls):
# 調(diào)用get_instance類方法的時候才會生成Singleton實例
if not cls.__instance:
cls.__instance = Singleton()
return cls.__instance
只有在使用的時候才創(chuàng)建對象,因此運(yùn)行的速度稍快,但線程不安全,多個線程同時訪問到 if not cls.__instance: 就有可能創(chuàng)建出多個不同的對象。
方法二,餓漢:
一開始就創(chuàng)建好 Singleton 實例
# 餓漢式
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
線程安全,但是由于要先創(chuàng)建對象再使用,當(dāng)對象比較大時,比較耗時間。
9、算法題
這類題目基本就是 LeetCode 上的原題,看來面試官也是懶得創(chuàng)新,直接拿原題考一考得了,不過有水平的面試官會拿一道簡單題目開始,然后逐漸增加難度,這樣更能考察候選人的真實水平。
10、實現(xiàn) LRU 緩存淘汰算法:
這是老生常談了,這里直接附上兩種實現(xiàn)的代碼:
LRU 緩存淘汰算法-雙鏈表+hash 表[2]
當(dāng)然還可以使用 Python 的有序字典:
LRU 緩存淘汰算法-Python 有序字典[3]
最后的話
技術(shù)面試,還是實力最重要,其他的回答技巧基本不起什么作用。如果本文對你有幫助,還請點個在看,感謝支持。至于面試的結(jié)果,且聽下回分解。
參考資料
官方文檔 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
