面向對象編程(OOP)對於初學者來說可能是一個很難理解的概念。很多書籍都是從解釋OOP開始,討論三大術語:封裝、繼承和多態性,但是解釋的效果往往讓人失望。
本文希望讓程式設計師、數據科學家和python愛好者們更容易理解這個概念。我們去掉所有的行話,通過一些例子來做解說。
這篇文章是關於解釋OOP的外行方式。
什麼是對象和類
簡單地說,Python中的一切都是對象,類是對象的藍圖。所以當我們寫下:
a = 2b = "Hello!"我們正在創建一個int類的對象a,該對象的值為2,str類的對象b的值為「Hello!」(在默認情況下,用兩個引號來提供字符串)。
另外,我們常常無意識地使用到了類和對象的概念,例如在使用scikit-learn模型時,我們實際上是在使用一個類。
clf = RandomForestClassifier()clf.fit(X,y)這裡的分類器clf是一個對象,fit是在RandomForestClassifier類中定義的一個方法。
為什麼要使用類
為什麼我們經常使用類呢?我們可以用函數實現同樣的功能嗎?
可以。但是與函數相比,類為我們提供了更多功能。舉個例子,str類有很多為對象定義的函數,只需按tab鍵就可以訪問這些函數。我們也可以編寫這些函數,但是只按tab鍵不能使用自己編寫的函數。
類的這個屬性被稱為封裝。封裝是指將數據與操作該數據的方法捆綁在一起,或者限制對對象某些組件的直接訪問。
所以這裡str類綁定了數據(「Hello!」)以及所有對數據進行操作的方法。同樣,『RandomForestClassifier』類將所有的方法(fit、predict等)捆綁在一起。
除此之外,使用類還可以使代碼更加模塊化和易於維護。假設我們要創建一個像Scikit-Learn這樣的庫,就需要創建許多模型,每個模型都有一個fit和predict方法,如果不使用類,我們需要為每個模型提供許多函數,例如:
RFCFitRFCPredictSVCFitSVCPredictLRFitLRPredict and so on.這種代碼結構簡直是一場噩夢,因此Scikit Learn將每個模型定義為一個具有fit和predict方法的類。
創建類
現在我們已經了解了為什麼要使用類,以及它們為何如此重要。那麼如何開始使用它們呢?創建一個類非常簡單,下面是編寫任何類的樣板代碼:
class myClass: def __init__(self, a, b): self.a = a self.b = b def somefunc(self, arg1, arg2): # 這裡有些代碼這裡有很多新的關鍵字。主要是class、__init__和self。這些是什麼呢?
假設你在一家有很多帳戶的銀行工作。我們可以創建一個名為account的類,用於處理任何帳戶。例如,下面我創建了一個基本的帳戶,它為用戶存儲數據,即帳戶名和餘額,它還為我們提供了兩種銀行存款/取款的方法。請通讀一遍以下代碼,它遵循與上面代碼相同的結構。
class Account: def __init__(self, account_name, balance=0): self.account_name = account_name self.balance = balance def deposit(self, amount): self.balance += amount def withdraw(self,amount): if amount <= self.balance: self.balance -= amount else: print("Cannot Withdraw amounts as no funds!!!")我們使用以下方法創建一個名為Rahul、金額為100的帳戶:
myAccount = Account("Rahul",100)使用以下方法訪問此帳戶的數據:
但是,如何將這些屬性balance和account_name分別設置為100和「Rahul」?我們從來沒有調用過__init__方法,為什麼對象會獲得這些屬性?答案是,只要我們創建對象,它就會運行。因此,當我們創建myAccount時,它會自動運行函數__init__。
現在讓我們試著存一些錢到我們的帳戶裡:
我們的餘額上升到200英鎊。你有沒有注意到,函數deposit需要兩個參數,即self和amount,但我們只提供了一個參數,而且仍然有效。
那麼這個self是什麼?下面我調用屬於類account的同一個函數deposit,並向它提供myAccount對象和amount。現在函數需要兩個參數。
我們的帳戶餘額如預期增加了100。所以這是我們調用的同一個函數。只有self和myAccount是完全相同的對象時,才會發生這種情況。Python為函數調用提供與參數self相同的對象myAccount。這就是為什麼self.balance在函數定義中真正指的是myAccount.balance.
但是仍然存在一些問題
我們知道如何創建類,但是還有一個重要的問題我還沒有提到。
假設你正在與蘋果iPhone部門合作,且必須為每種iPhone型號創建一個不同的類。對於這個例子,假設我們的iPhone的第一個版本目前只做一件事——打電話並存儲。可以這樣寫:
class iPhone: def __init__(self, memory, user_id): self.memory = memory self.mobile_id = user_id def call(self, contactNum): # 這裡有些實現現在,蘋果計劃推出iPhone1,這款iPhone機型引入了一項新功能——拍照功能。一種方法是複製粘貼上述代碼並創建一個新的類iPhone1,如下所示:
class iPhone1: def __init__(self, memory, user_id): self.memory = memory self.mobile_id = user_id self.pics = [] def call(self, contactNum): # 這裡有些實現 def click_pic(self): # 這裡有些實現 pic_taken = ... self.pics.append(pic_taken)但正如你所看到的,這是大量不必要的代碼重複(上面用粗體顯示),Python有一個消除代碼重複的解決方案。編寫iPhone1類的一個好方法是:
Class iPhone1(iPhone): def __init__(self,memory,user_id): super().__init__(memory,user_id) self.pics = [] def click_pic(self): # 這裡有些實現 pic_taken = ... self.pics.append(pic_taken)這就是繼承的概念,繼承是將一個對象或類基於另一個保留類似實現的對象或類的機制。簡單地說,iPhone1現在可以訪問類iPhone中定義的所有變量和方法。
在本例中,我們不必進行任何代碼複製,因為我們已經從父類iPhone繼承(獲取)了所有方法。因此,我們不必再次定義調用函數。另外,我們不使用super在函數中設置mobile_uid和內存。
super().__init__(memory,user_id)是什麼?
在現實生活中,你的初始函數不是這些漂亮的函數。你將需要在類中定義許多變量/屬性,並且複製並粘貼子類(這裡是iphone1),會很麻煩。因此存在super(),這裡super().__init__()實際上是調用父iPhone類的__init__方法。因此當類iPhone1的__init__函數運行時,它會自動使用父類的__init__函數設置類的memory和user_id。
在ML/DS/DL中的哪裡可以看到?下面我們創建PyTorch模型,此模型繼承了nn.Module類,並使用super調用該類的__init__函數。
class myNeuralNet(nn.Module): def __init__(self): super().__init__() # 在這裡定義所有層 self.lin1 = nn.Linear(784, 30) self.lin2 = nn.Linear(30, 10) def forward(self, x): # 在此處連接層輸出以定義前向傳播 x = self.lin1(x) x = self.lin2(x) return x那麼多態又是什麼?看下面的類:
import mathclass Shape: def __init__(self, name): self.name = name def area(self): pass def getName(self): return self.nameclass Rectangle(Shape): def __init__(self, name, length, breadth): super().__init__(name) self.length = length self.breadth = breadth def area(self): return self.length*self.breadthclass Square(Rectangle): def __init__(self, name, side): super().__init__(name,side,side)class Circle(Shape): def __init__(self, name, radius): super().__init__(name) self.radius = radius def area(self): return pi*self.radius**2這裡我們有基類Shape和其他派生類-Rectangle和Circle。另外,看看我們如何在Square類中使用多個級別的繼承,Square類是從Rectangle派生的,而Rectangle又是從Shape派生的。每個類都有一個名為area的函數,它是根據形狀定義的。
因此,通過Python中的多態性,一個同名函數可以執行多個任務。事實上,這就是多態性的字面意思:「具有多種形式的東西」。所以這裡我們的函數area有多種形式。
多態性與Python一起工作的另一種方式是使用isinstance方法。因此,使用上面的類,如果我們這樣做:
對象mySquare的實例類型是方形、矩形和形狀,因此對象是多態的,有很多好的特性。例如,我們可以創建一個與Shape對象一起工作的函數,它將通過使用多態性完全處理任何派生類(Square、Circle、Rectangle等)。
更多信息
為什麼有些函數名或屬性名以單下劃線和雙下劃線開頭?有時我們想讓類中的屬性和函數私有化,而不允許用戶看到它們,這是封裝的一部分,我們希望「限制對對象某些組件的直接訪問」。例如,假設我們不想讓用戶看到我們的iPhone創建後的memory(RAM)。在這種情況下,我們使用變量名中的下劃線創建屬性。
因此,當我們以下面的方式創建iPhone類時,你將無法訪問你的memory或iphone私有函數,因為該屬性現在使用_。
但你仍然可以使用(儘管不建議使用)更改變量值。
你還可以使用私有函數myphone._privatefunc()。如果要避免這種情況,可以在變量名前面使用雙下劃線。例如,在調用print(myphone.__memory)下面拋出一個錯誤。此外,你無法使用myphone更改對象的內部數據myphone.__memory = 1。
但是,正如你所見,你可以在類定義中的函數setMemory中訪問和修改self.__memory。
結論
希望本文對你理解類很有用。總結一下在這篇文章中我們學習的OOP和創建類以及OOP的各種基礎知識:
封裝:對象包含自身的所有數據;繼承:創建一個類層次結構,其中父類的方法傳遞給子類;多態:函數有多種形式,或者對象可能有多種類型。我們以一個練習結束本文,讓你去實現:創建一個類,使你可以使用體積和曲面面積管理三維對象(球體和立方體)。基本樣板代碼如下所示:
import mathclass Shape3d: def __init__(self, name): self.name = name def surfaceArea(self): pass def volume(self): pass def getName(self): return self.nameclass Cuboid(): passclass Cube(): passclass Sphere(): pass如果你想了解更多關於Python的知識,可以參考密西根大學(universityofmichigan)的一門關於學習中級Python的優秀課程:https://bit.ly/2XshreA。