本文希望讓程式設計師、資料科學家和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的各種基礎知識:
封裝:物件包含自身的所有資料;
繼承:建立一個類層次結構,其中父類的方法傳遞給子類;
多型:函式有多種形式,或者物件可能有多種型別。
我們以一個練習結束本文,讓你去實現:建立一個類,使你可以使用體積和曲面面積管理三維物件(球體和立方體)。基本樣板程式碼如下所示: