2011年9月10日 星期六

抽象類別

定義類別,本身就是在進行抽象化,如果一個類別定義時不完整,有些狀態或行為必須留待子類別來具體實現,則它是個抽象類別(Abstract Class)。例如,在定義銀行帳戶時,你也許想將 一些帳戶的共同狀態與行為定義在父類別中:

class Account:
    def withdraw(self, amount):
        if amount >= self.balance:
            self.balance -= amount
        else:
            raise ValueError('餘額不足')

    def __str__(self):
        return ('Id:\t\t' + self.id +
               '\nName:\t\t' + self.name +
               '\nBalance:\t' + str(self.balance))

顯然地,這個類別的定義不完整,
self.id、self.name、self.balance沒有定義,嘗試使用這個類別進行操作時,就會發生直譯錯誤:
acct = Account()
print(acct)

你可以繼承這個類別來實作未完整的定義:
class CheckingAccount(Account):
    def __init__(self, id, name):
        self.id = id
        self.name = name
        self.balance = 0
        self.overdraftlimit = 30000

    def withdraw(self, amount):
        if amount <= self.balance + self.overdraftlimit:
            self.balance -= amount
        else:
            raise ValueError('超出信用')

    def __str__(self):
        return (super(CheckingAccount, self).__str__() + 
                '\nOverdraft limit\t' + str(self.overdraftlimit));
                
acct = CheckingAccount('E1223', 'Justin Lin')
print(acct)

現在的問題是,實際上開發人員還是可以用Account()實例化,也許您可以修改一下Account的定義:
class Account:
    def __init__():
        raise NotImplementedError("Account is abstract")
       
    ...略

如此,嘗試使用Account()實例化後,在初始化方法中就會引發錯誤(不過,實際上Account實例確實有產生了,但就這邊的需求來說,目的算已達到)。

像Python這類的動態語言,沒有Java的abstractinterface這種機制來規範一個類別所需實作的介面,遵循物件之間的協定基本上是開發 人員的自我約束(當然,還得有適當的說明文件)。如果你非得有個方式,強制實現某個公開協定,那該怎麼作?像上面一樣,藉由直譯錯誤是一種方式,實際上
視你的需求而定(是否可實例化、子類別是否定義初始化方法等),還有許多模擬的方式,不過在Python中,可以使用Meta class@abstractmethod來達到規範的需求。

舉個例子來說,
您想要設計一個猜數字遊戲,猜數字遊戲的流程大致就是:
顯示訊息(歡迎
隨 機產生數字

遊戲迴圈
   
顯示訊息(提示使用者輸入
    取得使用者輸入
    比較是否猜中   顯示訊息(輸入正確與否

在描述流程輸廓時,並沒有提及如何顯示訊息、沒有提及如何取得使用者輸 入等具體的作法,只是歸納出一些共同的流程步驟
import random
from abc import ABCMeta, abstractmethod

class GuessGame(metaclass=ABCMeta):
    @abstractmethod
    def message(self, msg):
        pass

    @abstractmethod
    def guess(self):
        pass     

    def go(self):
        self.message(self.welcome)
        number = int(random.random() * 10)
        while True:
            guess = self.guess();
            if guess > number:
                self.message(self.bigger)
            elif guess < number:
                self.message(self.smaller)
            else:
                break
        self.message(self.correct)

現在GuessGame是個抽象類別,如果你嘗試實例化GuessGame
game = GuessGame()

則會引發錯誤:
TypeError: Can't instantiate abstract class GuessGame with abstract methods guess, message

如果是個文字模式下的猜數字遊戲,可以將顯示訊息、取得使用者輸入等以文字模式下的具體作法實現出來。例如:
class ConsoleGame(GuessGame):
    def __init__(self):
        self.welcome = "歡迎"
        self.prompt = "輸入數字:"
        self.correct = "猜中了"
        self.bigger = "你猜的比較大"
        self.smaller = "你猜的比較小"
    
    def message(self, msg):
        print(msg)
    
    def guess(self):
        return int(input(self.prompt))

game = ConsoleGame()
game.go()

如果子類別忘了實作某個方法,則該子類別仍被視為一個抽象類別,如果嘗試實例化抽象類別就會引發錯誤。例如若忘了實作message(),就會發生以下錯誤:
TypeError: Can't instantiate abstract class ConsoleGame with abstract methods message

所以,如果你真的想要模擬Java中interface的作用,則可以定義一個抽象類別,完全沒有實作的方法即可。例如:
import random
from abc import ABCMeta, abstractmethod

class Flyer(metaclass=ABCMeta): # 就像是Java中的interface
    @abstractmethod
    def fly(self):
        pass

class Bird:
    pass
    
class Sparrow(Bird, Flyer):  # 就像Java中繼承Bird類別並實作Flyer介面
    def fly(self):
        print('麻雀飛')

s = Sparrow()
s.fly()

沒有留言:

張貼留言