PYTHON 補充知識點 面向對象三大特性:封裝,繼承和多態。 繼承的意義在於兩點: 第一,子類如若繼承自父類,則自動獲取父類所有功能,並且可以在此基礎上再去添加自己獨有的功能。 第二,當子類和父類存在同樣方法時,子類的方法覆寫了父類的代碼,於是子類對象執行的將是子類的方法,即“多態”。 多態到底有 ...
PYTHON 補充知識點
面向對象三大特性:封裝,繼承和多態。
繼承的意義在於兩點:
第一,子類如若繼承自父類,則自動獲取父類所有功能,並且可以在此基礎上再去添加自己獨有的功能。
第二,當子類和父類存在同樣方法時,子類的方法覆寫了父類的代碼,於是子類對象執行的將是子類的方法,即“多態”。
多態到底有什麼用呢?在繼承關係里,子類對象的數據類型也可以被當作父類的數據類型,但是反過來不行。那麼當我們編寫一個函數,參數需要傳入某一個類型的對象時,我們根本不需要關心這個對象具體是哪個子類,統統按照基類來處理,而具體調用誰的重載函數,再具體根據對象確切類型來決定:
class Animal():
def get_name(self):
print("animal")
class Dog(Animal):
def get_name(self):
print("dog")
class Cat(Animal):
def get_name(self):
print("cat")
def run(animal): # 待執行的函數。
animal.get_name()
cat = Cat()
run(cat)
dog = Dog()
run(dog)
>>>cat
>>>dog
當我們再新增子類rabbit,wolf等時,根本不需要改變run函數。這就是著名的開閉原則:即對擴展開放(增加子類),對修改關閉(不需要改變依賴父類的函數參數)
多態存在的三個必要條件:繼承,重寫,父類引用指向子類對象。
對於靜態語言來說,需要傳入參數類型,那麼實參則必須是該類型或者子類才可以。而對於動態語言(比如python)來說,甚至不需要非得如此,只需要這個類型裡面有個get_name方法即可。這就是動態語言的鴨子特性:只要走路像鴨子,就認為它是鴨子!”這樣的話,多態的實現甚至可以不要求繼承。
裝飾器的使用
裝飾器的本質就是一個Python函數,它可以在其他函數不做任何代碼變化的前提下增強額外功能。裝飾器也返回一個函數對象。當我們想給很多函數額外添加功能,給每一個添加太麻煩時,就可以使用裝飾器。
比如有幾個函數想給它們添加一個“輸出函數名稱的功能”:
def test():
print("this is test")
print(test.__name__)
test()
但是如果還有其他函數也需要這個功能,那麼就要每個都添加一個print,這樣代碼非常冗雜。這時候就可以使用裝飾器。
def log(func):
def wrapper(*args,**kwargs):
print(func.__name__)
return func(*args,**kwargs)
return wrapper
@log # 裝飾器
def test():
print("this is test")
test()
當把@log加到test的定義處時相當於執行了:
test = log(test)
於是乎再執行test()其實就wrapper(),可以得到原本函數的功能以及新添加的功能,非常方便。
還可以定義一個帶參數的裝飾器,這給裝飾器帶來了更大的靈活性:
def log(text):
def decorator(func):
def wrapper(*args, **kwargs):
print(text)
return func(*args, **kwargs)
return wrapper
return decorator
@log("ahahahahaha") # 帶參數的裝飾器
def test():
print("this is test")
這樣就相當於執行了:
test = log("ahahahahaha")(test)
還有一點,雖然裝飾器非常方便,但是這樣使用會導致test的原信息丟失(比如上面的代碼test.__name__
會變成"wrapper"
而不是"test"
)。這樣的話需要導入一個functool包,然後在wrapper函數前面加上一句@functools.wraps(func)
就可以了:
def log(text):
def decorator(func):
@functools.wraps(func) # 加上一句
def wrapper(*args, **kwargs):
print(text)
return func(*args, **kwargs)
return wrapper
return decorator
@log("ahahahahaha")
def test():
print("this is test")
最後,如果一個函數使用多個裝飾器進行裝飾的話,則需要根據邏輯確定裝飾順序。
Python中的多繼承與MRO
Python是允許多繼承的(即一個子類有多個父類)。多重繼承的作用主要在於給子類拓展功能:比如dog和cat都是mamal類的子類,它們擁有mamal的功能,如果想給它們添加“runnable”的功能,只需要再繼承一個類runnable即可。這個額外功能的類一般都會加一個“Mixin”尾碼作為區分,表示這個類是拓展功能用的:
class Dog(mamal,RunableMixIn,CarnivorousMixIn)
pass
使用Mixin的好處在於不用設計複雜的繼承關係即可拓展類的功能。
多繼承會產生一個問題:如果同時有多個父類都實現了一個方法,那麼子類應該調用誰的方法呢?這就涉及到MRO(方法解析順序)。
在Python3之後,MRO使用的都是C3演算法(不區分經典類和新式類,全部都是新式類)。C3演算法首先使用深度優先演算法,先左後右,得到搜索路徑後,只保留“好的結點”。好的結點指的是:該節點之後沒有任何節點繼承自這個節點。
比如說下圖:
按照深度優先,從左至右的順序應該是F,A,Y,X,B,Y,X。其中第一個Y和第一個X後面B也繼承自Y,X,於是把它們去掉,最終路徑是:F,A,B,Y,X
在MRO中,使用super方法也要註意:假設有類A,B,C,A繼承自B和C。如果B中使用了Super重寫了一個方法,這個方法會在C中去尋找(儘管C並不是B的父類):
class B:
def log(self):
print("log B")
def go(self):
print("go B")
super(B, self).go()
self.log()
class C:
def go(self):
print("go C")
class A(B, C):
def log(self):
print("log A")
super(A,self).log()
if __name__ == "__main__":
a = A()
# 1.a對象執行go,先找自己有沒有go,自己沒有按照MRO去找B
# 2.B中找到了go並執行,super順位查找到c的go,執行。
# 3.然後執行self.log,因為self此時是A對象,於是首先找到A中的log函數執行
# 4.再次執行super函數,此時順位查找到B,執行B.log
a.go()
"結果"
go B
go C
log A
log B
進程與線程
以前的單核CPU也可以進行多任務,方法是任務1一會兒,任務2一會兒,任務3一會兒......因為CPU速度很快,所以看起來像是執行了多任務,其實就是交替執行任務。
真正的並行需要在多核CPU上進行。但實際開發中的任務數量一定比核數量要多,所以每個核依舊是在交替執行任務。
進程:對於操作系統來說,一個任務就是一個“進程”,打開一個文件,程式等都是打開一個“進程”。
當一個程式在硬碟上運行起來,會在記憶體中形成一個獨立的記憶體體,這個記憶體體中有自己獨立的地址空間,有自己的堆。操作系統會以進程為最小的資源分配單位。
線程:線程是操作系統調度執行的最小單位。有時一個進程會有多個子任務:比如一個word文件它同時進行打字、編排、拼寫檢查等任務,這些子任務被稱為“線程”。每個進程至少要有一個線程,和進程一樣,真正的多線程只能依靠多核CPU來實現。
進程與線程的區別:進程是擁有資源的獨立單位,線程不擁有系統資源,但可以訪問自己所隸屬的進程的資源。在系統開銷方面,進程的創建與撤銷都需要系統都要為之分配和回收資源,它的開銷要比線程大。
多進程的優點是穩定性高,因為一個進程掛了,其他進程還可以繼續工作。(主進程不能掛,但是由於主進程負責分配任務,所以掛的概率很低)。而線程呢,由於多線程共用一個進程的記憶體,所以一個線程掛了全體一起掛。並且多線程在操作共用資源時容易出錯(死鎖等問題)
一個線程只能屬於一個進程,而一個進程可以有多個線程,但至少有一個線程;
資源分配給進程,同一進程的所有線程共用該進程的所有資源;
處理機分給線程,即真正在處理機上運行的是線程;
線程在執行過程中,需要協作同步。不同進程的線程間要利用消息通信的辦法實現同步
無論是多進程還是多線程,一旦任務數量多了效率都會急劇下降,因為切換任務不僅僅是直接去執行就好了,這中間還需要保存現場、創建新環境等一系列操作,如果任務數量一多會有很多時間浪費在這些準備工作上面。
實現多任務主要有三種辦法:
- 多個進程,每個進程僅有一個線程
- 一個進程,進程中有多個線程
- 多進程多線程(過於複雜,不推薦)
同時執行多個任務的時候,任務之間是要互相協調的,有時任務2必須要等任務1結束之後去執行,有時任務3和4不能同時執行......所以多進程和多線程編寫架構要相對更複雜。
任務類型主要分為IO密集型和計算密集型”。計算密集型任務主要是需要大量計算,比如計算圓周率,對視頻解碼等。計算密集型任務主要消耗的是CPU資源(不適合Python這種運算效率低的腳本語言編寫代碼,適合C語言);而IO密集型任務主要是IO操作,CPU消耗很少,大部分時間都消耗在等待IO。(適合開發效率高的腳本語言)
對於計算密集型任務來說,如果多進程多線程任務遠遠大於核數,那麼效率反而會急劇下降;而對於IO密集型任務來說,多進程多線程可能會好一些,但還是不夠好。
利用非同步IO可以實現單進程單線程執行多任務。利用非同步IO可以顯著提升操作系統的調度效率。對應到Python語言,非同步IO被稱為協程。
協程:比線程更加輕量級的“微線程”,協程不被操作系統內核管理,而是由用戶自己執行。這樣的好處就是極大的提升了調度效率,不會像線程那樣切換浪費資源。
子程式在所有語言里都是層級調用(棧實現),即A調用B,B又調用C,C完了B完了最後A才能完。一個線程就是調用一個子程式。而協程不同,用戶可以任意在執行A時暫停去執行B,過一會兒再來執行A。
協程的優勢在於:由於是程式自己控制子程式切換,所以相比線程可以節省大量不必要的切換,對於IO密集型任務來說協程更優。而且不需要線程的鎖機制。
協程是在單線程上執行的,那麼如何利用多核CPU呢?方法就是多進程+協程。