物件導向武功秘笈(2):招式篇 — Python與Java的物件導向編程介紹

Posted on April 10, 2018 in CS. View: 14,109

物件導向編程

在上一章當中,我們藉由好的程式碼的特性:「正常執行」、「穩健」、「不重複撰寫」、「可讀性」、「可擴展」,自然而然引出物件導向的概念。在這一章當中YC會接續介紹完整的物件導向要如何實現,包括物件導向三大特性:封裝、繼承和多型。

在本章我會採用兩種語言交叉作說明,一種是靜態型別的語言Java,另一種是動態型別的語言Python,這兩種語言都是可以實現物件導向的語言,而所謂型別的動態與靜態可以用一個簡單的方法來區分:型別檢查(Type Checking)發生在什麼時候?像Java這類的靜態型別語言,它的型別檢查是在編譯時期(Compile Time)完成的,而像是Python這類的動態型別語言,它的型別檢查則是在執行時期(Runtime)才去做,所以Python可以不事先宣告變數型別,這點使得Python在開發上方便許多。

雖然Python和Java都是支援物件導向的語言,但在使用上有所差異。Java是一套對物件導向支援非常完整的語言,甚至你可以說Java是物件導向的原生種。而Python為了使程式語言更為簡潔,做出了許多改變,而這也造成在Python使用物件導向要跟著做出相應的改變,如果還守著嚴格的定義,使用起來會非常的彆扭。使用兩種語言說明物件導向是為了讓讀者更能了解物件導向的本質,而非語言本身。

本篇採用『大話設計模式』書中的物件導向篇範例。

類別(Class)與物件(Object)

首先來看物件導向的基本組成,類別(Class)與物件(Object)。物件導向程式設計中,類別(Class)是一種模板,它描述了物件的共同屬性和行為。物件(Object)是類別的實例,它具有類別定義的屬性和行為。物件是程式中實際存在的東西,而類別只是用來定義物件的模板。

舉個例子,我想要創造一隻有名字的貓,她有喵喵叫的能力,在Java中可以寫成

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* Java */

class Cat {  //{1}
    private String name; //{2}

    public Cat(String name) { //{3}
        this.name = name; //{4}
    }

    public String shout() { //{5}
        return "My name is " + name + ". meow~"; //{6}
    }
}

public class Test {
    public static void main(String[] args) { //{7}
        Cat cat = new Cat("May"); //{8}
        System.out.println(cat.shout()); //{9}
    }
}
// output:
// My name is May. meow~

{1} 建構一個Cat的類別,類別不是物件,類別只是物件的藍圖。

{2} 建立一個私有變數name,用來代表貓的名字,我們使用private的修飾詞讓它是私有的,也就是說外部環境沒辦法去讀取到這個變數,只有物件內部才可以讀取的到

{3} 提供建造方法(constructor)來初始化這一個物件,初始化需要name的參數。

{4} 在初始化的過程中,我們會將從外部讀取的name存入私有變數this.name裡,在Java裡頭,如果外部變數名稱與本地變數名稱相同,需要使用this來特別區分。

{5} 創造一個公開的類別方法shout()

{6} 使用私有變數name讓貓可以自我介紹,再發出喵喵叫的聲音。

{7} Java只要遇到main就會去執行,方法main具有靜態方法的修飾詞static,也就是說Test不需要被實體化也能執行main這個方法。

{8} 使用new來創造一個物件,在創造的過程會執行初始化,所以必須放入初始化需要的參數name,所以上面的新的物件有了"May"的名字。

{9} 接下來使用cat.shout()去執行喵喵叫的動作,這個方法會回傳字串,再利用System.out.println的方法將字串顯示出來。注意!在物件導向的習慣中,會用.來表示在那物件中的方法或屬性,所以cat.shout()就是執行在物件cat中的方法shout()

再來看Python怎麼表示,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
### Python3

class Cat: #{1}
    def __init__(self, name): #{2}
        self._name = name #{3}

    def shout(self): #{4}
        return "My name is " + self._name + ". meow~" #{5}

def main():
    cat = Cat("May") #{6}
    print(cat.shout()) #{7}

if __name__=="__main__": #{8}
    main()

# output:
# My name is May. meow~

{1} 建構Cat的類別,這是Python3的表示方法,如果是使用Python2.7的話,要寫成class Cat(object):才可以。

{2} Python的初始化方法,Python在初始化之前會先自行執行__new__的方法,這個過程會產生一個新的物件,也就是實體化,而這個新的物件會以第一個參數的方法被帶入__init__的方法裡進行初始化,我們通常會命名這個變數為self,這裡的self已經是個物件而不是類別,那初始化的過程需要引入外部資訊name的參數來進行命名,所以第二個參數就要設name,記住喔!第一個參數是Python自動產生的,不是由外部帶入的,所以外部只要給name一個參數就足夠了。

{3} 創造一個私有本地變數_name來將name存入,在Python當中以雙底線__或底線_開頭的變數會被視為是「私有的」,效果和Java的private接近,不過Python並沒有這麼嚴格禁止外部去讀取私有變數,所以需要配合工程師的自我規範。

{4} 類別方法shout(),只要你不是靜態的類別方法,Python都會自動幫你帶入物件資訊當作第一個參數,通常命名為self,那為什麼靜態方法沒有自動帶入,因為靜態方法不用實體化,所以根本不擁有物件的資訊。

{5} 使用到本地的self._name變數

{6} 創造一個物件,在創造的過程會執行初始化,所以必須放入初始化需要的參數name,所以上面的新的物件有了"May"的名字。

{7} 接下來使用cat.shout()去執行喵喵叫的動作,這個方法會回傳字串,再利用print的方法將字串顯示出來。注意!在物件導向的習慣中,會用.來表示在那物件中的方法或屬性,所以cat.shout()就是執行在物件cat中的方法shout()

{8} 在Python程式執行時,它的__name__會是"__main__",也就是說會去執行這個if判斷式下面的程式。

方法多載(Method Overloading)

物件導向的方法多載(Method Overloading)是指在同一類別中定義多個名稱相同的方法,但傳入參數的型別或數量不同。這可以讓同一個方法名稱能夠支援不同的參數,並讓程式碼更具彈性和易讀性。

來延伸剛剛的貓的例子,假設今天我們允許用戶不去設定貓咪的名字,而程式會預先給貓咪No-Name的預設值,所以我們需要另外一個初始化方法是不用貓咪名字的參數形式。

Java的實現程式碼如下所示,如此一來只要碰到沒有參數的形式,程式會給予"No-Name"的名字去當作貓咪的名字,並進行初始化。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/* Java */

class Cat {  
    private String name; 

    public Cat(String name) { 
        this.name = name; 
    }
    // method overloading
    public Cat() {
        this("No-Name"); // given "No-Name" as its name
    }

    public String shout() { 
        return "My name is " + name + ". meow~"; 
    }
}

public class Test {
    public static void main(String[] args) { 
        Cat cat = new Cat(); // no-argumant format
        System.out.println(cat.shout());
    }
}
// output:
// My name is No-Name. meow~

但在Python當中,不允許這種「相同方法名稱,卻又不同參數形式」,Python採用其他的方式來產生同樣的方法多載效果,如以下所示,我們可以看到Python使用default方法來實現多載,只要我們不給予name,它的default就是"No-Name"

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
### Python3

class Cat: 
    def __init__(self, name="No-Name"): # name's default is "No-Name"
        self._name = name 

    def shout(self): 
        return "My name is " + self._name + ". meow~" 

def main():
    cat = Cat() # no-argument format
    print(cat.shout()) 

if __name__=="__main__": 
    main()

# output:
# My name is No-Name. meow~

物件導向三大特性—封裝(Encapsulation)

還記得「低耦合,高內聚」的原則嗎?為了符合這原則,每個物件都要盡可能的去包含需要用到的屬性和方法,並且使得外部不能以不合理的方法去影響物件,這就稱之為「封裝」。

我們來看看上次的成果,我們就用這個例子來說明「封裝」。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
### Python3

class Calculation:
    def __init__(self, nums):
        self._nums = nums  # {1}
        for num in self._nums:
            self._checkPositiveInteger(num)

    def _checkPositiveInteger(self, num):  # {2}
        if (not isinstance(num, int)) or (num <= 0):
            raise ValueError("invalid positive integer: " + str(num))

    def _primeFactorize(self, num):  # {3}
        prime_factorize = dict()
        i = 2
        while(num > 1):
            if num % i == 0:
                prime_factorize[i] = prime_factorize.get(i, 0) + 1
                num /= i
            else:
                i += 1
        return prime_factorize

    def findGCF(self):
        prime_factorize = [self._primeFactorize(num) for num in nums]

        common_prime = set(prime_factorize[0].keys())
        for pf in prime_factorize[1:]:
            common_prime &= set(pf.keys())

        gcf = 1
        for prime in common_prime:
            m = min([pf[prime] for pf in prime_factorize])
            gcf = gcf * (prime ** m)

        return gcf

    def findLCM(self):
        gcf = self.findGCF()
        lcm = gcf
        for num in self._nums:
            lcm *= int(num / gcf)
        return lcm

{1} 使用私有變數,讓外部不能任意的改變self._nums變數,在這個例子當中,如果self._nums被任意改變,它將會逃過_checkPositiveInteger的檢查。 {2}&{3} 不需要由外部讀取的方法,就盡量讓它是私有的。

再回到貓咪的這個例子,如果我們想要可以調控「叫聲次數」的話,可以這樣實現。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/* Java */

class Cat {
    private String name = "";

    public Cat(String name) {
        this.name = name;
    }
    public Cat() {
        this("No-Name");
    }

    private int shout_num = 3; //{1}
    public int getShoutNum() { return shout_num; } //{2}
    public void setShoutNum(int num) { //{3}
        if (num < 0) throw new java.lang.IllegalArgumentException();
        shout_num = num; 
    }

    public String shout() {
        String result = "";
        for (int i=0; i<shout_num; i++){
            result += "meow~ ";
        }
        return "My name is " + name + ". " + result;
    }
}

public class Test {
    public static void main(String[] args) {
        Cat cat = new Cat("May");
        cat.setShoutNum(5); //{4}
        System.out.println(cat.shout());
    }
}
// output:
// My name is May. meow~ meow~ meow~ meow~ meow~

{1} 設置私有變數shout_num來決定叫聲次數。

{2} 為了做到封裝,外部不能直接去讀取shout_num,而是經由getShoutNum的外部方法來得到叫聲次數,這個getXXX的形式在物件導向裡頭被稱為Getter。

{3} 為了做到封裝,外部不能直接去改變shout_num,而是經由setShoutNum的外部方法來以合理的方法改變叫聲次數,在這個方法中,我們會先檢查再設定,如果是直接的改變shout_num將會少了這一份檢查,這個setXXX的形式在物件導向裡頭被稱為Setter。

{4} 使用Setter方法由外部來改變叫聲次數。

在Python中,Getter和Setter被簡化了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
### Python3

class Cat:
    def __init__(self, name="No-Name"):
        self._name = name
        self._shout_num = 3

    @property  #{1}
    def shout_num(self):
        return self._shout_num

    @shout_num.setter  #{2}
    def shout_num(self, num):
        if num < 0: 
            raise ValueError()
        self._shout_num = num

    def shout(self):
        result = ""
        for _ in range(self.shout_num):
            result += "meow~ "
        return "My name is " + self._name + ". " + result

def main():
    cat = Cat("May")
    cat.shout_num = 5  #{3}
    print(cat.shout())

if __name__ == "__main__":
    main()

# output:
# My name is May. meow~ meow~ meow~ meow~ meow~

{1} Python使用Decorator@property來創造Getter,一旦加上了@property,當下的函數方法就會變成一種性質。

{2} 再使用shout_num.setter來替shout_num這個特性加上Setter。

{3} 然後我們就可以以像是修改一般變數的方式來修改shout_num,但實際上shout_num是有被封裝的,如此一來就可以更為簡潔,不用去寫getXXsetXXX等囉唆的寫法。

物件導向三大特性—繼承(Inheritance)

還記得「Don't Repeat Yourself」原則嗎?物件導向同樣提供了這個選項,「繼承」可以讓子類擁有父類的屬性和方法,避免不必要的重寫,但同時也會增加父類和子類之間的耦合,所以使用時要去評估它影響了多少耦合性。

子類可以先繼承父類的屬性和方法,再去新增屬於子類自己的屬性和方法,甚至還可以去覆寫父類的方法,這稱之為方法覆寫(Method Overriding),有了這些方法,子類可以在不重複撰寫父類方法的情況下,去增加自己的特色和自己的功能。

依循著剛剛的例子,如果我們今天想要增加狗的類別,但是又不想重複撰寫相同的部分,所以我們可以選擇創造動物的類別,再讓貓和狗繼承自動物。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/* Java */

class Animal {
    protected String name = ""; //{1}

    public Animal(String name) {
        this.name = name;
    }
    public Animal() {
        this("No-Name");
    }

    protected int shout_num = 3;
    public int getShoutNum() {
        return shout_num;
    }
    public void setShoutNum(int num) {
        if (num < 0) throw new java.lang.IllegalArgumentException();
        shout_num = num;
    }
}

class Cat extends Animal{
    public Cat(String name) {
        super(name); //{2}
    }
    public Cat() {
        super();
    }

    public String shout() {
        String result = "";
        for (int i=0; i<shout_num; i++){
            result += "meow~ ";
        }
        return "My name is " + name + ". " + result;
    }
}
class Dog extends Animal{
    public String shout() {
        String result = "";
        for (int i=0; i<shout_num; i++){
            result += "woof~ ";
        }
        return "My name is "+name+". "+result;
    }
}

{1} protected的效果和private一樣,讓外部無法讀取到內部的私有化,但是private無法被「繼承」,而protected可以被繼承,所以如果希望可以被繼承的私有變數或方法,就使用protected

{2} super指的是父類,這裡我們使用父類來做初始化,事實上這邊可以不用再初始化一次,子類本身就會繼承父類的初始化方法,所以可以像Dog一樣省略不寫。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
### Python3

class Animal:
    def __init__(self, name="No-Name"):
        self._name = name
        self._shout_num = 3

    @property
    def shout_num(self):
        return self._shout_num

    @shout_num.setter
    def shout_num(self,num):
        if num < 0:
            raise ValueError()
        self._shout_num = num

class Cat(Animal):
    def __init__(self, name="No_Name"):
        super().__init__(name)  #{1}

    def shout(self):
        result = ""
        for _ in range(self.shout_num):
            result += "meow~ "
        return "My name is " + self._name + ". " + result

class Dog(Animal):
    def shout(self):
        result = ""
        for _ in range(self.shout_num):
            result += "woof~ "
        return "My name is " + self._name + ". " + result

{1} super指的是父類,這裡我們使用父類來做初始化,事實上這邊可以不用再初始化一次,子類本身就會繼承父類的初始化方法,所以可以像Dog一樣省略不寫。

抽象化:抽象類別(Abstract Class)、抽象方法(Abstract Method)和接口(Interface)

事實上,剛剛使用Animal的方法並不是很完整,我們將Animal當作一個類別處理,所以Animal其實是可以被實例化的,但是Animal根本沒有什麼有用的方法,它必須被繼承後再添加方法才有用處,所以我們其實可以把Animal抽象化,將Animal視為抽象類別,其中擁有一些方法需要在子類實現的,稱為抽象方法,我們直接看怎麼做。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/* Java */

abstract class Animal { //{1}
    protected String name = "";

    public Animal(String name) {
        this.name = name;
    }
    public Animal() {
        this("No-Name");
    }

    protected int shout_num = 3;
    public int getShoutNum() {
        return shout_num;
    }
    public void setShoutNum(int num) {
        if (num < 0) throw new java.lang.IllegalArgumentException();
        shout_num = num;
    }

    public String shout() { //{2}
        String result = "";
        for (int i=0; i<shout_num; i++){
            result += getShoutSound()+" "; //{3}
        }
        return "My name is " + name + ". " + result;
    }
    abstract protected String getShoutSound(); //{4}
}

class Cat extends Animal{
    protected String getShoutSound() { //{5}
        return "meow~"; 
    }
}
class Dog extends Animal{
    protected String getShoutSound() {
        return "woof~";
    }
}

{1} 使用abstract class修飾詞來創建抽象類別,只有在抽象類別中才可以擁有抽象方法,抽象類別不能直接被實例化。

{2} 抽象類別也可以有一般的具體方法。

{3} 這裡使用的getShoutSound()方法要等在子類才會被實現。

{4} 使用abstract設置抽象方法,繼承自抽象類別的子類必須要完全實現所有的抽象方法。

{5} 實現抽象方法。

在 Python 中沒有原生的抽象類別和方法,過去可能會使用 abc ,但是 YC 發現其實幾乎很少人使用這樣的方法,大部分的作法是使用 raise NotImplementedError ,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
### Python3

class Animal:
    def __init__(self, name="No-Name"):
        self._name = name
        self._shout_num = 3

    @property
    def shout_num(self):
        return self._shout_num

    @shout_num.setter
    def shout_num(self, num):
        self._shout_num = num

    def shout(self):
        result = ""
        for _ in range(self.shout_num):
            result += self._getShoutSound() + " "
        return "My name is " + self._name + ". " + result

    def _getShoutSound(self):  #{1}
        raise NotImplementedError

class Cat(Animal):
    def _getShoutSound(self): #{2}
        return "meow~"

class Dog(Animal):
    def _getShoutSound(self):
        return "woof~"

{1} _getShoutSound 是一個未實現的方法,如果繼承之後沒有去Override這個方法,將會報錯。

{2} Override這個方法。

還有一種類型抽象化的更為徹底,稱之為「接口」,「接口」上的所有方法都是抽象未實現的,「接口」不能擁有任何具體的方法。雖然「接口」很像是完全抽象化的「抽象類別」,也確實可以利用「抽象類別」來創造「接口」,但是兩者的意義是不同的,「抽象類別」是從子類中發現共通的東西,而泛化出現的,但是「接口」可以根本不預先知道子類是什麼,而僅僅事先定義行為本身,換句話說,「抽象類別」是類別的抽象化,而「接口」則是行為的抽象化。

例如,我想要讓某些動物擁有「飛」的能力,這是一個行為,而不會事先知道它會套用到哪一個類別上面。

在Java之中只允許單一繼承,但是卻可以有多個「接口」;而Python沒有現成的「接口」可以使用,我們必須使用「抽象方法」來創造「接口」,所以開發者要謹記「接口」的限制:不能有任何的具體方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/* Java */

interface IFly { //{1}
    public String flyTo(String place); //{2}
}
class FlyingCat extends Cat implements IFly { //{3}
    public String flyTo(String place) { //{4}
        return shout() + " I'm going to fly to " + place + ".";
    }
}

public class Test {
    public static void main(String[] args) {
        FlyingCat cat = new FlyingCat("May");
        System.out.println(cat.flyTo("Taiwan"));
    }
}
// output: 
// My name is May. meow~ meow~ meow~ I'm going to fly to Taiwan.

{1} 使用interface創建「接口」,我們習慣會使用開頭大寫I來表示Interface(接口)。

{2} 「接口」定義未實現的抽象方法flyTo

{3} FlyingCat繼承自Cat並且裝上IFly的「接口」。

{4} 必須實現「接口」上所有的抽象方法。

因為Python允許多重繼承,所以就可以直接將模擬「接口」的抽象類別直接疊加上去。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
### Python3

class IFly:
    def flyTo(self, place): 
        raise NotImplementedError

class FlyingCat(Cat, IFly): #{1}
    def flyTo(self, place):
        return self.shout() + " I'm going to fly to " + place + ".";

def main():
    cat = FlyingCat("May")
    print(cat.flyTo("Taiwan"))

if __name__ == "__main__":
    main()

# output: 
# My name is May. meow~ meow~ meow~ I'm going to fly to Taiwan.

{1} 直接使用多重繼承,將IFly安裝上去。

物件導向三大特性—多型(Polymorphism)

最後,來講講物件導向的最後一個特性,那就是「多型」。「多型」的涵義是指「子類可以以父類的身分出現」,而因為是以父類的角色出現,所以只能執行父類擁有的方法,也就是只能執行這些子類共同泛化分享的方法,當然不同的子類實現後的效果會不一樣,不然使用「多型」的意義就不大了,至於子類自己的特殊方法則不可以使用「多型」去執行。

直接來看範例,假設今天我要邀請三隻貓貓狗狗來參加叫聲比賽,分別請他們叫個幾聲來聽聽,此時就需要使用到「多型」的方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/* Java */

abstract class Animal { 
    protected String name = "";

    public Animal(String name) {
        this.name = name;
    }
    public Animal() {
        this("No-Name");
    }

    protected int shout_num = 3;
    public int getShoutNum() {
        return shout_num;
    }
    public void setShoutNum(int num) {
        if (num < 0) throw new java.lang.IllegalArgumentException();
        shout_num = num;
    }

    public String shout() { 
        String result = "";
        for (int i=0; i<shout_num; i++){
            result += getShoutSound() + " "; 
        }
        return "My name is " + name + ". " + result;
    }
    abstract protected String getShoutSound(); 
}

class Cat extends Animal{
    protected String getShoutSound() { 
        return "meow~"; 
    }
}
class Dog extends Animal{
    protected String getShoutSound() {
        return "woof~";
    }
}

public class ShoutGame {
    public static void main(String[] args) {
        Animal[] arrayAnimal = new Animal[3]; //{1}
        // polymorphism
        arrayAnimal[0] = new Cat("May"); //{2}
        arrayAnimal[1] = new Dog("Linda");
        arrayAnimal[2] = new Cat("Joy");
        for (Animal animal: arrayAnimal) {
            System.out.println(animal.shout()); //{3}
        }
    }
}
// output: 
// My name is May. meow~ meow~ meow~
// My name is Linda. woof~ woof~ woof~
// My name is Joy. meow~ meow~ meow~

{1} 創建Animal的Array,Animal是抽象類別不能實體化,這裡預定要放的是它的繼承實現類別。

{2} 將CatDog任意放到Animal的Array是可以的,此時就套用「多型」,不管是CatDog都是以Animal的形式出現,只能執行Animal有的方法。

{3} shout()是父類Animal有的方法,可以被執行。

而在Python當中,「多型」就沒這麼重要了,因為Python具有「鴨子型別」(Duck Typing),什麼是「鴨子型別」呢?有一句話道出它的意義:「當看到一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就可以被稱為鴨子」,因為Python是動態型別的語言,變數型態不需要事先宣告,所以一個變數具有彈性可以放入任意型別,直到出現不合適的使用方法,才會報錯,所以在Python中變數可以任意放入不同的類別的物件,只要確保這些類別都具有這些變數所需要用到的方法,就可以了,這不正是接近「多型」的概念。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
### Python3

class Animal: 
    def __init__(self, name="No-Name"):
        self._name = name
        self._shout_num = 3

    @property
    def shout_num(self):
        return self._shout_num

    @shout_num.setter
    def shout_num(self,num):
        self._shout_num = num

    def shout(self):
        result = ""
        for _ in range(self.shout_num):
            result += self._getShoutSound() + " "
        return "My name is " + self._name + ". " + result  

    def _getShoutSound(self):
        raise NotImplementedError

class Cat(Animal):
    def _getShoutSound(self):
        return "meow~"

class Dog(Animal):
    def _getShoutSound(self):
        return "woof~"

def main():
    # Shout Game
    arrayAnimal = [] #{1}
    arrayAnimal.append(Cat("May")) 
    arrayAnimal.append(Dog("Linda"))
    arrayAnimal.append(Cat("Joy"))
    for animal in arrayAnimal:
        if not isinstance(animal, Animal): 
            raise TypeError #not necessary #{2}
        print(animal.shout())

if __name__ == "__main__":
    main()

# output: 
# My name is May. meow~ meow~ meow~
# My name is Linda. woof~ woof~ woof~
# My name is Joy. meow~ meow~ meow~

{1} 要存入多型的List不需要特別處理。

{2} 可以檢查是不是繼承自Animal,以確保多型的嚴格定義,但這個過程是非必要的。

總結:物件導向使用方法

好!我們花了好大的力氣,終於了解如何在Java和Python中使用物件導向。從一開始的「類別」和「物件」講起,再來談到物件導向的三大特性:「封裝」、「繼承」和「多型」,還談到方法可以「多載」也可以「覆寫」,以及一些抽象化的東西,包括「抽象類別」、「抽象方法」和「接口」。

但是等等!你知道該怎麼運用這些技巧嗎?沒錯,僅僅是了解這些招式不足以讓你寫出卓越的程式碼,你還需要了解如何使用,就像是外功招式還得配合內功才可以是一套完整的功夫,否則只是半吊子而已,我們將在下一章節中來帶大家了解如何去使用這些招式。