17. 物件導向的特徵

星光映梦發表於2024-10-16

一、物件導向的三大特徵

  物件導向的三大特徵指的是 封裝繼承多型

  封裝(encapsulation,有時稱為資料隱藏)是處理物件的一個重要概念。從形式上看,封裝就是將資料和行為組合在一個包中,並對物件的使用者隱藏具體的實現方式。

  繼承(inheritance)的基本思想是,可以基於已有的類建立新類。繼承以存在的類就是複用(繼承)這些類的方法,而且可以增加一些新的屬性和方法,使新類能夠適應新的情況。

  多型(polynirphic)就是一個類的多種形態。

二、封裝性

2.1、什麼是封裝性

  所謂的封裝,就是把客觀事物封裝成抽象概念的類,並且類可以把自己的資料和方法只向可信的類或物件開放,向沒必要開放的類或物件異常資訊。封裝性就是隱藏內部物件的複雜性,只對外公開簡單的介面。便於外部呼叫,從而提高系統的可擴充套件性、可維護性。通俗的來說,把該隱藏的隱藏起來,該暴露的暴露出來。

  當我們建立一個類時,我們可以透過“物件.屬性”的方式,對物件的屬性進行賦值。此時,賦值操作要受到屬性的資料型別和儲存範圍的制約。除此之外,沒有其它制約條件。但是,在實際問題中,我們往往需要給屬性賦值加入額外的限制條件。這個條件就不能在屬性宣告時體現,只能透過方法進行限制條件的新增。同時,我們需要避免使用者在使用“物件.屬性”的方式對屬性進行賦值,則需要將屬性宣告為私有的(private)。此時,針對於屬性就體現了封裝性。

  使用封裝後,確實增加了類的定義的複雜程度,但是它也確保了資料的安全性。我們隱藏了屬性名,使呼叫者無法任意修改物件中的屬性。並且我們增加了 getter() 和 setter() 方法,可以很好的控制屬性是否為只讀的。如果希望屬性是隻讀的,則可以直接去掉 setter() 方法,如果希望屬性不能被外部訪問,則可以直接去掉 getter() 方法。而且在使用 setter() 方法設定屬性時,可以增加資料的驗證,確保資料的值是正確的。在使用 getter() 方法獲取屬性,使用 setter() 方法設定屬性時,可以在讀取屬性和修改屬性的同時做一些其它的處理。

2.2、封裝性的體現

  我們將類的屬性(xxx)隱藏,同時,提供的公共的方法類獲取(getter())和設定(setter(self))此屬性的值。在 Python 中,我們可以為物件的屬性使用雙下劃線開頭 __xxx,雙下劃線開頭的屬性,是物件的隱藏屬性,隱藏屬性值只能在類的內部訪問,無法透過物件訪問。

class Person:
    def __init__(self,name,age):
        self.__name = name
        self.__age = age

    def get_name(self):
        return self.__name
  
    def set_name(self,name):
        self.__name = name

    def get_age(self):
        return self.__age
  
    def set_age(self,age):
        if age >= 0:
            self.__age = age

    def __show_info(self):
        print(f"name: {self.__name}, age: {self.__age}")

  如果我們直接訪問類中的隱藏屬性,會報以下錯誤:

p1 = Person("Sakura",10)
print(p1.__name)
AttributeError: 'Person' object has no attribute '__name'

  此時,我們可以透過公共的 getter() 方法和 setter() 方法呼叫類的私有屬性:

p1 = Person("Sakura",10)
print("name: ", p1.get_name() , ", age: ", p1.get_age())

p1.set_age(12)
print("name: ", p1.get_name() , ", age: ", p1.get_age())

p1.set_age(-10)
print("name: ", p1.get_name() , ", age: ", p1.get_age())

  其實,隱藏屬性是假隱藏,只不過是 Python 自動為屬性改了一個名字。實際上,Python 將名字修改為 _型別__屬性名

p1 = Person("Sakura",10)
p1._Person__age = 12
print(p1._Person__age)
p1._Person__show_info()

使用雙下劃線開頭的屬性,實際上依然可以在外部訪問。在開發中,我們可以將一些私有屬性以下劃線開頭(實際上是公開的屬性),告訴開發人員沒有特殊需要不要修改私有屬性。

2.3、property裝飾器

  property 裝飾器,用來將一個 getter() 方法,轉換為物件的屬性,新增了 property 裝飾器以後,我們就可以向呼叫屬性一樣使用 getter() 方法。使用 property 裝飾的方法必須和屬性名是一樣的。setter() 方法的裝飾器為:@屬性名.setter;

class Person:
    def __init__(self,name):
        self.__name = name

    @property
    def name(self):
        print("get_name(self)方法執行了")
        return self.__name
  
    @name.setter
    def name(self,name):
        print("set_name(self,name)方法執行了")
        self.__name = name
  
    @name.deleter
    def name(self):
        print("del_name(self,name)方法執行了")

p = Person("Sakura")
print(p.name,end="\n\n")

p.name = "Mikoto"
print(p.name,end="\n\n")

del p.name

  在一些較老版本的 Python 直譯器中,我們可以透過如下的方法實現。

class Person:
    def __init__(self,name):
        self.__name = name

    def get_name(self):
        print("get_name(self)方法執行了")
        return self.__name
  
    def set_name(self,name):
        print("set_name(self,name)方法執行了")
        self.__name = name
  
    def del_name(self):
        print("del_name(self,name)方法執行了")

    name = property(get_name, set_name, del_name)

p = Person("Sakura")
print(p.name,end="\n\n")

p.name = "Mikoto"
print(p.name,end="\n\n")

del p.name

三、繼承性

3.1、什麼是繼承性

  繼承,其基本思想就是基於某個父類的擴充套件,制定出一個新的子類,子類可以繼承父類原有的屬性和方法,也可以增加原來父類所不具備的屬性和方法,或者直接重寫父類中的某些方法。

  透過繼承可以直接讓子類獲取父類的方法和屬性,避免編寫重複性的程式碼,並且也符合 OCP 原則。所以,我們經常需要透過繼承來對一個類進行擴充套件。

3.2、繼承性的應用

  使用繼承後,我們可以減少程式碼的冗餘,提高了程式碼的複用性,並且使用繼承後,便於功能的擴充。繼承的出現讓類與類之間產生了 is-a 的關係。

  在 Python 中,繼承性的格式如下:

class A(父類):
    pass

  其中,A 類代表 子類(派生類、subclass),B 類代表 父類(基類、superclass)。一旦 子類 A 繼承 父類 B 之後,子類 A 中就獲取 父類 B 中宣告的所有的結構:屬性、方法(包括特殊方法);子類繼承父類以後,還可以宣告自己特有的屬性和方法,實現功能的擴充;

class Animal:
    def run(self):
        print("動物在跑")

    def sleep(self):
        print("動物在睡覺")

class Dog(Animal):
    def bark(self):
        print("汪汪汪")

dog = Dog()

dog.run()
dog.sleep()
dog.bark()
print()

# isinstance()檢查一個物件是否是一個類的例項
# 如果這個類是這個物件的父類,也會返回true
print(isinstance(dog,Dog))
print(isinstance(dog,Animal))
print()

# issubclass()檢查一個類是或否是另一個類的子類
print(issubclass(Dog,Animal))

如果在建立類時,省略了父類,則預設父類為 object,它是所有類的父類,所有類都繼承 object;

3.3、方法重寫

  如果在子類中有和父類同名的方法,則透過子類例項呼叫方法時,會呼叫子類的方法而不是父類的方法,這個特點我們成為 方法重寫(覆蓋,override)。

  當我們呼叫一個物件的方法時,它會優先去當前物件中尋找是否具有該方法,如果有則直接呼叫,如果沒有則取當前物件的父類中尋找,如果父類中有則直接呼叫父類中的方法,如果沒有則取父類中的父類中尋找,以此類推,直到找到 object,如果依然沒有找到則報錯。

class Animal:
    def run(self):
        print("動物在跑")

    def sleep(self):
        print("動物在睡覺")

class Dog(Animal):
    def run(self):
        print("狗在跑")

    def bark(self):
        print("汪汪汪")

dog = Dog()

dog.run()
dog.sleep()
dog.bark()

3.4、super()方法的使用

  父類中所有結構:屬性 和 方法(包括特殊方法)都會被子類繼承。如果我們希望直接呼叫父類中的結構,我們可以使用 super() 方法獲取當前類的父類,並且透過 super() 呼叫父類方法時,不需要傳遞 self。

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

    @property
    def name(self):
        return self.__name
  
    @name.setter
    def name(self,name):
        self.__name = name

class Dog(Animal):
    def __init__(self,name,age):
        # 直接呼叫父類中的__init__(self)來初始化父類中定義的屬性
        super().__init__(name)
        self.__age = age

    @property
    def age(self):
        return self.__age
  
    @age.setter
    def age(self,age):
        self.__age = age

dog = Dog("旺財",3)
print("name: ", dog.name, ", age: ", dog.age)

3.5、多重繼承

  在 Python 中是支援多重繼承的,也就是我們可以為一個類同時指定多個父類。我們可以在類名的 () 後面新增多個類,來實現多重繼承。多重繼承會使子類同時擁有多個父類,並且或獲取所有父類中的方法。如果多個父類中有同名的方法,則會先在第一個父類中尋找,然後找第二個,然後找第三個,...,前面父類的方法會覆蓋後面父類的方法。

class A:
    def test(self):
        print("AAA")

class B:
    def test(self):
        print("BBB")

class C(A,B):
    pass

# 類名.__bases__這個屬性可以獲取當前類的所有父類
print(C.__bases__)
# 我們可以透過類.mro()檢視屬性的查詢順序
print(C.mro())

c = C()
c.test()

super() 在呼叫父類的時候,它需要計算出當前到底呼叫哪個父類,在 Python 中實現這個功能的演算法叫 C3 演算法;

  在 Python 中子類可以同時繼承多個父類的屬性,這樣可以最大限度地重用程式碼,但是使用多繼承會導致擴充套件性變差,並且有可能會導致稜形問題(鑽石問題)。稜形問題(鑽石問題)指的是一個子類繼承的多個父類彙集到一個非 object 類的身上。
稜形問題

class A:
    def test(self):
        print("from A")

class B(A):
    def test(self):
        print("from B")

class C(A):
    def test(self):
        print("from C")

class D(B):
    def test(self):
        print("from D")

class E(C):
    def test(self):
        print("from E")

class F(A):
    def test(self):
        print("from F")

class G(D,E,F):
    pass

# 類名.__bases__這個屬性可以獲取當前類的所有父類
print(G.__bases__)
# 我們可以透過類.mro()檢視屬性的查詢順序
print(G.mro())
obj = G()
obj.test()

  如果真的涉及到一個子類不可避免使用多個父類的屬性,應該使用 Mixins 機制。Mixin 類表示某一功能,而不是某個物品,Python 對於Mixin 類的命名方式一般以 Mixin,able,ible 為字尾。我們可以將相關的功能放在一個 Mixin 類中,如果有多個不同功能,那可以寫多個 Mixin 類,一個類可以繼承多個 Mixin 類,但應該只繼承一個表示其歸屬含義的父類,以確保遵循繼承的 "is-a" 原則。Mixin 類不應該依賴於子類的實現,子類即便沒有繼承這個 Mixin 類,也可以照常工作,只是缺少了某個功能而已。

class Vehicle:
    pass

class FlyerMixin:
    def fly(self):
        pass

class CivilAircraft(FlyerMixin,Vehicle):
    pass

class Helicopter(FlyerMixin,Vehicle):
    pass

class Car(Vehicle):
    pass

四、多型性

  多型性指的是一個事物的多種形態;同樣的行為(函式),傳入不同的物件,得到不同的狀態;多型性指的是可以在不考慮物件具體型別的情況下而直接使用物件。

  它的格式如下:

class A:
    def __init__(self,name):
        self.__name = name

    @property
    def name(self):
        return self.__name
  
    @name.setter
    def name(self,name):
        self.__name = name
  
class B:
    def __init__(self,name):
        self.__name = name

    @property
    def name(self):
        return self.__name
  
    @name.setter
    def name(self,name):
        self.__name = name

a = A("Sakura")
b = B("Mikoto")

# 對於say_hello()這個函式來說,只要物件中含有name屬性,它就可以作為引數傳遞
# 這個函式並不會考慮物件的型別,只要有name屬性即可
def say_hello(obj):
    print("你好,%s" %obj.name)

say_hello(a)
say_hello(b)
import abc

# 使用模組abc統一所有子類的標準
class Animal(metaclass=abc.ABCMeta):

    @abc.abstractmethod
    def speak(self):
        pass

class Dog(Animal):
    def speak(self):
        print("汪汪汪!")

class Cat(Animal):
    def speak(self):
        print("喵喵喵!")

def make_noise(animal):
    animal.speak()

#animal = Animal()       # 不能例項化抽象類自己

dog = Dog()
cat = Cat()

make_noise(dog)
make_noise(cat)

看上去呼叫相同的方法,但實際上需要這個看這個物件是父類還是子類建立的物件,如果是父類建立的物件,一定呼叫父類中定義的方法,如果是子類建立的物件,那麼就要看子類是否重寫了父類的方法,如果子類要是重寫了父類的方法,那麼會呼叫子類的方法,如果子類沒有重寫父類方法,那麼會呼叫父類的方法。

相關文章