python物件導向入門(1):從程式碼複用開始

駿馬金龍發表於2018-11-15

本文從程式碼複用的角度一步一步演示如何從python普通程式碼進化到物件導向,並通過程式碼去解釋一些物件導向的理論。所以,本文前面的內容都是非物件導向的語法實現方式,只有在最結尾才給出了物件導向的簡單語法介紹。各位道兄不妨一看,如果留下點筆墨指導,本人感激不盡。

最初程式碼

3種動物牛Cow、羊Sheep、馬Horse發出的聲音各不相同,於是在同一個目錄下建立三個模組檔案:

$ tree .
.
|-- cow.py
|-- horse.py
`-- sheep.py

三個模組檔案的內容都只定義了各自的speak()函式:

# cow.py
def speak():
    print("a cow goes moooo!")

# sheep.py
def speak():
    print("a sheep goes baaaah!")

# horse.py
def speak():
    print("a horse goes neigh!")

然後當前目錄下在建立一個程式檔案main.py,匯入這三個模組檔案,分別呼叫這三種動物的speak()函式,它們將發出不同聲音:

# main.py
import cow,sheep,horse

cow.speak()
sheep.speak()
horse.speak()

讓程式碼更具共性的兩種基本方法

上面的cow.py、sheep.py和horse.py中,都是speak()函式,不同的是函式內容,確切地說是函式內容中print()輸出的部分不同,它們輸出的結構是a 動物名 goes 叫聲!。於是為了讓程式碼更具共性,或者說複用性更高,可以將各模組檔案中的動物名和叫聲都變得通用化。

目前來說,有兩種最基本的方式可以讓一段程式碼變得更共性、共通用化:使用引數或變數、使用額外的輔助函式。當然,除此之外還有更多的方法,但目前來說這兩種是最基本的,也是最容易理解的。

使用引數(變數)讓程式碼更具共性

首先讓動物名變得共性化。可以讓speak()中的動物名使用一個引數來替代。例如名為self的引數變數(之所以使用self,是因為在物件導向中它有特殊含義,後文解釋),於是修改這三個模組檔案:

# cow.py
def speak(self):
    print("a %s goes moooo!" % (self))

# sheep.py
def speak(self):
    print("a %s goes baaaah!" % (self))

# horse.py
def speak(self):
    print("a %s goes neigh!" %(self))

它們現在在動物名上和引數名上已經完全相同,需要呼叫它們時,只需在函式呼叫處為他們傳遞不同的動物名即可。例如,在main.py中:

import cow,sheep,horse

cow.speak("cow")
sheep.speak("sheep")
horse.speak("horse")

使用輔助函式讓程式碼更具共性

除了引數(變數),還可以定義額外的函式來上面的程式碼變得更具共性。例如,這三種動物的叫聲,可以額外定義一個sound()函式描述它們。於是在前面的基礎上繼續修改這三個模組檔案:

# cow.py
def speak(self):
    print("a %s goes %s!" % (self,sound()))

def sound():
    return "moooo"

# sheep.py
def speak(self):
    print("a %s goes %s!" % (self,sound()))

def sound():
    return "baaaah"

# horse.py
def speak(self):
    print("a %s goes %s!" % (self,sound()))

def sound():
    return "neigh"

在main.py中,仍然可以使用之前的方式對這3個speak()進行呼叫:

import cow,sheep,horse

cow.speak("cow")
sheep.speak("sheep")
horse.speak("horse")

現在,這3個模組檔案的speak()已經完完全全地共性化了。

初步理解類和物件

所謂的類,就像是一個模板;所謂物件,就像是通過模板生成的具體的事物。類一般具有比較大的共性,物件一般是具體的,帶有自己的特性。

類與物件的關係,例如人類和人,鳥類和麻雀,交通工具和自行車。其中人類、鳥類、交通工具類都是一種型別稱呼,它們中的任何一種都具有像模板一樣的共性。例如人類的共性是能說話、有感情、雙腳走路、能思考等等,而根據這個人類别範本生成一個人,這個具體的人是人類的例項,是一個人類物件,每一個具體的人都有自己的說話方式、感情模式、性格、走路方式、思考能力等等。

類與類的關係。有的類的範疇太大,模板太抽象,它們可以稍微細化一點,例如人類可以劃分為男性人類和女性人類,交通工具類可以劃分為燒油的、電動的、腳踏的。一個大類按照不同的種類劃分,可以得到不同標準的小類。無論如何劃分,小類總是根據大類的模板生成的,具有大類的共性,又具有自己的個性。

在物件導向中,小類和大類之間的關係稱之為繼承,小類稱之為子類,大類稱之為父類。

類具有屬性,屬性一般包括兩類:像名詞一樣的屬性,像動詞一樣的行為。例如,人類有父母(parent),parent就是名詞,人類能吃飯(eat),eat這種行為就是動詞。鳥類能飛(fly),fly的行為就是動詞,鳥類有翅膀(wing),wing就是名詞。對於物件導向來說,名詞就是變數,動詞行為就是方法(也就是子程式)。通常,變數和方法都成為類的屬性。

當子類繼承了父類之後,父類有的屬性,子類可以直接擁有。因為子類一般具有自己的個性,所以子類可以定義自己的屬性,甚至修改從父類那裡繼承來的屬性。例如,人類中定義的eat屬性是一種非常抽象的、共性非常強的動詞行為,如果女性人類繼承人類,那麼女性人類的eat()可以直接使用人類中的eat,也可以定義自己的eat(比如淑女地吃)覆蓋從人類那裡繼承來的eat(沒有形容詞的吃),女性人類還可以定義人類中沒有定義的跳舞(dance)行為,這是女性人類的特性。子類方法覆蓋父類方法,稱之為方法的重寫(override),子類定義父類中沒有的方法,稱為方法的擴充套件(extend)。

當通過類構造出物件後,物件是類的例項,是類的具體化,物件將也具備類的屬性,且物件的屬性都有各自的值。例如,student類具有成績、班級等屬性,對於一個實際的學生A物件來說,他有成績屬性,且這個成績具有值,比如89分,班級也一樣,比如2班,此外,學生B也有自己的成績和班級以及對應的值。也就是說,根據類别範本生成物件後,物件的各個屬性都屬於自己,不同物件的屬性互不影響。

無論是物件與類還是子類與父類,它們的關係都可以用一種”is a”來描述,例如”自行車 is a 交通工具”(物件與類的關係)、”筆記本 is a 計算機”(子類與父類的關係)。

繼承

回到上面的3個模組檔案。它們具有共性的speak()和sound(),儘管sound()的返回內容各不相同,但至少函式名sound是相同的。

可以將這3個檔案中共性的內容抽取到同一個模組檔案中,假設放進animal.py的檔案中。animal.py檔案的內容為(但這是錯誤的程式碼,稍後修改):

def speak(self):
    print("a %s goes %s!" % (self,sound()))

def sound(): pass

然後修改cow.py、sheep.py和horse.py,使它們”繼承”animal.py。

# cow.py
import animal

def sound(): return "moooo"

# sheep.py
import animal

def sound(): return "baaaah"

# horse.py
import animal

def sound(): return "neigh"

現在,這三個模組檔案都沒有了speak(),因為它們都借用它們的”父類”animal中的speak()。

這表示horse、cow和sheep”繼承”了animal,前三者為”子類”,後者為”父類”。

但注意,這裡不是真正的繼承,因為python不支援非class物件的繼承,所以沒法通過非物件導向語法演示繼承。但至少從程式碼複用的角度上來說,它和繼承的功能是類似的。

另外注意,前面animal.py檔案是錯誤的,因為它的speak()函式中呼叫了sound()函式,但sound()函式在animal.py中是一個沒任何用處的函式,僅僅只是代表這個animal具有sound()功能(表示類的一個屬性)。而我們真正需要的sound()是可以呼叫cow、horse、sheep中的sound(),而不是animal自身的sound()。

所以,在沒有使用物件導向語法的情況下,改寫一下animal.py檔案,匯入cow、horse、sheep,使得可以在”父類”的speak()中呼叫各個”子類”的sound()。再次說明,這裡只是為了演示,這種程式設計方式是不規範的,在真正的物件導向語法中根本無需這些操作。

以下是修改後的animal.py檔案:

import cow,horse,sheep

def speak(self):
    print( "a %s goes %s!" % (self, eval(self + ".sound()")) )

def sound(): 
    pass

上面使用eval函式,因為python不支援普通的變數名作為模組名來呼叫模組的屬性sound(),所以使用eval先解析成cow或horse或sheep,再呼叫各自的sound()函式。如果不懂eval()的功能,可無視它。只需知道這是為了實現self.sound()來呼叫self所對應變數的sound()函式。

現在,在main.py中,使用下面的程式碼來呼叫speak(),得到的結果和前面是一樣的。

import cow,sheep,horse

cow.animal.speak("cow")
sheep.animal.speak("sheep")
horse.animal.speak("horse")

由於不是真正的”繼承”,所以這裡只能通過模組的方式新增一層animal.來呼叫speak()。

雖然上面的程式碼變得”人不人鬼不鬼”(因為沒有使用物件導向的語法),但物件導向的基本目標達到了:共性的程式碼全部抽取出去,實現最大程度的程式碼複用。

self是什麼

在python的物件導向語法中,將會經常看見self這個字眼。其實不僅python,各種動態型別的、支援物件導向的語言都使用self,例如perl、ruby也是如此。但是,self是約定俗成的詞,並非是強制的,可以將self換成其它任何字元,這並不會出現語法錯誤。

實際上,對於靜態面嚮物件語言來說,用的更多的可能是this,比如java、c#、c++都使用this來表示例項物件自身。

那麼self到底是什麼東西?

在前文,為了將cow、sheep和horse模組中speak()函式中的動物名稱變得共性,新增了一個self引數。之前的那段程式碼如下:

# cow.py
def speak(self):
    print("a %s goes moooo!" % (self))

# sheep.py
def speak(self):
    print("a %s goes baaaah!" % (self))

# horse.py
def speak(self):
    print("a %s goes neigh!" %(self))

當呼叫這三個函式時,分別傳遞各自的動物名作為引數:

import cow,sheep,horse

cow.speak("cow")
sheep.speak("sheep")
horse.speak("horse")

所以,對於cow來說,self是名為”cow”的動物,對於sheep來說,self是名為”sheep”的動物,對於horse來說,self是名為”horse”的動物。

也就是說,self是各種動物物件,cow.speak()時是cow,sheep.speak()時是sheep,horse.speak()時是horse。這裡的模組名變數和speak()的引數是一致的,這是我故意設計成這樣的,因為物件導向語法中預設的行為和這是完全一樣的,僅僅只是因為語法不同而寫法不同。

簡而言之,self是各個動物物件自身。

後來將cow、sheep和horse的speak()函式抽取到了animal中,仍然使用self作為speak()的引數。

以下是animal.py檔案中的speak()函式:

def speak(self):
    print( "a %s goes %s!" % (self, eval(self + ".sound()")) )

當使用下面的方式去呼叫它時:

cow.animal.speak("cow")
sheep.animal.speak("sheep")
horse.animal.speak("horse")

self是cow、是sheep、是horse,而不是animal。前面說了,在真正的物件導向語法中,中間的這一層animal是被省略的,這裡之所以加上一層animal,完全是因為python的非物件導向語法中沒辦法實現繼承。

當真正使用物件導向語法的時候,self將表示例項物件自身。例如student類有name屬性,當根據此類建立一個stuA物件,並使用self.name時,表示stuA.name,換句話說,self是stuA這個物件自身,self.name是stuA物件自身的屬性name,和另一個學生物件的stuB.name無關。

重寫父類方法

前面的animal.py中定義了一個空程式碼體的sound()函式,在cow、sheep和horse中定義了屬於自己叫聲的sound()函式。這其實就是方法的重寫(方法就是函式,只是在物件導向中稱為方法):父類定義了某個方法,子類修改和父類同名的方法。

例如,新新增一個類mouse,重寫animal的speak()方法,mouse的speak()方法中會叫兩聲,而不是其它動物一樣只有一聲。假設mouse類定義在mouse.py檔案中,程式碼如下:

import animal

def speak(self):
    animal.speak(self)
    print(sound())

def sound():
    return "jijiji"

這裡重寫了父類animal的speak(),並在mouse.speak()中呼叫了父類animal.speak(),再次基礎上還叫了一聲。

為了讓這段程式碼執行,需要在animal.py中匯入mouse,但在真正物件導向語法中是不需要的,原因前面說了。

# animal.py
import cow,horse,sheep,mouse

def speak(self):
    print( "a %s goes %s!" % (self, eval(self + ".sound()")) )

def sound(): 
    pass

然後在main.py中呼叫mouse.speak()即可:

import cow,sheep,horse,mouse

cow.animal.speak("cow")
sheep.animal.speak("sheep")
horse.animal.speak("horse")
mouse.speak("mouse")

按照”里氏替換原則”:子類重寫父類方法時,應該擴充套件父類的方法行為,而不是直接否定父類的方法程式碼並修改父類方法的程式碼。這是一種程式設計原則,並非強制,但是經驗所在,我們應當參考甚至儘量遵循這些偉人提出的原則。

正如上面的mouse,speak()是在父類的speak()上擴充套件的。如果將mouse.speak()改為如下程式碼,則不符合里氏替換原則:

import animal

def speak(self):
    print(sound())
    print(sound())

def sound():
    return "jijiji"

並非一定要遵循里氏替換原則,應該根據實際場景去考慮。比如上面的sound()方法,父類的sound()是一個空方法,僅僅只是宣告為類的屬性而存在。子類可以隨意根據自己的類特性去定製sound()。

再舉一個擴充套件父類方法的例子。在父類中定義了一個clean()方法,用於清理、回收父類的一些資訊。子類中也重寫一個clean()方法,但這時應當確保子類的clean()中包含了呼叫父類的clean()方法,再定義屬於子類獨有的應當清理的一些資訊。這就是父類方法的擴充套件,而不是父類方法的直接否定。因為子類並不知道父類的clean()會清理哪些資訊,如果完全略過父類clean(),很可能本該被父類clean()清理的東西,子類沒有去清理。

真正物件導向的語法

前面的所有內容都只是為了從程式碼複用的角度去演示如何從普通程式設計方式演變成物件導向程式設計。現在,簡單介紹python物件導向程式設計的語法,實現前文的animal、horse、cow和sheep,由此來和前文的推演做個比較。關於物件導向,更多內容在後面的文章會介紹。

使用class關鍵字定義類,就像定義函式一樣。這裡定義4個類,父類animal,子類cow、sheep、horse,子類繼承父類。它們分別儲存到animal.py、cow.py、sheep.py和horse.py檔案中。

animal.py檔案:

# 定義Animal類
class Animal():
    def speak(self):
        print( "a %s goes %s!" % (self, self.sound()) )
    def sound(self):
        pass

cow.py檔案:

import animal

# 定義Cow類,繼承自Animal
class Cow(animal.Animal):
    def sound(self):
        return "moooo"

sheep.py檔案:

import animal

# 定義Sheep類,繼承自Animal
class Sheep(animal.Animal):
    def sound(self):
        return "baaaah"

horse.py檔案:

import animal

# 定義Horse類,繼承自Animal
class Horse(animal.Animal):
    def sound(self):
        return "neigh"

在main.py檔案中生成這3個子類的例項,並通過例項物件去呼叫定義在父類的speak()方法:

import cow,horse,sheep

# 生成這3個子類的例項物件
cowA = cow.Cow()
sheepA = sheep.Sheep()
horseA = horse.Horse()

# 通過例項物件去呼叫speak()方法
cowA.speak()
sheepA.speak()
horseA.speak()

輸出結果:

a <cow.Cow object at 0x03341BD0> goes moooo!
a <sheep.Sheep object at 0x03341BF0> goes baaaah!
a <horse.Horse object at 0x03341F50> goes neigh!

輸出結果和想象中不一樣,先別管結果。至少如果把<xxx>換成對應的例項物件名稱,就和前文的效果一樣了。這個稍後再改。

先看語法。

使用class關鍵字宣告類,類名一般首字母大寫。如果要繼承某個類,在類名的括號中指定即可,例如class Cow(Animal)

因為Cow、Horse、Sheep類繼承了Animal類,所以即使這3個子類沒有定義speak()方法,也將擁有(繼承)父類Animal的speak()方法。

通過呼叫類,可以建立這個類的例項物件。例如上面cowA=cow.Cow(),表示建立一個Cow()的物件,這個物件在記憶體中,賦值給了cowA變數。也即是說cowA引用了這個物件,是這個物件的唯一識別符號。注意,cowA是變數,因為引用物件,所以可以稱為物件變數。

當呼叫cowA.speak()時,首先查詢speak()方法,因為沒有定義在Cow類中,於是查詢父類Animal,發現有speak()方法,於是呼叫父類的speak()方法。呼叫時,python會自動將cowA這個物件作為speak()的第一個引數,它將傳遞給Animal類中speak()的self引數,所以此時self表示cowA這個物件,self.sound()表示cowA.sound(),由於Cow類中定義了sound(),所以直接呼叫Cow類的sound(),而不會呼叫Animal中的sound()。

和前面的推演程式碼複用的過程比較一下,不難發現物件導向的語法要輕便很多,它將很多過程自動化了。

現在還有一個問題,上面的程式碼輸出結果不是我們想要的。見下文。

類的屬性

為了讓speak()輸出物件名(如物件變數名cowA),這並非一件簡單的事。

在python中,變數都是儲存物件的,變數和資料物件之間是相互對映的,只要引用變數就會得到它的對映目標。如果這個物件具有__name__屬性,則直接引用該屬性即可獲取該變數的名稱,很簡單。

但是很多物件並沒有__name__屬性,比如自定義的類的物件例項,這時想要獲取類的物件變數名,實非易事。有兩個內建函式可以考慮:globals()函式和locals()函式,它們返回當前的全域性變數和本地變數的字典。遍歷它們並對字典的value和給定變數進行比較,即可獲取想要的變數名key。

但如果跨檔案了,例如Animal類在一個檔案,Cow類在一個檔案,建立物件的程式碼又在另一個檔案,它們的作用域都是各自獨立的,想要在Animal類的方法speak()中獲取Cow類的物件變數名cowA,python應該是沒辦法實現的(perl支援,且實現非常簡單)。

所以,只能使用另一種標識物件的方法:為類新增屬性,例如name屬性,然後在speak()中引用物件的這個name屬性即可。

修改animal.py檔案如下:

class Animal():
    def speak(self,name):
        self.name = name
        print( "a %s goes %s!" % (self.name, self.sound()) )
    def sound(self):
        pass

然後,在main.py中呼叫speak()的時候,傳遞name引數即可:

import cow,horse,sheep

# 生成這3個子類的例項物件
cowA = cow.Cow()
sheepA = sheep.Sheep()
horseA = horse.Horse()

# 通過例項物件去呼叫speak()方法
cowA.speak("cowA")
sheepA.speak("sheepA")
horseA.speak("horseA")

輸出結果:

a cowA goes moooo!
a sheepA goes baaaah!
a horseA goes neigh!

這正是期待的結果。

構造方法__init__()

上面是在speak()方法中通過self.name = name的方式設定物件horseA的name屬性。一般來說,對於那些物件剛建立就需要具備的屬性,應當放在構造方法中進行設定。

構造方法是指從類構造物件時自動呼叫的方法,是物件的初始化方法。python的構造方法名為__init__()。以下是在構造方法中設定name屬性的程式碼:

class Animal():
    def __init__(self,name):
        self.name = name

    def speak(self):
        print( "a %s goes %s!" % (self.name, self.sound()) )

    def sound(self):
        pass

然後構造horseA物件的時候,傳遞name引數的值即可構造帶有name屬性的物件:

horseA = Horse("baima")
horseA.speak()

__init__()是在呼叫Horse()的時候自動被呼叫的,由於Horse類中沒有定義構造方法,所以將搜尋繼承自父類的構造方法__init__(),發現定義了,於是呼叫父類的構造方法,並將物件名horseA傳遞給self引數,然後設定該物件的name屬性為”baima”。

python設定或新增物件的屬性和其它語言非常不同,python可以在任意地方設定物件的屬性,而不必先在構造方法中宣告好具有哪些屬性。比如前面在speak()方法中通過self.name = name設定,此外還可以在main.py檔案中新增物件的屬性。例如新增一個color屬性:

horseA = Horse("baima")
horseA.color = "white"

只要通過self.xxx或者obj_name.xxx的方式設定屬性,無論在何處設定都無所謂,都會是該物件獨有的屬性,都會被代表名稱空間的__dict__屬性收集到。

horseA.__dict__

相關文章