Python 學習筆記之類「物件導向,超類,抽象」

dicksonjyl560101發表於2018-07-25

Python 學習筆記之類「物件導向,超類,抽象」

 

 

物件魔法

在物件導向程式設計中,術語 物件 大致意味著一系列資料 (屬性) 以及一套訪問和操作這些資料的方法。

封裝

封裝講究結構複用,邏輯內斂,以固定介面對外提供服務。其遵循單一職責,規定每個型別僅有一個引發變化的原因。單一封裝的核心是解耦和內聚,這讓設計更簡單,清晰,程式碼更易測試和凍結,避免了不確定性。

繼承

繼承在遵循原有設計和不改變既有程式碼的前提下,新增新功能,或改進演算法。其對應開閉原則,對修改封閉,對擴充套件開放。

多型

多型特性遵循里氏替換原則,所有繼承子類應該能直接用於引用父類的場合。

我們習慣於將複雜型別的公用部分剝離出來,形成穩固的抽象類。其他引發變化的相似因素則被分離成多個子類,以確保單一職責得到遵守,並能相互替換。

我們習慣於將複雜型別的公用部分剝離出來,形成穩固的抽象類。其他引發變化的相似因素則被分離成多個子類,以確保單一職責得到遵守,並能相互替換。

場景: 先將整體框架抽象成基類,然後每個子類僅保留單一分支邏輯。

繼承和多型

定義一個名為 Animal 的 class,有一個 run() 方法可以直接列印:

class Animal (object):
 
def run (self):
      print(
"Animal is running..." )


當我們需要編寫 Dog 和 Cat 類時,就可以直接從 Animal 類繼承:

class Dog (Animal):
 
pass
class Cat (Animal):
 
pass

 

對於 Dog 來說,Animal 就是它的父類,對於 Animal 來說,Dog 就是它的子類。Cat 和 Dog 類似。

繼承的好處一:子類獲得了父類的全部功能,擴充套件子類自己的功能

上例中 Animial 實現了 run() 方法,因此,Dog 和 Cat 作為它的子類也擁有 run() 方法:

dog = Dog()
dog.run()

cat = Cat()
cat.run()

'''
Animal is running...
Animal is running...
'''


我們也可以擴充套件子類的方法,比如 Dog 類:

class Dog (Animal):
 
def run (self):
      print(
"Dog is running..." )
 
def dog_eat (self):
      print(
"Eating bones..." )


繼承的好處二:重寫父類的功能。

無論是 Dog 還是 Cat,它們 run() 的時候,顯示的都是 Animal is running...,而對於 Dog 和 Cat 本身,應該具有自己的 run() 特性,即: Dog is running..  和  Cat is running...,因此,對 Dog 和 Cat 類改進如下:

class Dog (Animal):
   
def run (self):
       print(
"Dog is running..." )
   
def dog_eat (self):
       print(
"Eating bone..." )

class Cat (Animal):
   
def run (self):
       print(
"Cat is running..." )
   
def cat_eat (self):
       print(
"Eating fish..." )

if __name__ == "__main__" :
   dog = Dog()
   dog.run()

   cat = Cat()
   cat.run()
'''
Dog is running...
Eating bone...
Cat is running...
Eating fish...
'''


當子類的 run() 覆蓋了父類的 run() 時候,執行時總是會呼叫子類的 run() 。 這樣,我們就獲得了繼承的另一個好處:多型。

 

判斷一個變數是否是某個型別可以用 isinstance() 判斷:

>>> isinstance(dog,Animal)
>>> isinstance(cat,Animal)
'''
True
True
'''


>>> isinstance(dog,Dog)
>>> isinstance(cat,Dog)
'''
True
False
'''


上面示例可以看到,dog 的資料型別既是 Animal,也是 Dog。因為 Dog 是從 Animal 繼承下來的,當我們建立 Dog 例項時,我們認為 dog 的資料型別是 Dog,但同時 dog 也是 animal 的資料型別,因為 Dog 本來就是 Animal  的一種。而 cat 的資料型別是 Animal 沒錯,但是 cat 不是 Dog 資料型別。

 

所以,在繼承關係中,如果一個例項的資料型別是某個子類,那它的資料型別也可以被看做是父類。但是,反過來就不行:

>>> b = Animal()
>>> isinstance(b, Dog)
'''
False
'''


Dog
可以看成 Animal,但 Animal 不可以看成Dog。

 

超類

要指定超類,可在 class 語句中的類名後加上超類名,並將其用圓括號括起。

Filter 是一個過濾序列的通用類。實際上,它不會過濾掉任何東西。

class Filter :
   
def init (self):
       self.blocked = []
   
def filter (self, sequence):
       
return [x for x in sequence if x not in self.blocked]

class SPAMFilter (Filter):
   
# SPAMFilter 是Filter的子類 def init(self): # 重寫超類Filter的方法init
   
def init (self): # 重寫超類Filter的方法init
       self.blocked = [
'SPAM' ]
       
if __name__== "__main__" :
   f = Filter()
   f.init()
   print(f.filter([
1 , 2 , 3 ]))
   
'''
1, 2, 3
'''


Filter
類的用途在於可用作其他類 (如將 'SPAM' 從序列中過濾掉的 SPAMFilter 類) 的基類 (超類)。


if __name__== "__main__" :
  s = SPAMFilter()
  s.init()
  a = s.filter([
'SPAM' , 'SPAM' , 'SPAM' , 'SPAM' , 'eggs' , 'bacon' , 'SPAM' ])
  print(a)
'''
['eggs', 'bacon']
'''


請注意 SPAMFilter 類的定義中有兩個要點。

 

  • 以提供新定義的方式重寫了 Filter 類中方法 init 的定義。

  • 直接從 Filter 類繼承了方法 filter 的定義,因此無需重新編寫其定義。

第二點說明了繼承很有用的原因:可以建立大量不同的過濾器類,它們都從 Filter 類派生而來,並且都使用已編寫好的方法 filter。

你可以用複數形式的 __ bases __ 來獲悉類的基類,而基類可能有多個。為說明如何繼承多個類,下面來建立幾個類。

class Calculator :
   
def calculate (self, expression):
       self.value = eval(expression)
class Talker :
   
def talk (self):
       print(
'Hi, my value is' ,self.value)
class TalkingCalculator (Calculator, Talker): pass

if __name__== "__main__" :
   tc = TalkingCalculator()
   tc.calculate(
'1 + 2 * 3' )
   tc.talk()
 
'''
Hi, my value is 7
'''


這被稱為多重繼承,是一個功能強大的工具。然而,除非萬不得已,否則應避免使用多重繼承,因為在有些情況下,它可能帶來意外的 “併發症”。 

使用多重繼承時,有一點務必注意: 如果多個超類以不同的方式實現了同一個方法 (即有多個同名方法),必須在class 語句中小心排列這些超類,因為位於前面的類的方法將覆蓋位於後面的類的方法。

因此,在前面的示例中,如果 Calculator 類包含方法 talk,那麼這個方法將覆蓋 Talker 類的方法 talk (導致它不可訪問)。

如果像下面這樣反轉超類的排列順序:

class TalkingCalculator (Talker, Calculator): pass


將導致 Talker 的方法 talk 是可以訪問的。多個超類的超類相同時,查詢特定方法或屬性時訪問超類的順序稱為方法解析順序 (MRO),它使用的演算法非常複雜。

 

抽象基類

一般而言,抽象類是不能例項化的類,其職責是定義子類應實 現的一組抽象方法。

下面是一個簡單的示例:  

from abc import ABC, abstractmethod
class Talker (ABC):
  @abstractmethod
 
def talk (self):
     
pass


這裡的要點是你使用 @abstractmethod 來將方法標記為抽象的 —— 在子類中必須實現的方法。

 

如果你使用的是較舊的 Python 版本,將無法在模組 abc 中找到 ABC 類。在這種情況下,需要匯入ABCMeta,並在類定義開頭包含程式碼行 __ metaclass __ = ABCMeta (緊跟在 class 語句後面並縮排)。如果你使用的是 3.4 之前的 Python 3 版本,也可使用 Talker(metaclass=ABCMeta) 代替 Talker(ABC)。

抽象類(即包含抽象方法的類)最重要的特徵是不能例項化。

>> > Talker()
  Traceback (most recent call last):
  File
"<stdin>" , line 1 , in < module >
 
TypeError: Can 't instantiate abstract class Talker with abstract methods talk


假設像下面這樣從它派生出一個子類:


class Knigget (Talker):
 
pass


由於沒有重寫方法 talk,因此這個類也是抽象的,不能例項化。如果你試圖這樣做,將出現類似於前面的錯誤訊息。然而,你可重新編寫這個類,使其實現要求的方法。


class Knigget (Talker):
 
def talk (self):
      print(
"Ni!" )


現在例項化它沒有任何問題。這是抽象基類的主要用途,而且只有在這種情形下使用 isinstance 才是妥當的:如果先檢查給定的例項確實是 Talker 物件,就能相信這個例項在需要的情況下有方法 talk。


> >> k = Knigget()
> >> isinstance(k, Talker) True
> >> k.talk()
Ni!



總結

 

將相關的東西放在一起。 如果一個函式操作一個全域性變數,最好將它們作為一個類的屬性和方法。

不要讓物件之間過於親密。 方法應只關心其所屬例項的屬性,對於其他例項的狀態,讓它們自己去管理就好了。

慎用繼承,尤其是多重繼承。 繼承有時很有用,但在有些情況下可能帶來不必要的複雜性。要正確地使用多重繼承很難,要排除其中的 bug 更難。

保持簡單,讓方法短小緊湊。 一般而言,應確保大多數方法都能在 30 秒內讀完並理解。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/29829936/viewspace-2158498/,如需轉載,請註明出處,否則將追究法律責任。

相關文章