https://m.toutiao.com/is/dmnvNk6/?=Python面向?qū)ο缶幊?nbsp;
面向?qū)ο缶幊?/span>
到目前為止,我們已經(jīng)使用函數(shù)、模塊來操作我們的數(shù)據(jù)。這叫做 面向過程 的方式進(jìn)行編程。然而還有另外一種方式來組織你的程序:把數(shù)據(jù)和函數(shù)結(jié)合起來,并將其置入一種叫做對象的東西。這就叫做 面向?qū)ο?編程范式。在大多數(shù)情況下,你可以使用面向過程的編程,但是當(dāng)寫大型程序或者遇到了一些更加適合這種方法的時候,你可以使用基于對象的編程技術(shù)。
類和對象是面向?qū)ο缶幊痰膬蓚€主要概念。一個類創(chuàng)造了一種新的 類型 ,而對象就是類的實(shí)例。一種觀點(diǎn)是:你可以把 int 類型的變量翻譯為存儲著整型的變量,這個變量是 int 類的一個實(shí)例。
對靜態(tài)語言編程者的提示
注意整型被看待為一個int類的對象。這一點(diǎn)與 C++ 和 Java ( 早于 1.5 版本)不同。在這些語言中,整型被看成一種基本數(shù)據(jù)類型。
C# 和 Java 1.5 的使用者會發(fā)現(xiàn)這個非常類似裝箱與拆箱的概念。
對象能夠使用原始變量( 屬于 對象)存儲數(shù)據(jù)。屬于對象或者類的變量被稱作域。一個對象可以通過使用 屬于 類的函數(shù)實(shí)現(xiàn)一定的功能。這些函數(shù)被稱作類的方法。這個術(shù)語是非常重要的,因?yàn)樗鼛椭覀儏^(qū)分獨(dú)立的函數(shù)和變量及屬于對象或者類的變量和函數(shù)??偟膩碚f,域和方法可以被看作類的屬性。
域有兩種類型 - 他們可以屬于每一個類的實(shí)例(也就是對象),也可以屬于類本身。他們被分別稱作實(shí)例變量和類的變量。
使用 class 關(guān)鍵字來創(chuàng)建一個類。這個類的域和方法被列在一個代碼塊中。
關(guān)于 self
類的方法與普通的函數(shù)相比只有一個區(qū)別 - 他們在入口參數(shù)表的開頭必須有一個額外的形式參數(shù),但是當(dāng)你調(diào)用這個方法的時候,你不會為這個參數(shù)賦予任何一個值,Python 會提供給它。這個特別的參數(shù)指向?qū)ο?本身 ,約定它的名字叫做 self .
盡管你可以給這個參數(shù)起任何一個名字,但是這里 強(qiáng)烈推薦 使用 self —— 任何其他的名字絕對會引起不滿。使用一個標(biāo)準(zhǔn)的名字有許多優(yōu)點(diǎn) - 如果你使用 self ,任何程序的渲染器將會自動的識別它,甚至一些特定的 IDEs (Integrated Development Environments) 可以給你提供額外的幫助。
給 C++/Java/C# 使用者的提示
Python 中的 self 和 C++ 中的 this 指針以及 Java 和 C# 中的 this 引用等價。
你一定好奇 Python 是如何給 self 賦值的,以及為什么你不必給它賦值。一個例子將會把這些問題說明清楚。假設(shè)你有一個類叫做 MyClass 以及這個類的一個對象叫做 myobject 。當(dāng)你需要這樣調(diào)用這個對象的方法的時候:myobject.method(arg1, arg2) ,這個語句會被 Python 自動的轉(zhuǎn)換成 MyClass.method(myobject, arg1, arg2) 這樣的形式 —— 這就是 self 特殊的地方。
這也意味著如果你有一個不聲明任何形式參數(shù)的方法,卻仍然有一個入口參數(shù) —— self 。
類
最簡單的類可能如下代碼所示(保存為文件 oop_simplestclass.py )。
輸出:
這是如何工作的
我們使用 class 語句和類名創(chuàng)建了一個類。在這之后跟著一個語句塊形成了類的主體。在這個例子中,我們使用 pass 語句聲明了一個空的語句塊。
之后,我們使用類的名字和一對括號創(chuàng)建了一個類的對象(實(shí)例)。(我們將在下一節(jié)學(xué)習(xí) 更多的例子。我們通過簡單地打印變量 p 的方法確認(rèn)這個變量類型。結(jié)果證明這是 __main__ 模塊中 Person類的一個對象。
注意這個對象在內(nèi)存中的地址也被顯示出來。這個地址可能在你的電腦上有一個不同的值,這是由于 Python 只要找到空閑的內(nèi)存空間就會在此處存放這個對象。
方法
我們已經(jīng)討論了類和對象可以擁有一些方法正如普通的函數(shù)。但是這些方法有一個額外的 self 變量?,F(xiàn)在我們來看一個例子(保存為文件 oop_method.py )。
輸出:
這是如何工作的
現(xiàn)在我們具體的看一下 self 是如何工作的。注意到在 say_hi 方法中沒有取得任何一個參數(shù),卻在方法定義的時候仍然有一個 self 參數(shù)。
對 Python 類來說,許多方法名有特別的重要性?,F(xiàn)在,我們來考察一個重要的 __init__ 方法。
例子(保存為文件 oop_init.py ):
輸出:
這是如何工作的
這里,我們定義了 __init__ 方法。這個方法除了通常的 self 變量之外,還有一個參數(shù) name 。 這里我們創(chuàng)建了一個新的也叫做 name 的域。注意這里有兩個不同的變量卻都被叫做 'name' 。這是沒有問題的,因?yàn)閹c(diǎn)的標(biāo)記 self.name 表示有一個叫做 'name' 的域是這個類的一部分,而另外一個 name 是一個局部變量。這里我們顯式地指出使用哪個變量,因此沒有任何沖突。
當(dāng)新建一個新的 Person 類的實(shí)例 p 的時候,我們通過調(diào)用類名的方式來創(chuàng)建這個新的實(shí)例,在緊跟著的括號中填入初始化參數(shù): p = Person('Swaroop') 。
我們沒有顯式的調(diào)用 __init__ 這個方法,這是這個方法特殊之處。
正如 say_hi 方法所示的,現(xiàn)在我們在我們的方法之中可以使用 self.name 這個域了。
類和對象中的變量
我們已經(jīng)討論了關(guān)于類和對象中函數(shù)的部分(也就是方法),現(xiàn)在讓我們來學(xué)習(xí)關(guān)于數(shù)據(jù)的部分。數(shù)據(jù)的部分(也就是域)并不是什么特別的東西,只是一些 綁定 到類或者對象命名空間的普通的變量。這意味著這些變量只在和這些類和對象有關(guān)的上下文中有效。這就是為什么他們被稱作 命名空間 。
有兩種類型的 域 -- 類變量和對象變量。這是通過他們是 屬于 類還是 屬于 對象這一點(diǎn)來區(qū)分的。
類變量是共享的 -- 他們可以通過所有這個類的對象來訪問。類變量只有一份拷貝,這意味著當(dāng)一個對象改變了一個類變量的時候,改變將發(fā)生在所有這個類的對象中。
對象變量屬于每一個對象(實(shí)例)自身。在這種情況下,每一個對象都有屬于它自己的域(在不同的對象中,這些變量不是共享的,它們也并不相關(guān),僅僅是名稱相同。一個例子將會讓這些便于理解(保存到文件 oop_objvar.py ):
輸出:
這是如何工作的
這將是一個非常長的解釋,但卻能夠幫助我們很自然地認(rèn)識類變量和對象變量。這里, population 屬于 Robot 類,因此是一個類變量。 name 變量屬于每一個個體(使用 self 來指向)因此是一個對象變量。
由此,我們可以推測出 population 類變量應(yīng)當(dāng)用 Robot.population 來訪問,而非 self.population ;可以推測在對象的方法之中,對象變量 name 應(yīng)當(dāng)使用 self.name 來訪問。請記住這個類變量和對象變量之間這一個簡單的區(qū)別。也請記住一個與類對象同名的對象變量將會把這個類變量屏蔽。
除了 Robot.population 之外,我們還可以通過 self.__class__.population 來訪問這個類對象,因?yàn)槊恳粋€對象都通過 self.__class__ 屬性指向自己的類。
how_many 實(shí)際上是一個屬于類的方法,而非屬于對象的方法,這意味著我們可以使用 classmethod 或者 staticmethod 來定義它。區(qū)別在于我們是否需要知道我們是哪個類中的一部分。因此既然我們想要聲明一個類變量,讓我們使用 classmethod 吧。
我們使用一個裝飾器 來標(biāo)記 how_many 方法,并將其作為一個類方法。
裝飾器可以被想象成為一個快捷的方式去調(diào)用一個包裹函數(shù)(一個包裹著另外一個函數(shù)的函數(shù),因此可以在內(nèi)部函數(shù)調(diào)用之前及之后做一些事情),因此使用 @classmethod 裝飾器和如下調(diào)用等價:
注意到 __init__ 方法被用作初始化一個 Robot 實(shí)例,并給這個機(jī)器人取一個名字。在這個方法之中,我們每獲得一個新的機(jī)器人,就使得 population 增加 1 。此外,注意到 self.name 變量的值會因?qū)ο蟮牟煌煌?,這展現(xiàn)了對象變量的自然之處。
請記住,你 只能 通過 self 來指向同一個對象的變量和方法。這被稱為 屬性引用 (attribute reference) 。
在這個程序中,我們還可以看到 文檔字符串 (docstrings) 在類和方法值中的使用。運(yùn)行時,我們可以通過 Robot.__doc__ 來訪問類的文檔字符串以及通過 Robot.say_hi.__doc__ 來訪問方法的文檔字符串。
在 die 方法之中,我們單純的使得 Robot.population 減少 1 。
所有的類成員都是公共的。只有一種情況除外:如果你使用 雙下劃線前綴 (例如 __privatevar )時,Python 會使用命名粉碎規(guī)則 (name-mangling) 作用于這個變量,并使其變?yōu)樗接凶兞俊?/p>
因此,結(jié)論就是所有的僅在類和對象之中的變量應(yīng)當(dāng)使用一個下劃線來開頭,所有其他的命名都是公共的,可以被其他類和對象訪問。請記住這只是約定而非 Python 強(qiáng)制規(guī)定(使用雙下劃線除外)。
給 C++/Java/C# 使用者的提示
在 Python 中,所有的成員(包括數(shù)據(jù)成員)都是 公共的 ,所有的方法都是 虛擬的。
繼承
面向?qū)ο缶幊痰闹饕獌?yōu)勢之一就是代碼的重用,一種獲得代碼重用的主要方式就是繼承體系。繼承可以被想象成為類之間的一種類型和子類型的關(guān)系的實(shí)現(xiàn)。
假設(shè)你想要寫一個程序來跟蹤一所大學(xué)之中的老師和同學(xué)。他們有一些共同的特征,比如名字、年齡、地址等。他們還有一些獨(dú)有的特征,比如對老師來說有薪水、課程、離開等,對學(xué)生來說有成績和學(xué)費(fèi)。
你當(dāng)然可以為這兩種類型構(gòu)建兩種獨(dú)立的類來驅(qū)動程序。但是當(dāng)需要添加一個共同的屬性的時候,意味著需要在這兩個獨(dú)立的類中同時添加。這很快就會變得非常笨拙。
一個更好的辦法就是構(gòu)造一個共同的類 SchoolMember ,然后在讓老師和學(xué)生分別 繼承 這個類。換句話說,他們都是這個類型(類)的子類型,之后我們也可以為這些子類型添加獨(dú)有的屬性。
這種方式有許多的好處。如果我們想要添加或者改變 SchoolMember 類中的功能,這將也會自動地反映在子類型之中。舉個例子,你可以通過簡單的修改 SchoolMember 類的方式來為學(xué)生和老師添加新的 ID 卡的域。然而,一個子類型之中的變化不能夠反映在其他子類型之中。另外一個好處就是你可以使用一個 SchoolMember 對象來指向任意一個老師或者學(xué)生的對象。這將會在某些情況下非常有用,比如統(tǒng)計(jì)學(xué)校中人的總數(shù)。這被稱作多態(tài):如果有父類型的話,子類型可以在任何一種情況下被替代。也就是說,一個子類型的對象可以被當(dāng)作父類型的實(shí)例。
此外,注意到我們重用了父類的代碼。不需要在不同的類中重復(fù)這些代碼,只要我們不使用獨(dú)立的類的方式來實(shí)現(xiàn)。
我們來看看這個例子(保存為 oop_subclass.py ):
輸出:
這是如何工作的
為了使用繼承,我們在類名之后的括號中指明父類的類名。(舉個例子, class Teacher(SchoolMember) )。之后我們可以看到在 __init__ 方法中,通過 self 變量顯式的調(diào)用了父類的 __init__ 方法來初始化子類對象中屬于父類的部分。這非常重要,請記住 -- 既然我們在 Teacher 和 Student 子類中定義了 __init__ 方法,Python 不會自動的調(diào)用父類 SchoolMember 中的構(gòu)造方法,你必須顯式的調(diào)用。
相反的,如果我們不定義子類的 __init__ 方法,Python 將會自動地調(diào)用父類中的構(gòu)造方法。
當(dāng)我們想把 Teacher 或者 Student 的實(shí)例當(dāng)作 SchoolMember 的實(shí)例,并且想調(diào)用 tell 方法的時候,只需要簡單的輸入 Teacher.tell 或者 Student.tell即可。我們在每個子類之中定義了另一個新的 tell 方法 ( 父類 SchoolMember 的 tell 方法作為其中的一部分)來定制子類的功能。因?yàn)槲覀円呀?jīng)做了這樣的工作,當(dāng)我們調(diào)用 Teacher.tell 的時候, Python 將會使用子類中 tell 方法,而非父類的。然而,如果我們沒有在子類中定義 tell 方法,Python 將使用父類中的方法。Python 總是首先在實(shí)際的子類中尋找方法,如果不存在,將會按照子類聲明語句中的順序,依次在父類之中尋找(在這里我們只有一個父類,但是你可以聲明多個父類)。
注意術(shù)語 -- 如果有超過一個類被列在繼承元組之中,這就叫做多重繼承。
end 參數(shù)在父類 tell() 方法中調(diào)用的 print 函數(shù)中被使用,以使得打印完一句話之后,下一次打印緊接在第一句話之后,而不換行。這個技巧可以使得 print 函數(shù)在輸出結(jié)束時不打印 \n 符號(換行)。
我們已經(jīng)探索關(guān)于類和對象的方方面面以及相關(guān)的術(shù)語。我們也已經(jīng)領(lǐng)略到了面向?qū)ο缶幊痰膬?yōu)勢和陷阱。 Python 是高度面向?qū)ο蟮?,因此仔?xì)地理解這些內(nèi)容將會長久地給予你極大的幫助。
關(guān)注我!下一章我們將學(xué)習(xí) Python 是如何處理輸入/輸出以及如何操作文件。