Python-常見面試題

什麼是Python

  • Python是一種解釋型語言,也就是說,它和C語言以及C的衍生語言不通,Python代碼在運行之前不需要編譯
  • Python是一種動態類型語言,指的是,你在聲明變量時不需要指定變量的類型
  • Python讓困難的事變的容易,因此程序員可以專注於算法和數據結構的設計,而不用處理底層的細節
  • Python用途非常廣泛–網絡應用,自動化,科學建模,大數據應用等等,它也常被用作“膠水語言”,用於幫助其他語言和組件改善運行狀況

Python支持的數據類型

數字、字符串、元組、字典、列表

下劃線的作用

  • _xxx:表示的是protected類型,即只允許其本身和子類進行訪問
  • __xxx:表示的是private類型
  • __xxx__:表示的特列方式,如__init__

如何生成一個不可變集合

使用frozenset函數,將一個列表變成一個不可變的集合,如下:

s = frozenset([1, 2, 3])

is與==

is對比地址,==是對比值

多線程與多進程

對比維度 多進程 多線程
數據共享、同步 數據共享複雜,需要用到IPC;數據是分開的、同步簡單 因爲共享進程數據,所以共享簡單,但也因爲這個導致同步複雜
內存、CPU 佔用內存多,切換複雜,CPU利用率低 佔用內存小,切換簡單,CPU利用率高
創建、銷燬、切換 創建銷燬、切換複雜,速度很慢 創建銷燬、切換簡單,速度很快
編程、調試 編程簡單、調試簡單 編程複雜、調試複雜
可靠性 進程之間不會相互影響 一個線程掛掉可能將會導致進程崩潰
分佈式 適用於多核、多機分佈式,如果一臺機器不夠,擴展到其他多臺機器比較簡單 適用於多核分佈式
通信方式 管道、命名管道、信號量、信號、消息隊列、共享內存和套接字 鎖機制、信號量機制和信號機制

什麼是協程

從編程的角度來看,協程的思想本質上就是控制流的主動讓出和恢復機制!一個程序可以包含多個協程,可以對於一個進程包含多個線程,因而下面來我們來比較線程和協程。我們知道多個線程相對獨立,有自己的上下文,切換受到系統控制;而協程也相對獨立,有自己的上下文,但是其切換由自己控制,由當前協程切換到其他協程由當前協程控制!協程有如下幾個特點:

  • 協同,因爲是由程序員寫的調度策略,其通過協作而不是通過搶佔來進行切換
  • 在用戶態完成創建、切換和銷燬
  • generator經常用來實現協程

協程的優勢:

  • 最大的優勢就是協程極高的執行效率。因爲協程之間的切換是由程序自身控制,因此,沒有線程切換的開銷,和多線程相比,線程數量越多,協程的性能優勢越明顯。
  • 第二大優勢就是不需要多線程的鎖機制,因爲只有一個線程,也不存在同時寫變量衝突,在協程中控制共享資源不加鎖,只需要判斷狀態就好了,所以執行效率比多線程高很多!

協程的缺點:

  • 無法利用多核資源:協程的本質是單線程,它不能同時將單個CPU的多個核用上,協程需要和進程配合才能運行在多CPU上。當然我們日常所編寫的絕大部分應用都沒有這個必要,除非是CPU密集型應用
  • 進行阻塞(Blocking)操作時(如IO)會阻塞掉整個程序

Restful Api

Restful Api有哪些設計規則和規範呢?

  • 資源。資源就是網絡上的一個實體,一短文本,一張圖片或一首歌曲。資源總是需要一個載體來反應它的內容。文本可以是TXT,也可以用HTML或者XML、圖片可以是JPG或者PNG格式。JSON是目前最常用的資源表現形式
  • 統一接口。Restful風格的數據元操(create、read、update、delete)分別對應HTTP方法:GET用來獲取資源,POST用來創建資源,PUT用來修改資源,DELETE用來刪除資源。這樣就統一了數據操作的接口
  • URI。可以用一個URI指向資源,即每個URI都對應一個特定的資源。要獲取這個資源只需訪問它對應的URI即可,因此URI就成了每一個資源的地址或標識符。
  • 無狀態。所謂無狀態就是所有的資源都可以使用URI來定位,而且這個定位與其他資源無關,也不會因爲其他資源的變化而變化。

三次握手

  • 客戶端發送位碼爲SYN=1,隨機產生seq數據包到服務器
  • 服務器接收到SYN=1,知道客戶端要求建立聯機。向客戶端發送ack(ack=客戶端的seq+1),SYN=1,ACK=1,以及隨機產生的seq數據包
  • 客戶端接收到ack,檢測是否正確。正確情況下,客戶端會發送一個ack(ack=服務端的seq+1),ACK=1。服務端接收並驗證正確後建立連接

四次揮手

  • 客戶端發送連接釋放報文,並且停止發送數據。釋放數據報文首部,FIN=1,seq=u(前面傳遞過來的數據的最後一個字節的序號加1),此時客戶端進入FIN_WAIT_1狀態
  • 服務端接收到連接釋放報文,發出確認報文,ACK=1,sck=u+1,並且帶上自己的序號seq=v,此時服務端進入CLOSE_WAIT狀態。客戶端接收到服務端發出的確認請求之後,客戶端進入FIN_WAIT_2狀態
  • 服務端將最後的數據發送完畢後,就向客戶端發送連接釋放報文,FIN=1,ack=u+1,由於處理半關閉狀態,服務端可能還會發出seq=w,服務端進入LAST_ACK狀態
  • 客戶端接收到服務端的連接釋放報文之後,發出確認報文,ACK=1,ack=w+1,seq=u+1,此時客戶端進入TIME-WAIT狀態。服務端接收到客戶端發出的確認,立即進入CLOSED狀態。撤銷連接,結束此次連接!

*args與**kwargs

如果我們不知道要往函數中傳入多少個參數時,或者我們想往函數中以列表或者元組的形式傳參時,那麼就可以使用*args
如果我們不知道要往函數中傳入多少個關鍵詞參數時,或者我們想往函數中以字典的形式傳參時,那麼就可以使用**kwargs
*args與**kwargs是約定成俗的寫法

xrange與range的區別

在Python2中,xrange會生成一個迭代器,range會生成一個列表,在生成很大的數字序列的時候,xrange會比range性能優很多,因爲不需要一上來就開闢一塊很大的內存空間!值得注意的是,在遍歷數值較大的情況之下,xrange將會報錯,range不會,如下:

start = 1000000000000000000000
end = 1000000000000000000010
print xrange(start, end)        # OverflowError
print range(start, end)         

在Python3中已將xrange更名爲range,並不存在名稱爲xrange的方法,並且不會出現如上的報錯問題

迭代器與生成器

什麼是迭代器?迭代器是一個帶狀態的對象,它能在你調用next()方法時,返回容器的下一個值,任何實現了__iter____next__()(python2中是next())方法的對象都是迭代器,__iter__返回迭代器自身,__next__()返回容器的下一個值,如果沒有更多元素了,將會引發StopIteration錯誤!
生成器是特殊的迭代器,不過這種迭代器更加優雅

負載均衡、正向代理、反向代理

負載均衡:負載均衡是一種服務器或網絡設備的集羣技術,負載均衡將特定的業務分擔給多個服務器或網絡設備,從而提高了業務處理能力,保證了業務的高可用性!
正向代理:作爲媒介將互聯網的資源傳遞給與之關聯的客戶端,代理跟客戶端處於同一局域網下,對於服務端是透明的
反向代理:根據客戶端的請求,從服務端獲取相應的資源傳遞給與之關聯的客戶端,代理跟服務端處於同一局域網下,對客戶端是透明的

什麼是猴子補丁

“猴子補丁”就是在函數或者對象已經定義之後,再去改變它們的行爲。舉個例子:

import datetime

datetime.datetime.now = lambda: datetime.datetime(2012, 12, 12)

大部分情況之下,這是一種很不好的做法,函數在代碼庫中行爲最好是都保持一致。

什麼是跨域

瀏覽器對JavaScript同源策略的限制,請求的URL必須與瀏覽器上的URL地址處於同域上,也就是域名,端口,協議相同

什麼是索引

索引是一種數據結構,可以快速的幫助我們進行數據的查找!索引的數據結構與具體存儲引擎的實現有關,MySQL中常用的索引有哈希索引、B+樹索引等,我們常用的InnoDB存儲引擎的默認索引實現是B+樹索引

哈希索引與B+樹索引的區別或者優劣

實現原理:哈希索引底層就是哈希表,進行查找時,調用一次哈希函數就可以獲取到對應的鍵值,之後進行回表查詢獲取到實際的數據;B+樹索引底層都是多路平衡查找樹,對於每一次查詢都是從根節點出發,查找到葉子節點方可以獲取所查鍵值,然後根據查詢判斷是否需要進行回表查詢數據!

從原理可以看出他們的不同,哈希索引進行等值查詢更快,卻無法進行範圍查詢!因爲哈希索引進過哈希函數建立索引之後,索引的順序與原順序無法保持一致,不能支持範圍查詢。B+樹索引的所有節點皆遵循左節點小於父節點,右節點大於父節點,天然支持範圍查詢!

  • 哈希索引不支持索引進行排序,原理同上
  • 哈希索引不支持模糊查詢和多列索引的最左前綴匹配
  • 哈希索引在任何時候都避免不了進行回表查詢;而B+樹索引在滿足一些特定條件(聚簇索引、覆蓋索引等)的時候,可以只通過索引完成查詢
  • 哈希索引雖然在等值查詢較快,但是不穩定,性能不可預測,如果某個鍵值存在大量重複時,發生哈希碰撞,此時效率可能極差;而B+樹索引的查詢比較穩定,所有的查詢都是從根節點到葉子節點,且樹的高度較低

什麼是事務

事務是一系列操作,要符合ACID特性。最常見的理解是:事務中的操作要麼全部成功,要麼全部失敗!

ACID是什麼?請詳細介紹一下

A=Atomicity

原子性,就是上面所說的要麼全部成功,要麼全部失敗,不可能只執行了一部分操作!

C=Consistency

一致性,系統總是從一個一致性的狀態轉移到另外一個一致性的狀態,不會存在中間狀態

I=Isolation

隔離性,通常來說,一個事務在提交之前對於其他事務是不可見的

D=Durability

持久性,一旦事務提交,那麼永遠就是這樣子了,哪怕系統崩潰,也不會影響這個事務的結果

如果有多個事務在同時進行會怎麼樣

多事務的併發一般會造成以下幾個問題:

  • 髒讀:A事務讀取到了B事務未提交的內容,而B事務後面進行了回滾
  • 不可重複讀:當設置A事務只能讀取B事務已經提交的部分,會造成A事務的兩次查詢結果不一樣,因爲期間B事務進行了提交操作
  • 幻讀:A事務讀取了一個範圍的內容,與此同時B事務插入了一條數據,造成“幻讀”

MySQL引擎

在MySQL客戶端中,可以使用以下命令查看MySQL支持的引擎

mysql> show engines;

MySQL支持多種存儲引擎,比如InnoDBMyISAMMemoryArchive等等,在大多數情況下,直接選擇InnoDB存儲引擎是最合適的,InnoDB也是MySQL默認的存儲引擎。

InnoDBMyISAM的區別:

  • InnoDB支持事務,MyISAM不支持事務
  • InnoDB支持行級鎖,MyISAM支持表級鎖
  • InnoDB支持MVCC,而MyISAM不支持
  • InnoDB支持外鍵,MyISAM不支持外鍵
  • InnoDB不支持全文檢索,MyISAM支持

MySQL中char與varchar的區別

  • 定長與變長的區別:char是定長,即長度固定;varchar是變長,即長度可變。當插入的字符串長度超過所設置的長度時,將會視情況來處理,如果是嚴格模式,將會拒絕插入並提示錯誤信息;如果是寬鬆模式,則會截取後插入。如果插入的字符串小於設置的長度時,char將會在後面補充空格,直至滿足設置的長度;varchar則不會如此,插入多少個就存多少個
  • 存儲容量的不同:char最多能存放字符個數爲255;varchar則最多能存放65532個字符

MySQL字段爲什麼建議設置爲not null

null值佔用更多的字節,且會在程序中造成很多與預期不符的情況

MySQL性能優化

  • 查詢緩存:MySQL服務器默認都開啓了查詢緩存
  • Limit 1:只獲取一條數據時,加了Limit 1可以提高效率
  • 創建索引:爲經常用來查詢的字段創建索引
  • Join查詢創建索引:Join查詢的字段應創建索引
  • 千萬不要 ORDER BY RAND()
  • 儘量避免使用select *
  • 同一字段的where查詢,使用in而不適用or
  • 使用正確的數據類型:例如手機號通常使用字符串類型進行存儲,如果查詢時傳遞的是一個數字類型,儘管mysql會對其轉義,但這依舊會增加查詢時間

超大分頁該怎麼處理

我們先來舉幾個查詢的例子:

select * from product limit 10, 20   0.016秒
select * from product limit 100, 20   0.016秒
select * from product limit 1000, 20   0.047秒
select * from product limit 10000, 20   0.094秒
select * from product limit 400000, 20   3.229秒
select * from product limit 866613, 20   37.44秒 

可以看出,隨着我們查詢的偏移量越來越大,查詢所用的時間也越來越久!此時我們可以利用的覆蓋索引來加速分頁查詢,我們知道如果索引查詢中的語句只包含了那個索引列(覆蓋索引),那麼速度就會很快,下面我們再來看一下上面的查詢:

select * from product where id >= (select id from product limit 866613, 1) limit 20

查詢時間爲0.2秒,這簡直是一個質的飛躍

MySQL中drop、delete和truncate的區別

  • drop直接刪除掉表;truncate刪除表中數據,再插入時自增ID又從1開始;delete刪除表中數據,可以加where字句。
  • delete語句執行刪除的過程是每次從表中刪除一行,並且會同時將該行的刪除操作作爲事務記錄在日誌中保存以便進行回滾操作;truncate則一次性的從表中刪除所有的數據並不把單獨的刪除操作記錄記入日誌保存,未備份情況下,刪除的行是不能回覆的,執行速度相對delete快!
  • truncate後,這個表和索引所佔用的空間會回覆到初始大小;delete操作不會減少表和索引所佔用的空間;drop會將表所佔用的空間全部釋放掉
  • 應用範圍:truncate只能針對table,而delete可以使table和view

Redis支持哪些數據結構

字符串、字典、列表、集合、有序集合

Redis分佈式鎖

先使用setnx來爭搶鎖,搶到之後再用expire給鎖加一個過期時間防止鎖忘記了釋放。但是,這樣並不完善,如果在執行setnx之後,執行expire之前進程意外crash或者要重啓維護了,那麼這個鎖將永遠得不到釋放!我們可以使用set指令,並指定相應的參數把setnxexpire合成一條指令來用

假如Redis裏面有1億個key,其中有10W個key是以固定已知的前綴命名的,如何將他們全部找出來?

使用keys指令可以掃除指定模式的key列表,但是因爲redis是單線程的,keys指令會導致線程阻塞一段時間,線上服務會停頓,直到指令執行完畢,服務才能恢復!這個時候我們可以使用scan指令可以無阻塞的提取出指定模式的key列表,但是會有一定的重複機率,需進行去重,整體花費時間會比直接使用keys指令長

如果有大量的key需要設置同一時間過期,一般需要注意什麼?

如果大量的key過期時間設置的過於集中,到了那個過期的時間點,Redis可能會出現短暫的卡頓現象,一般需要在時間上加一個隨機值,使得過期時間分散一點

Redis缺點

  • 數據庫容量受到物理內存的限制,不能用作海量數據的高性能讀寫,因此Redis適合的場景主要侷限在較小數據量的高性能操作和運算上。
  • Redis較難支持在線擴容,在集羣容量達到上限時在線擴容會變得很複雜。爲避免這一問題,運維人員在系統上線時必須確保有足夠的空間,這對資源造成了很大的浪費。

什麼是LRU算法

LRU(Least Recently Used),即最近最少使用,是一種緩存置換算法。在使用內存作爲緩存的時候,緩存的大小一般是固定的。當緩存被佔滿,這個時候繼續往緩存裏面添加數據,就需要淘汰一部分老的數據,釋放內存空間來存儲新的數據。這個時候就可以使用LRU算法了。其核心思想是:如果一個數據在最近一段時間沒有被用到,那麼將來被使用到的可能性也很小,所以就可以被淘汰掉。

修改redis佔用內存大小

  • 通過配置文件修改
# 設置redis最大佔用內存大小爲100MB
maxmemory 100mb
  • 通過命令修改
# 設置redis最大佔用內存大小爲100MB
127.0.0.1:6379> config set maxmemory 100mb

# 獲取redis能使用的最大內存大小
127.0.0.1:6379> config get maxmemory

redis相較於memcached有哪些優勢

  1. redis支持的數據類型比memcached多
  2. redis的速度比memcached快很多
  3. redis可以持久化其數據

redis中一個字符串類型的值能存儲最大容量是多少?

512M

程序題

獲取指定路徑下的所有文件(包含子孫文件)

import os

def get_files(parent_path):
    for child in os.listdir(parent_path):
        child_path = os.path.join(parent_path, child)
        if os.path.isdir(child_path):
            get_files(child_path)
        else:
            print child_path

下面代碼會輸出什麼

def f(x, l=[]):
    for i in range(x):
        l.append(i * i)
    print l
    
f(2)
f(2, [3, 2, 1])
f(3)

################ 結果如下 ################
[0, 1]
[3, 2, 1, 0, 1]
[0, 1, 0, 1, 4]

解析:第一個函數調用十分明顯,for循環先後將0和1添加至了空列表l中。l是變量的名字,指向內存中存儲的一個列表;第二個函數調用在一塊新的內存中創建了新的列表。l這時指向了新生成的列表。之後再往新列表中添加0、1;第三個函數調用的結果就有些奇怪了。它使用了之前內存地址中存儲的舊列表。這就是爲什麼它的前兩個元素是0和1了。

函數參數傳遞

a = 1
def func_1(a):
    a = 2

func_1(a)
print(a)
    
l = []
def func_2(l):
    l.append(1)
    
func_2(l)
print(l)

################ 結果如下 ################
1
[1]

爲什麼會出現兩種截然不同的效果。下面我們加一些打印:

a = 1
def func_1(a):
    print("func_1, first", id(a))
    a = 2
    print("func_1, second", id(a), id(2))

print("first", id(a))
func_1(a)
print("second", id(a))
print(a)

    
l = []
def func_2(l):
    print("func_2, first", id(l))
    l.append(1)

print("first", id(l))
func_2(l)
print("second", id(l))
print(l)

################ 結果如下 ################
('first', 140670366951256)
('func_1, first', 140670366951256)
('func_1, second', 140670366951232, 140670366951232)
('second', 140670366951256)
1
('first', 4322204704)
('func_2, first', 4322204704)
('second', 4322204704)
[1]

請看案例1的打印,進入函數時,a的內存地址還是引用上面a的地址,當經過重新賦值以後,內存地址變成了2的內存地址,而函數外面的a還是原來的地址,所以不發生改變!

生成器題目

下面程序打印結果爲何:

>>> array = [1, 4, 5]
>>> g = (x for x in array if array.count(x) > 0)
>>> array = [2, 4, 8]
>>> print(list(array))

最終打印結果爲[4]。意不意外,驚不驚喜!這是因爲在生成器表達式中,in子句在聲明時執行,而條件子句是在運行時執行

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章