1、Python 中類方法、類實體方法、靜態方法有何區別?
類方法:是類物件的方法,在定義時需要在上方使用“@ classmethod”進行裝飾,形參為 cls,表示類物件,類物件和實體物件都可呼叫
類實體方法:是類實體化物件的方法,只有實體物件可以呼叫,形參為 self,指代物件本身
靜態方法:是一個任意函式,在其上方使用“@ staticmethod”進行裝飾,可以用物件直接呼叫,靜態方法實際上跟該類沒有太大關系
2、Python 的記憶體管理機制及調優手段?
記憶體管理機制:參考計數、垃圾回收、記憶體池,
參考計數
參考計數是一種非常高效的記憶體管理手段, 當一個 Python 物件被參考時其參考計數增加 1, 當其不再被一個變數參考時則計數減 1. 當參考計數等于 0 時物件被洗掉,
垃圾回收
參考計數
參考計數也是一種垃圾收集機制,而且也是一種最直觀,最簡單的垃圾收集技術,當 Python 的某
個物件的參考計數降為 0 時,說明沒有任何參考指向該物件,該物件就成為要被回收的垃圾了,比如
某個新建物件,它被分配給某個參考,物件的參考計數變為 1,如果參考被洗掉,物件的參考計數為 0,
那么該物件就可以被垃圾回收,不過如果出現回圈參考的話,參考計數機制就不再起有效的作用了
(2)標記清除
如果兩個物件的參考計數都為 1,但是僅僅存在他們之間的回圈參考,那么這兩個物件都是需要被
回收的,也就是說,它們的參考計數雖然表現為非 0,但實際上有效的參考計數為 0,所以先將回圈引
用摘掉,就會得出這兩個物件的有效計數,
(3) 分代回收
從前面“標記-清除”這樣的垃圾收集機制來看,這種垃圾收集機制所帶來的額外操作實際上與系統
中總的記憶體塊的數量是相關的,當需要回收的記憶體塊越多時,垃圾檢測帶來的額外操作就越多,而垃圾
回收帶來的額外操作就越少;反之,當需回收的記憶體塊越少時,垃圾檢測就將比垃圾回收帶來更少的額
外操作,
舉個例子:
當某些記憶體塊 M 經過了 3 次垃圾收集的清洗之后還存活時,我們就將記憶體塊 M 劃到一個集合 A 中去,而新分配的記憶體都劃分到集合 B 中去,當垃圾收集開始作業時,大多數情況都只對集合 B 進行垃圾回收,而對集合 A 進行垃圾回收要隔相當長一段時間后才進行,這就使得垃圾收集機制需要處理的記憶體少了,效率自然就提高了,在這個程序中,集合 B 中的某些記憶體塊由于存活時間長而會被轉移到集合 A 中,當然,集合 A 中實際上也存在一些垃圾,這些垃圾的回識訓因為這種分代的機制而被延遲,
記憶體池
(1) Python 的記憶體機制呈現金字塔形狀,-1,-2 層主要有作業系統進行操作
(2) 第 0 層是 C 中的 malloc,free 等記憶體分配和釋放函式進行操作
(3)第 1 層和第 2 層是記憶體池,有 Python 的介面函式 PyMem_Malloc 函式實作,當物件小于
256K 時有該層直接分配記憶體
(4) 第 3 層是最上層,也就是我們對 Python 物件的直接操作
Python 在運行期間會大量地執行 malloc 和 free 的操作,頻繁地在用戶態和核心態之間進行切換,這將嚴重影響 Python 的執行效率,為了加速 Python 的執行效率,Python 引入了一個記憶體池機制,用于管理對小塊記憶體的申請和釋放,
Python 內部默認的小塊記憶體與大塊記憶體的分界點定在 256 個位元組,當申請的記憶體小于 256 位元組時,PyObject_Malloc 會在記憶體池中申請記憶體;當申請的記憶體大于 256 位元組時,PyObject_Malloc 的行為將蛻化為 malloc 的行為,當然,通過修改 Python 源代碼,我們可以改變這個默認值,從而改變 Python 的默認記憶體管理行為,
3、記憶體泄露是什么?如何避免?
由于疏忽或錯誤造成程式未能釋放已經不再使用的記憶體的情況,
記憶體泄漏并非指記憶體在物理上的消失,而是應用程式分配某段記憶體后,由于設計錯誤,失去了對該段記憶體的控制,因而造成了記憶體的浪費,導致程式運行速度減慢甚至系統崩潰等嚴重后果,
del() 函式的物件間的回圈參考是導致記憶體泄漏的主兇,
不使用一個物件時使用:del object 來洗掉一個物件的參考計數就可以有效防止記憶體泄漏問題,
通過 Python 擴展模塊 gc 來查看不能回收的物件的詳細資訊,
可以通過 sys.getrefcount(obj) 來獲取物件的參考計數,并根據回傳值是否為 0 來判斷是否記憶體
泄漏,
4、Python 函式呼叫的時候引數的傳遞方式是值傳遞還是參考傳遞?
Python 的引數傳遞有:位置引數、默認引數、可變引數、關鍵字引數,函式的傳值到底是值傳遞還是參考傳遞,要分情況:
不可變引數用值傳遞
像整數和字串這樣的不可變物件,是通過拷貝進行傳遞的,因為你無論如何都不可能在原處改變不可變物件
可變引數是參考傳遞的
比如像串列,字典這樣的物件是通過參考傳遞、和 C 語言里面的用指標傳遞陣列很相似,可變物件能在函式內部改變,
5、對預設引數的理解?
預設引數指在呼叫函式的時候沒有傳入引數的情況下,呼叫默認的引數,在呼叫函式的同時賦值時,所傳入的引數會替代默認引數,
*args 是不定長引數,他可以表示輸入引數是不確定的,可以是任意多個,
**kwargs 是關鍵字引數,賦值的時候是以鍵 = 值的方式,引數是可以任意多對在定義函式的時候
不確定會有多少引數會傳入時,就可以使用兩個引數,
補充
*args
如果你之前學過 C 或者 C++,看到星號的第一反應可能會認為這個與指標相關,然后就開始方了,其實放寬心,Python 中是沒有指標這個概念的,
在 Python 中我們使用星號收集位置引數,請看下面的例子:
def fun(x,*args):
... print(x)
... res = x
... print(args)
... for i in args:
... res += i
... return res
...
print(fun(1,2,3,4,5,6))
上述例子中,函式的引數有兩個,但是我們在輸出的時候賦給函式的引數個數不僅僅是兩個,讓我們來運行這個代碼,得到如下的結果:
1
(2, 3, 4, 5, 6)
21
從上面我們可以看出,引數 x 得到的值是 1,引數 args 得到的是一個元組 (2,3,4,5,6) ,由此我們可以得出,如果輸入的引數個數不確定,其它的引數全部通過 *args 以元組的方式由 arg 收集起來,
為了更能明顯的看出 *args,我們下面用一個簡單的函式來表示:
def print_args(*args):
... print(args)
...
接下來我傳入不同的值,通過引數 *args 得到的結果我們來看一下:
print_args(1,2,3)
(1, 2, 3)
print_args('abc','def','ghi')
('abc', 'def', 'ghi')
print_args('abc',['a','b','c'],1,2,3)
('abc', ['a', 'b', 'c'], 1, 2, 3)
不管是什么,都可以一股腦的塞進元組里,即使只有一個值,也是用元組收集,所以還記得在元組里一個元素的時候的形式嗎?元組中如果只有一個元素,該元素的后面要有一個逗號,
那么如果不給 *args 傳值呢?
def print_args(*args):
... print(args)
...
print_args()
()
答案就是這時候 *args 收集到的是一個空的元組,
最后提醒一點的是,當使用星號的時候,不一定要把元組引數命名為 args,但這個是 Python 中的一個常見做法,
**kwargs
使用兩個星號是收集關鍵字引數,可以將引數收集到一個字典中,引數的名字是字典的 “鍵”,對應的引數的值是字典的 “值”,請看下面的例子:
def print_kwargs(**kwargs):
... print(kwargs)
...
print_kwargs(a = 'lee',b = 'sir',c = 'man')
{'a': 'lee', 'b': 'sir', 'c': 'man'}
由例子可以看出,在函式內部,kwargs 是一個字典,
看到這的時候,可能聰明的你會想,引數不是具有不確定型嗎?如何知道引數到底會用什么樣的方式傳值?其實這個很好辦,把 args 和 *kwargs 綜合起來就好了啊,請看下面的操作:
def print_all(x,y,*args,**kwargs):
... print(x)
... print(y)
... print(args)
... print(kwargs)
...
print_all('lee',1234)
lee
1234
()
{}
print_all('lee',1,2,3,4,5)
lee
1
(2, 3, 4, 5)
{}
print_all('lee',1,2,3,4,5,like = 'python')
lee
1
(2, 3, 4, 5)
{'like': 'python'}
如此這般,我們就可以應對各種各樣奇葩無聊的引數請求了,當然在這還是要說的是,這里的關鍵字引數命名不一定要是 kwargs,但這個是通常做法,
6、面向物件中super的作用?
super() 函式是用于呼叫父類(超類)的一個方法,
super 是用來解決多重繼承問題的,直接用類名呼叫父類方法在使用單繼承的時候沒問題,但是如果使用多繼承,會涉及到查找順序(MRO)、重復呼叫(鉆石繼承)等種種問題,
MRO 就是類的方法決議順序表, 其實也就是繼承父類方法時的順序表,
作用:
- 根據 mro 的順序執行方法
- 主動執行 Base 類的方法
7、是否使用過functools中的函式?其作用是什么?
Python的functools模塊用以為可呼叫物件(callable objects)定義高階函式或操作,
簡單地說,就是基于已有的函式定義新的函式,
所謂高階函式,就是以函式作為輸入引數,回傳也是函式,
8、json序列化時,默認遇到中文會轉換成unicode,如果想要保留中文怎么辦?
import json
a = json.dumps({"ddf": "你好"}, ensure_ascii=False)
print(a)
# {"ddf": "你好"}
9、什么是斷言?應用場景?
assert斷言——宣告其布林值必須為真判定,發生例外則為假,
info = {}
info['name'] = 'egon'
info['age'] = 18
# 用assert取代上述代碼:
assert ('name' in info) and ('age' in info)
設定一個斷言目的就是要求必須實作某個條件,
10、有用過with statement嗎?它的好處是什么?
with陳述句的作用是通過某種方式簡化例外處理,它是所謂的背景關系管理器的一種
用法舉例如下:
with open('output.txt', 'w') as f:
f.write('Hi there!')
當你要成對執行兩個相關的操作的時候,這樣就很方便,以上便是經典例子,with陳述句會在嵌套的代碼執行之后,自動關閉檔案,
這種做法的還有另一個優勢就是,無論嵌套的代碼是以何種方式結束的,它都關閉檔案,
如果在嵌套的代碼中發生例外,它能夠在外部exception handler catch例外前關閉檔案,
如果嵌套代碼有return/continue/break陳述句,它同樣能夠關閉檔案,
簡述 Python 在例外處理中,else 和 finally 的作用分別是什么?
如果一個 Try - exception 中,沒有發生例外,即 exception 沒有執行,那么將會執行 else 陳述句的內容,反之,如果觸發了 Try - exception(例外在 exception 中被定義),那么將會執行exception
中的內容,而不執行 else 中的內容,
如果 try 中的例外沒有在 exception 中被指出,那么系統將會拋出 Traceback(默認錯誤代碼),并且終止程式,接下來的所有代碼都不會被執行,但如果有 Finally 關鍵字,則會在程式拋出 Traceback 之前(程式最后一口氣的時候),執行 finally 中的陳述句,這個方法在某些必須要結束的操作中頗為有用,如釋放檔案句柄,或釋放記憶體空間等,
11、map 函式和 reduce 函式?
(1) 從引數方面來講:
map()包含兩個引數,第一個引數是一個函式,第二個是序列(串列 或元組),其中,函式(即 map 的第一個引數位置的函式)可以接收一個或多個引數,
reduce()第一個引數是函式,第二個是序列(串列或元組),但是,其函式必須接收兩個引數,
(2) 從對傳進去的數值作用來講:
map()是將傳入的函式依次作用到序列的每個元素,每個元素都是獨自被函式“作用”一次 ,
reduce()是將傳人的函式作用在序列的第一個元素得到結果后,把這個結果繼續與下一個元素作用(累積計算),
補充 Python 特殊函式
lambda 函式
lambda 是一個可以只用一行就能解決問題的函式,讓我們先看下面的例子:
>>> def add(x):
... x += 1
... return x
...
>>> numbers = range(5)
>>> list(numbers)
[0, 1, 2, 3, 4]
>>> new_numbers = []
>>> for i in numbers:
... new_numbers.append(add(i))
...
>>> new_numbers
[1, 2, 3, 4, 5]
在上面的這個例子中,函式 add() 充當了一個中間角色,當然上面的例子也可以如下實作:
>>> new_numbers = [i+1 for i in numbers]
>>> new_numbers
[1, 2, 3, 4, 5]
首先我要說,上面的串列決議式其實是很好用的,但是我偏偏要用 lambda 這個函式代替 add(x) :
>>> lamb = lambda x: x+1
>>> new_numbers = []
>>> for i in numbers:
... new_numbers.append(lamb(i))
...
>>> new_numbers
[1, 2, 3, 4, 5]
在這里的 lamb 就相當于 add(x) ,lamb = lambda x : x+1 就相當于 add(x) 里的代碼塊,下面再寫幾個應用 lambda 的小例子:
>>> lamb = lambda x,y : x + y
>>> lamb(1,2)
3
>>> lamb1 = lambda x : x ** 2
>>> lamb1(5)
25
由上面的例子我們可以總結一下 lambda 函式的具體使用方法:lambda 后面直接跟變數,變臉后面是冒號,冒號后面是運算式,運算式的計算結果就是本函式的回傳值,
在這里有一點需要提醒的是,雖然 lambda 函式可以接收任意多的引數并且回傳單個運算式的值,但是 lambda 函式不能包含命令且包含的運算式不能超過一個,如果你需要更多復雜的東西,你應該去定義一個函式,
lambda 作為一個只有一行的函式,在你具體的編程實踐中可以選擇使用,雖然在性能上沒什么提升,但是看著舒服呀,
map 函式
我們在上面講 lambda 的時候用的例子,其實 map 也可以實作,請看下面的操作:
>>> numbers = [0,1,2,3,4]
>>> map(add,numbers)
[1, 2, 3, 4, 5]
>>> map(lambda x: x + 1,numbers)
[1, 2, 3, 4, 5]
map 是 Python 的一個內置函式,它的基本格式是:map(func, seq),
func 是一個函式物件,seq 是一個序列物件,在執行的時候,seq 中的每個元素按照從左到右的順序依次被取出來,塞到 func 函式里面,并將 func 的回傳值依次存到一個串列里,
對于 map 要主要理解以下幾個點就好了:
1.對可迭代的物件中的每一個元素,依次使用 fun 的方法(其實本質上就是一個 for 回圈),
2.將所有的結果回傳一個 map 物件,這個物件是個迭代器,
我們接下來做一個簡單的小題目:將兩個串列中的對應項加起來,把結果回傳在一個串列里,我們用 map 來做,如果你做完了,請往下看:
>>> list1 = [1,2,3,4]
>>> list2 = [5,6,7,8]
>>> list(map(lambda x,y: x + y,list1,list2))
[6, 8, 10, 12]
你看上面,是不是很簡單?其實這個還看不出 map 的方便來,因為用 for 同樣也不麻煩,要是你有這樣的想法的話,那么請看下面:
>>> list1 = [1,2,3,4]
>>> list2 = [5,6,7,8]
>>> list3 = [9,10,11,12]
>>> list(map(lambda x,y,z : x + y + z,list1,list2,list3))
[15, 18, 21, 24]
你看三個呢?是不是用 for 的話就稍顯麻煩了?那么我們在想如果是 四個,五個乃至更多呢?這就顯示出 map 的簡潔優雅了,并且 map 還不和 lambda 一樣對性能沒有什么提高,map 在性能上的優勢也是杠杠的,
filter 函式
filter 翻譯過來的意思是 “過濾器”,在 Python 中,它也確實是起到的是過濾器的作用,這個解釋起來略微麻煩,還是直接上代碼的好,在代碼中體會用法是我在所有的文章里一直在體現的:
>>> numbers = range(-4,4)
>>> list(filter(lambda x: x > 0,numbers))
[1, 2, 3]
上面的例子其實和下面的代碼是等價的:
>>> [x for x in numbers if x > 0]
[1, 2, 3]
然后我們再來寫一個例子體會一下:
>>> list(filter(lambda x: x != 'o','Rocky0429'))
['R', 'c', 'k', 'y', '0', '4', '2', '9']
reduce 函式
我在之前的文章中很多次都說過,我的代碼都是用 Python3 版本的,在 Python3 中,reduce 函式被放到 functools 模塊里,在 Python2 中還是在全域命名空間,
同樣我先用一個例子來跑一下,我們來看看怎么用:
>>> reduce(lambda x,y: x+y,[1,2,3,4])
10
reduce 函式的第一個引數是一個函式,第二個引數是序列型別的物件,將函式按照從左到右的順序作用在序列上,如果你還不理解的話,我們下面可以對比一下它和 map 的區別:
>>> list1 = [1,2,3,4]
>>> list2 = [5,6,7,8]
>>> list(map(lambda x,y: x + y,list1,list2))
[6, 8, 10, 12]
對比上面的兩個例子,就知道兩者的區別,map 相當于是上下運算的,而 reduce 是從左到右逐個元素進行運算,
12、setattr,getattr,__delattr__函式使用詳解?
1.setattr(self,name,value):如果想要給 name 賦值的話,就需要呼叫這個方法,
2.getattr(self,name):如果 name 被訪問且它又不存在,那么這個方法將被呼叫,
3.delattr(self,name):如果要洗掉 name 的話,這個方法就要被呼叫了,
13、請描述抽象類和介面類的區別和聯系?
(1) 抽象類
規定了一系列的方法,并規定了必須由繼承類實作的方法,由于有抽象方法的存在,所以抽象類不能實體化,可以將抽象類理解為毛坯房,門窗、墻面的樣式由你自己來定,所以抽象類與作為基類的普通類的區別在于約束性更強,
(2) 介面類
與抽象類很相似,表現在介面中定義的方法,必須由參考類實作,但他與抽象類的根本區別在于用途:與不同個體間溝通的規則(方法),你要進宿舍需要有鑰匙,這個鑰匙就是你與宿舍的介面,你的同室也有這個介面,所以他也能進入宿舍,你用手機通話,那么手機就是你與他人交流的介面,
(3) 區別和關聯
- 介面是抽象類的變體,介面中所有的方法都是抽象的,而抽象類中可以有非抽象方法,抽象類是宣告方法的存在而不去實作它的類,
- 介面可以繼承,抽象類不行,
- 介面定義方法,沒有實作的代碼,而抽象類可以實作部分方法,
- 介面中基本資料型別為 static 而抽類象不是,
- 介面可以繼承,抽象類不行,
- 可以在一個類中同時實作多個介面,
- 介面的使用方式通過 implements 關鍵字進行,抽象類則是通過繼承 extends 關鍵字進行,
14、請描述方法多載與方法重寫?
(1)方法多載
是在一個類里面,方法名字相同,而引數不同,回傳型別呢?可以相同也可以不同,多載是讓類以統一的方式處理不同型別資料的一種手段,
(2) 方法重寫
子類不想原封不動地繼承父類的方法,而是想作一定的修改,這就需要采用方法的重寫,方法重寫又稱方法覆寫,
15、單例模式的應用場景有哪些?
單例模式應用的場景一般發現在以下條件下:
(1)資源共享的情況下,避免由于資源操作時導致的性能或損耗等,如日志檔案,應用配置,
(2)控制資源的情況下,方便資源之間的互相通信,如執行緒池等, 1.網站的計數器 2.應用配置 3.多執行緒池 4.資料庫配置,資料庫連接池 5.應用程式的日志應用....
補充
01.單例設計模式
「單例設計模式」估計對很多人來說都是一個陌生的概念,其實它就環繞在你的身邊,比如我們每天必用的聽歌軟體,同一時間只能播放一首歌曲,所以對于一個聽歌的軟體來說,負責音樂播放的物件只有一個;再比如列印機也是同樣的道理,同一時間列印機也只能列印一份檔案,同理負責列印的物件也只有一個,
結合說的聽歌軟體和列印機都只有唯一的一個物件,就很好理解「單例設計模式」,
單例設計模式確保一個類只有一個實體,并提供一個全域訪問點,
「單例」就是單個實體,我們在定義完一個類的之后,一般使用「類名()」的方式創建一個物件,而單例設計模式解決的問題就是無論執行多少遍「類名()」,回傳的物件記憶體地址永遠是相同的,
02.*new* 方法
當我們使用「類名()」創建物件的時候,Python 解釋器會幫我們做兩件事情:第一件是為物件在記憶體分配空間,第二件是為物件進行初始化,初始化(init)我們已經學過了,那「分配空間」是哪一個方法呢?就是我們這一小節要介紹的 new 方法,
那這個 new 方法和單例設計模式有什么關系呢?單例設計模式回傳的物件記憶體地址永遠是相同的,這就意味著在記憶體中這個類的物件只能是唯一的一份,為達到這個效果,我們就要了解一下為物件分配空間的 new 方法,
明確了這個目的以后,接下來讓我們看一下 new 方法,new 方法在內部其實做了兩件時期:第一件事是為「物件分配空間」,第二件事是「把物件的參考回傳給 Python 解釋器」,當 Python 的解釋器拿到了物件的參考之后,就會把物件的參考傳遞給 init 的第一個引數 self,init 拿到物件的參考之后,就可以在方法的內部,針對物件來定義實體屬性,
這就是 new 方法和 init 方法的分工,
總結一下就是:之所以要學習 new 方法,就是因為需要對分配空間的方法進行改造,改造的目的就是為了當使用「類名()」創建物件的時候,無論執行多少次,在記憶體中永遠只會創造出一個物件的實體,這樣就可以達到單例設計模式的目的,
03.重寫 *new* 方法
在這里我用一個 new 方法的重寫來做一個演練:首先定義一個列印機的類,然后在類里重寫一下 new 方法,通過對這個方法的重寫來強化一下 new 方法要做的兩件事情:在記憶體中分配記憶體空間 & 回傳物件的參考,同時驗證一下,當我們使用「類名()」創建物件的時候,Python 解釋器會自動幫我們呼叫 new 方法,
首先我們先定義一個列印機類 Printer,并創建一個實體:
class Printer():
def __init__(self):
print("列印機初始化")
# 創建列印機物件
printer = Printer()
接下來就是重寫 new 方法,在此之前,先說一下注意事項,只要??了這幾點,重寫 new 就沒什么難度:
重寫 new 方法一定要回傳物件的參考,否則 Python 的解釋器得不到分配了空間的物件參考,就不會呼叫物件的初始化方法;
new 是一個靜態方法,在呼叫時需要主動傳遞 cls 引數,
# 重寫 __new__ 方法
class Printer():
def __new__(cls, *args, **kwargs):
# 可以接收三個引數
# 三個引數從左到右依次是 class,多值元組引數,多值的字典引數
print("this is rewrite new")
instance = super().__new__(cls)
return instance
def __init__(self):
print("列印機初始化")
# 創建列印機物件
player = Printer()
print(player)
上述代碼對 new 方法進行了重寫,我們先來看一下輸出結果:
this is rewrite new
列印機初始化
<__main__.Printer object at 0x10fcd2ba8>
上述的結果列印出了 new 方法和 init 方法里的內容,同時還列印了類的記憶體地址,順序正好是我們在之前說過的,new 方法里的三行代碼正好做了在本小節開頭所說的三件事:
- print(this is rewrite new):證明了創建物件時,new 方***被自動呼叫;
- instance = super().new(cls):為物件分配記憶體空間(因為 new 本身就有為物件分配記憶體空間的能力,所以在這直接呼叫父類的方法即可);
- return instance:回傳物件的參考,
04.設計單例模式
說了這么多,接下來就讓我們用單例模式來設計一個單例類,乍一看單例類看起來比一般的類更唬人,但其實就是差別在一點:單例類在創建物件的時候,無論我們呼叫多少次創建物件的方法,得到的結果都是記憶體中唯一的物件,
可能到這有人會有疑惑:怎么知道用這個類創建出來的物件是同一個物件呢?其實非常的簡單,我們只需要多呼叫幾次創建物件的方法,然后輸出一下方法的回傳結果,如果記憶體地址是相同的,說明多次呼叫方法回傳的結果,本質上還是同一個物件,
class Printer():
pass
printer1 = Printer()
print(printer1)
printer2 = Printer()
print(printer2)
上面是一個一般類的多次呼叫,列印的結果如下所示:
<__main__.Printer object at 0x10a940780>
<__main__.Printer object at 0x10a94d3c8>
可以看出,一般類中多次呼叫的記憶體地址不同(即 printer1 和 printer2 是兩個完全不同的物件),而單例設計模式設計的單例類 Printer(),要求是無論呼叫多少次創建物件的方法,控制臺列印出來的記憶體地址都是相同的,
那么我們該怎么實作呢?其實很簡單,就是多加一個「類屬性」,用這個類屬性來記錄「單例物件的參考」,
為什么要這樣呢?其實我們一步一步的來想,當我們寫完一個類,運行程式的時候,記憶體中其實是沒有這個類創建的物件的,我們必須呼叫創建物件的方法,記憶體中才會有第一個物件,在重寫 new 方法的時候,我們用 instance = super().new(cls) ,為物件分配記憶體空間,同時用 istance 類屬性記錄父類方法的回傳結果,這就是第一個「物件在記憶體中的回傳地址」,當我們再次呼叫創建物件的方法時,因為第一個物件已經存在了,我們直接把第一個物件的參考做一個回傳,而不用再呼叫 super().new(cls) 分配空間這個方法,所以就不會在記憶體中為這個類的其它物件分配額外的記憶體空間,而只是把之前記錄的第一個物件的參考做一個回傳,這樣就能做到無論呼叫多少次創建物件的方法,我們永遠得到的是創建的第一個物件的參考,
這個就是使用單例設計模式解決在記憶體中只創建唯一一個實體的解決辦法,下面我就根據上面所說的,來完成單例設計模式,
class Printer():
instance = None
def __new__(cls, *args, **kwargs):
if cls.instance is None:
cls.instance = super().__new__(cls)
return cls.instance
printer1 = Printer()
print(printer1)
printer2 = Printer()
print(printer2)
上述代碼很簡短,首先給類屬性復制為 None,在 new 方法內部,如果 instance 為 None,證明第一個物件還沒有創建,那么就為第一個物件分配記憶體空間,如果 instance 不為 None,直接把類屬性中保存的第一個物件的參考直接回傳,這樣在外界無論呼叫多少次創建物件的方法,得到的物件的記憶體地址都是相同的,
下面我們運行一下程式,來看一下結果是不是能印證我們的說法:
<__main__.Printer object at 0x10f3223c8>
<__main__.Printer object at 0x10f3223c8>
上述輸出的兩個結果可以看出地址完全一樣,這說明 printer1 和 printer2 本質上是相同的一個物件,
16、什么是閉包?
我們都知道在數學中有閉包的概念,但此處我要說的閉包是計算機編程語言中的概念,它被廣泛的使用于函式式編程,
關于閉包的概念,官方的定義頗為嚴格,也很難理解,在《Python語言及其應用》一書中關于閉包的解釋我覺得比較好 -- 閉包是一個可以由另一個函式動態生成的函式,并且可以改變和存盤函式外創建的變數的值,乍一看,好像還是比較很難懂,下面我用一個簡單的例子來解釋一下:
>>> a = 1
>>> def fun():
... print(a)
...
>>> fun()
1
>>> def fun1():
... b = 1
...
>>> print(b)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'b' is not defined
毋庸置疑,第一段程式是可以運行成功的,a = 1 定義的變數在函式里可以被呼叫,但是反過來,第二段程式則出現了報錯,
在函式 fun() 里可以直接使用外面的 a = 1,但是在函式 fun1() 外面不能使用它里面所定義的 b = 1,如果我們根據作用域的關系來解釋,是沒有什么異議的,但是如果在某種特殊情況下,我們必須要在函式外面使用函式里面的變數,該怎么辦呢?
我們先來看下面的例子:
>>> def fun():
... a = 1
... def fun1():
... return a
... return fun1
...
>>> f = fun()
>>> print(f())
1
如果你看過昨天的文章,你一定覺得的很眼熟,上述的本質就是我們昨天所講的嵌套函式,
在函式 fun() 里面,有 a = 1 和 函式 fun1() ,它們兩個都在函式 fun() 的環境里面,但是它們兩個是互不干擾的,所以 a 相對于 fun1() 來說是自由變數,并且在函式 fun1() 中應用了這個自由變數 -- 這個 fun1() 就是我們所定義的閉包,
閉包實際上就是一個函式,但是這個函式要具有 1.定義在另外一個函式里面(嵌套函式);2.參考其所在環境的自由變數,
上述例子通過閉包在 fun() 執行完畢時,a = 1依然可以在 f() 中,即 fun1() 函式中存在,并沒有被識訓,所以 print(f()) 才得到了結果,
當我們在某些時候需要對事務做更高層次的抽象,用閉包會相當舒服,比如我們要寫一個二元一次函式,如果不使用閉包的話相信你可以輕而易舉的寫出來,下面讓我們來用閉包的方式完成這個一元二次方程:
>>> def fun(a,b,c):
... def para(x):
... return a*x**2 + b*x + c
... return para
...
>>> f = fun(1,2,3)
>>> print(f(2))
11
上面的函式中,f = fun(1,2,3) 定義了一個一元二次函式的函式物件,x^2 + 2x + 3,如果要計算 x = 2 ,該一元二次函式的值,只需要計算 f(2) 即可,這種寫法是不是看起來更簡潔一些,
17、什么是裝飾器?
「裝飾器」作為 Python 高級語言特性中的重要部分,是修改函式的一種超級便捷的方式,適當使用能夠有效提高代碼的可讀性和可維護性,非常的便利靈活,
「裝飾器」本質上就是一個函式,這個函式的特點是可以接受其它的函式當作它的引數,并將其替換成一個新的函式(即回傳給另一個函式),
可能現在這么看的話有點懵,為了深入理解「裝飾器」的原理,我們首先先要搞明白「什么是函式物件」,「什么是嵌套函式」,「什么是閉包」,關于這三個問題我在很久以前的文章中已經寫過了,你只需要點擊下面的鏈接去看就好了,這也是面試中常問的知識哦:
零基礎學習 Python 之函式物件
零基礎學習 Python 之嵌套函式
零基礎學習 Python 之閉包
裝飾器
搞明白上面的三個問題,其實簡單點來說就是告訴你:函式可以賦值給變數,函式可嵌套,函式物件可以作為另一個函式的引數,
首先我們來看一個例子,在這個例子中我們用到了前面列出來的所有知識:
def first(fun):
def second():
print('start')
fun()
print('end')
print fun.__name__
return second
def man():
print('i am a man()')
f = first(man)
f()
上述代碼的執行結果如下所示:
start
i am a man()
end
man
上面的程式中,這個就是 first 函式接收了 man 函式作為引數,并將 man 函式以一個新的函式進行替換,看到這你有沒有發現,這個和我在文章剛開始時所說的「裝飾器」的描述是一樣的,既然這樣的話,那我們就把上述的代碼改造成符合 Python 裝飾器的定義和用法的樣子,具體如下所示:
def first(func):
def second():
print('start')
func()
print('end')
print (func.__name__)
return second
@first
def man():
print('i am a man()')
man()
上面這段代碼和之前的代碼的作用一模一樣,區別在于之前的代碼直接“明目張膽”的使用 first 函式去封裝 man 函式,而上面這個是用了「語法糖」來封裝 man 函式,至于什么是語法糖,不用細去追究,你就知道是類似「@first」這種形式的東西就好了,
在上述代碼中「@frist」在 man 函式的上面,表示對 man 函式使用 first 裝飾器,「@」 是裝飾器的語法,「first」是裝飾器的名稱,
下面我們再來看一個復雜點的例子,用這個例子我們來更好的理解一下「裝飾器」的使用以及它作為 Python 語言高級特性被人津津樂道的部分:
def check_admin(username):
if username != 'admin':
raise Exception('This user do not have permission')
class Stack:
def __init__(self):
self.item = []
def push(self,username,item):
check_admin(username=username)
self.item.append(item)
def pop(self,username):
check_admin(username=username)
if not self.item:
raise Exception('NO elem in stack')
return self.item.pop()
上述實作了一個特殊的堆疊,特殊在多了檢查當前用戶是否為 admin 這步判斷,如果當前用戶不是 admin,則拋出例外,上面的代碼中將檢查當前用戶的身份寫成了一個獨立的函式 check_admin,在 push 和 pop 中只需要呼叫這個函式即可,這種方式增強了代碼的可讀性,減少了代碼冗余,希望大家在編程的時候可以具有這種意識,
下面我們來看看上述代碼用裝飾器來寫成的效果:
def check_admin(func):
def wrapper(*args, **kwargs):
if kwargs.get('username') != 'admin':
raise Exception('This user do not have permission')
return func(*args, **kwargs)
return wrapper
class Stack:
def __init__(self):
self.item = []
@check_admin
def push(self,username,item):
self.item.append(item)
@check_admin
def pop(self,username):
if not self.item:
raise Exception('NO elem in stack')
return self.item.pop()
對比一下使用「裝飾器」和不使用裝飾器的兩種寫法,乍一看,好像使用「裝飾器」以后代碼的行數更多了,但是你有沒有發現代碼看起來好像更容易理解了一些,在沒有裝飾器的時候,我們先看到的是 check_admin 這個函式,我們得先去想這個函式是干嘛的,然后看到的才是對堆疊的操作;而使用裝飾器的時候,我們上來看到的就是對堆疊的操作陳述句,至于 check_admin 完全不會干擾到我們對當前函式的理解,所以使用了裝飾器可讀性更好了一些,
就和我在之前的文章中所講的「生成器」那樣,雖然 Python 的高級語言特性好用,但也不能亂用,裝飾器的語法復雜,通過我們在上面縮寫的裝飾器就可以看出,它寫完以后是很難除錯的,并且使用「裝飾器」的程式的速度會比不使用裝飾器的程式更慢,所以還是要具體場景具體看待,
18、函式裝飾器有什么作用?
裝飾器本質上是一個 Python 函式,它可以在讓其他函式在不需要做任何代碼的變動的前提下增加額外的功能,裝飾器的回傳值也是一個函式的物件,它經常用于有切面需求的場景, 比如:插入日志、性能測驗、事務處理、快取、權限的校驗等場景 有了裝飾器就可以抽離出大量的與函式功能本身無關的雷同代碼并發并繼續使用,
19、生成器、迭代器的區別
迭代器是一個更抽象的概念,任何物件,如果它的類有 next 方法和 iter 方法回傳自己本身,對于 string、list、dict、tuple 等這類容器物件,使用 for 回圈遍歷是很方便的,在后臺 for 陳述句對容器物件呼叫 iter()函式,iter()是 python 的內置函式,iter()會回傳一個定義了 next()方法的迭代器物件,它在容器中逐個訪問容器內元素,next()也是 python 的內置函式,在沒有后續元素時,next()會拋出一個 StopIteration 例外,
生成器(Generator)是創建迭代器的簡單而強大的工具,它們寫起來就像是正規的函式,只是在需要回傳資料的時候使用 yield 陳述句,每次 next()被呼叫時,生成器會回傳它脫離的位置(它記憶陳述句最后一次執行的位置和所有的資料值)
區別:生成器能做到迭代器能做的所有事,而且因為自動創建了 iter()和 next()方法,生成器顯得特別簡潔,而且生成器也是高效的,使用生成器運算式取代串列決議可以同時節省記憶體,除了創建和保存程式狀態的自動方法,當發生器終結時,還會自動拋出 StopIteration 例外,
20、多執行緒互動,訪問資料,如果訪問到了就不訪問了,怎么避免重讀?
創建一個已訪問資料串列,用于存盤已經訪問過的資料,并加上互斥鎖,在多執行緒訪問資料的時候先查看資料是否已經在已訪問的串列中,若已存在就直接跳過,
21、Python 中 yield 的用法?
yield 就是保存當前程式執行狀態,你用 for 回圈的時候,每次取一個元素的時候就會計算一次,用yield 的函式叫 generator,和 iterator 一樣,它的好處是不用一次計算所有元素,而是用一次算一次,可以節省很多空間,generator每次計算需要上一次計算結果,所以用 yield,否則一 return,上次計算結果就沒了,
補充
在 Python 中,定義生成器必須要使用 yield 這個關鍵詞,yield 翻譯成中文有「生產」這方面的意思,在 Python 中,它作為一個關鍵詞,是生成器的標志,接下來我們來看一個例子:
>>> def f():
... yield 0
... yield 1
... yield 2
...
>>> f
<function f at 0x00000000004EC1E0>
上面是寫了一個很簡單的 f 函式,代碼塊是 3 個 yield 發起的陳述句,下面讓我們來看看如何使用它:
>>> fa = f()
>>> fa
<generator object f at 0x0000000001DF1660>
>>> type(fa)
<class 'generator'>
上述操作可以看出,我們呼叫函式得到了一個生成器(generator)物件,
>>> dir(fa)
['__class__', '__del__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__name__', '__ne__',
'__new__', '__next__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'gi_yieldfrom', 'send', 'throw']
在上面我們看到了 iter() 和 next(),雖然我們在函式體內沒有顯示的寫 iter() 和 next(),僅僅是寫了 yield,但它就已經是「迭代器」了,既然如此,那我們就可以進行如下操作:
>>> fa = f()
>>> fa.__next__()
0
>>> fa.__next__()
1
>>> fa.__next__()
2
>>> fa.__next__()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
從上面的簡單操作可以看出:含有 yield 關鍵詞的函式 f() 是一個生成器物件,這個生成器物件也是迭代器,所以就有了這樣的定義:把含有 yield 陳述句的函式稱為生成器,生成器是一種用普通函式語法定義的迭代器,
通過上面的例子可以看出,這個生成器(即迭代器)在定義的程序中并沒有昨天講的迭代器那樣寫 iter(),而是只用了 yield 陳述句,之后一個普普通通的函式就神奇的成了生成器,同樣也具備了迭代器的特性,
yield 陳述句的作用,就是在呼叫的時候回傳相應的值,下面我來逐行的解釋一下上面例子的運行程序:
1.fa = f():fa 參考生成器物件,
2.fa.next():生成器開始執行,遇到了第一個 yield,然后回傳后面的 0,并且掛起(即暫停執行),
3.fa.next():從上次暫停的位置開始,繼續向下執行,遇到第二個 yield,回傳后面的值 1,再掛起,
4.fa.next():重復上面的操作,
5.fa.next():從上次暫停的位置開始,繼續向下執行,但是后面已經沒有 yield 了,所以 next() 發生例外,
22、談下 python 的 GIL
GIL 是python的全域解釋器鎖,同一行程中假如有多個執行緒運行,一個執行緒在運行python程式的時候會霸占python解釋器(加了一把鎖即GIL),使該行程內的其他執行緒無法運行,等該執行緒運行完后其他執行緒才能運行,如果執行緒運行程序中遇到耗時操作,則解釋器鎖解開,使其他執行緒運行,所以在多執行緒中,執行緒的運行仍是有先后順序的,并不是同時進行,
多行程中因為每個行程都能被系統分配資源,相當于每個行程有了一個python解釋器,所以多行程可以實作多個行程的同時運行,缺點是行程系統資源開銷大
23、Python 中的可變物件和不可變物件?
不可變物件,該物件所指向的記憶體中的值不能被改變,當改變某個變數時候,由于其所指的值不能被改變,相當于把原來的值復制一份后再改變,這會開辟一個新的地址,變數再指向這個新的地址,
可變物件,該物件所指向的記憶體中的值可以被改變,變數(準確的說是參考)改變后,實際上是其所指的值直接發生改變,并沒有發生復制行為,也沒有開辟新的出地址,通俗點說就是原地改變,
Python 中,數值型別(int 和 float)、字串 str、元組 tuple 都是不可變型別,而串列 list、字典 dict、集合 set 是可變型別,
24、談談你對面向物件的理解?
面向物件是相對于面向程序而言的,
面向程序語言是一種基于功能分析的、以演算法為中心的程式設計方法
面向物件是一種基于結構分析的、以資料為中心的程式設計思想,在面向物件語言中有一個有很重要東西,叫做類,面向物件有三大特性:封裝、繼承、多型,
用 Python 匹配 HTML g tag 的時候,<.> 和 <.?> 有什么區別?
<.*>是貪婪匹配,會從第一個“<”開始匹配,直到最后一個“>”中間所有的字符都會匹配到,中間可能會包含“<>”,
<.*?>是非貪婪匹配,從第一個“<”開始往后,遇到第一個“>”結束匹配,這中間的字串都會匹配到,但是
不會有“<>”,
25、Python 中的行程與執行緒的使用場景?
多行程適合在 CPU 密集型操作(cpu 操作指令比較多,如位數多的浮點運算),
多執行緒適合在 IO 密集型操作(讀寫資料操作較多的,比如爬蟲),
26、解釋一下并行(parallel)和并發(concurrency)的區別?
并行(parallel)是指同一時刻,兩個或兩個以上時間同時發生,
并發(parallel)是指同一時間間隔(同一段時間),兩個或兩個以上時間同時發生,
27、如果一個程式需要進行大量的 IO 操作,應當使用并行還是并發?
并發
28、如果程式需要進行大量的邏輯運算操作,應當使用并行還是并發?
并行
29、在 Python 中可以實作并發的庫有哪些?
- 執行緒
- 行程
- 協程
- threading
30、Python 如何處理上傳檔案?
Python 中使用 GET 方法實作上傳檔案,下面就是用 Get 上傳檔案的例子,client 用來發 Get 請求,server 用來收請求,
請求端代碼
import requests #需要安裝 requests
with open('test.txt', 'rb') as f:
requests.get('http://服務器 IP 地址:埠', data=https://www.cnblogs.com/ivanlee717/archive/2023/03/26/f)
服務端代碼
var http = require('http');
var fs = require('fs');
var server = http.createServer(function(req, res){
var recDatahttps://www.cnblogs.com/ivanlee717/archive/2023/03/26/= "";
req.on('data', function(data){
recData += data;
})
req.on('end', function(data){
recData += data;
fs.writeFile('recData.txt', recData, function(err){
console.log('file received');
})
})
res.end('hello');
})
server.listen(埠);
31、請列舉你使用過的 Python 代碼檢測工具?
- 移動應用自動化測驗 Appium
- OpenStack 集成測驗 Tempest
- 自動化測驗框架 STAF
- 自動化測驗平臺 TestMaker
- JavaScript 記憶體泄露檢測工具 Leak Finder
- Python 的 Web 應用驗收測驗 Splinter
- 即插即用設備除錯工具 UPnP-Inspector
本文來自博客園,作者:ivanlee717,轉載請注明原文鏈接:https://www.cnblogs.com/ivanlee717/p/17258441.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/548231.html
標籤:其他
