抽象類別


定義類別,本身就是在進行抽象化,如果一個類別定義時不完整,有些狀態或行為必須留待子類別來具體實現,則它是個抽象類別(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()