24 - 面向對象基礎-多繼承-super-mro-Mixin

1 類的繼承

        繼承是面向對象的重要特性之一,是相對兩個類而言的父子關係,子類繼承了父類的全部的屬性和方法,繼承最大的好處是實現了代碼的重用,能夠重用已經存在的數據和行爲,減小代碼的重複編寫。算法

2 不一樣版本的類

        在Python2.2以前,類是沒有共同的祖先的,以後,引入了object類,它是全部類的共同祖先類。Python2中爲了兼容,分爲古典類(舊式類)和新式類。而在Python 3中所有都爲新式類,新式類都是繼承object類的,而且可使用super函數(後面會說)。下面是Python2.x中的代碼設計模式

class A:
    pass

class B(object):
    pass

>>> dir(A)  # 查看類的__dict__
['__doc__', '__module__']
>>> dir(B)
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']

在Python2.x中 A和B是不一樣的兩個類。A沒有繼承,被稱爲古典類,B繼承自object,被稱爲新式類。不止少了不少方法,連實例對象的屬性也是不太相同的。Python 3中的代碼以下ssh

class A:
    pass

class B(object):
    pass
 
>>> dir(A)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
>>> dir(B)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
>>>

在Python 3中,都爲新式類,因此A和A(object)是兩個結果相同的不一樣寫法而已 class A:pass == class A(object):passide

更多的區別這裏就不在詳述,Python 3是將來,忘記舊式類吧。函數

3 基本概念

Python在類名後使用一對括號來表示繼承關係,括號中的類即爲父類。先來看看不用繼承的例子:優化

class Animal:

    def shout(self):
        print('{} shout'.format(self.__class__.__name__))

class Cat:
    def shout(self):
        print('{} shout'.format(self.__class__.__name__))

class Dog:
    def shout(self):
        print('{} shout'.format(self.__class__.__name__))

a = Animal()
c = Cat()
d = Dog()

a.shout()
c.shout()
d.shout()

從上面例子來看,雖然貓狗動物均可以叫,可是卻分別實現了叫這個動做,那麼下一步使用繼承來優化.net

class Animal:

    def shout(self):
        print('{} shout'.format(self.__class__.__name__))

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

a = Animal()
c = Cat()
d = Dog()

a.shout()
c.shout()
d.shout()

經過繼承,貓類、狗類就不用寫代碼,直接繼承了父類Animal類的叫方法了。因此,在上面的例子中:設計

  • 父類:Animal是Cat和Dog的父類,也成爲基類、超類
  • 子類:Cat和Dog是Animal的子類,也成爲派生類

4 特殊屬性和方法

和繼承相關的經常使用特殊屬性和方法以下:code

特殊屬性和方法 含義 示例
__base__ 類的基類
__bases__ 類的基類們(元組)
__mro__ 方法解析順序(基類們的元組)
mro() 方法解析順序(基類們的列表) int.mro()
__subclasses__() 類的子類列表 int.__subclasses__()

5 繼承中的訪問控制

經過一個例子來看繼承中的訪問控制

class Animal:
    __COUNT = 100   # _Animal__COUNT = 100
    HEIGHT = 0

    def __init__(self, age,weight,height):
        self.__COUNT += 1  # self._Animal_COUNT = self._Animal_COUNT + 1
        self.age = age
        self.__weight = weight  # self._Animal__weight = weight
        self.HEIGHT = height

    def eat(self):
        print('{} eat'.format(self.__class__.__name__))

    def __getweight(self):    # def _Animal__getweight(self):
        print(self.__weight)  #     print(self._Animal__weight)

    @classmethod
    def showcount1(cls):
        print(cls)
        print(cls.__dict__)
        print(cls.__COUNT)    # print(cls._Animal__COUNT)

    @classmethod
    def __showcount2(cls):    # def _Animal__showcount2(cls):
        print(cls.__COUNT)    #     print(cls._Animal__COUNT)

    def showcount3(self):
        print(self.__COUNT)   # print(self._Animal__COUNT)

class Cat(Animal):
    NAME = 'CAT'
    __COUNT = 200   # _Cat__Count = 200

c = Cat(3,5,15)
c.eat()          # 1 
print(c.HEIGHT)  # 2 
print(c.__COUNT) # 3
print('c:',c.__dict__)
print('cat:',Cat.__dict__)
print('Animal:',Animal.__dict__)
c.showcount1()   # 4
c.showcount2()   # 5
c.showcount3()   # 6
print(c.NAME)    # 7

分析:

  1. 經過c調用eat方法,c沒有eat方法,在類Cat中尋找,Cat中沒有,在Cat的父類Animal中尋找,找到eat方法,把c綁定在eat上,執行是把c傳入eat,在eat內部self是c,因此self.__class__就是Cat,因此會打印'Cat eat'
  2. 在查找屬性時,優先在c.__dict__中尋找,由於c實例化時設置了HEIGHT屬性,因此,這裏是15
  3. __COUNT是私有屬性,定義完畢後會被更名,因此在外部訪問時,會提示沒有該屬性
  4. 使用c調用類方法時,@classmethod會把c的類傳入到cls中去,因此cls就是Cat類,cls.__dict__就是Cat.__dict__,而Cat中只定義兩個屬性,因此值爲{'__module__': '__main__', 'NAME': 'CAT', '_Cat__COUNT': 200, '__doc__': None}(魔術方法先不考慮),類方法定義在哪一個類中,那麼私有變量就會被改爲以那個類爲前綴的變量名,因此showcount1方法定義在Animal中,__COUNT就變成了_Animal__COUNT。cls.__COUNT就是在尋找cls._Animal__COUNT屬性,而Cat類中的__COUNT被更名爲_Cat__COUNT,因此最後只能在Animal中找到,因此值爲100
  5. 和私有屬性相同,私有方法也會被更名爲_類名__方法,因此直接從外部訪問,會提示沒有該屬性方法
  6. showcount3定義在Animal中,因此self.__COUNT實際上爲self._Animal__COUNT,c在實例化的同時進行了初始化,Cat沒了__init__函數,因此繼承了父類Animal的init,在初始化過程當中定義的self.__COUNT,實際上就是self._Animal__COUNT,這裏self._Animal__COUNT = self._Animal__COUNT + 1,先算等式右邊,在執行時c尚未_Animal__COUNT屬性,因此會從Cat類開始找直到Animal類,Cat類的__COUNT更名爲了_Cat__COUNT,不是咱們想要的,而後找到了Animal的100,而後加1,再賦給實例c(等於c._Animal__COUNT = Animal._Animal__COUNT + 1),因此實例c來講,它本身就已經擁有_Animal__COUNT屬性,它的值爲101

    通常狀況下不會這麼寫,這裏只是練習知識點。分析這種狀況時,直接把私有變量/方法更名後就很是好分析了。

總結:

  1. 從父類繼承,本身沒有的,就能夠到父類中尋找。
  2. 私有的都是不能夠直接在外部進行訪問的,可是本質上依然是改了名稱後放在這個屬性所在的類或實例的__dict__中,若是知道這個新名稱,就能夠直接找到這個隱藏的變量。不建議使用。
  3. 繼承時,公有的,子類和實例均可以隨意訪問;私有成員被吟唱,子類和實例不可直接訪問,但私有變量所在的類內的方法能夠直接訪問這個私有變量。
  4. 實例屬性查找順序:實例的__dict__ , 類的__dict__ , 父類的__dict__(若是有繼承)。

    遇到私有變量/方法看定義的位置,直接進行更名就比較好分析了。

6 方法的重寫(override)

        方法重寫,顧名思義就是重寫繼承來的方法,Python和其餘語言不一樣的是,在Java中,要重寫的方法,參數數量和類型要和原方法徹底相同才行,不然會被認爲是方法重載。Python因爲其動態的語言的特性,只要方法相同,則表示的就是方法重寫。

class Animal:

    def __init__(self, name):
        self.name = name

    def shout(self):
        print('Animal shout')

class Cat(Animal):

    def shout(self):
        print('miaomiao')

c = Cat('daxin')
c.shout()

        上面的例子中,Cat繼承了Animal類的shout方法,可是因爲Animal中定義的shout不符合Cat的需求,因此在Cat中重寫了shout方法,可是須要注意的是,重寫不是覆蓋,嚴格來講的話應該算是遮蓋,由於Python的類查找順序是按照當前實例->父類->基類等來的,因此當給Cat類定義shout方法後,實例調用方法shout時,父類中已經包含了shout方法,因此就直接調用了,而Animal中的shout依舊在,只是Cat的shout方法被預先發現了。

        若是咱們並非要改寫,而是要加強原方法的功能呢?

class Animal:

    def shout(self):
        print('Animal shout')

class Cat(Animal):

    def shout(self):
        self.__class__.__base__.shout(self)  # 須要手動傳入self
        print('miaomiao')
c = Cat()
c.shout()

經過查找父類而後傳入實例調用,是能夠的,可是不建議這樣使用,在這種狀況下,咱們通常會使用super.

6.1 super

        super函數(類)是用於調用父類(超類)的一個方法的。主要的兩種寫法以下:

super()   # 指代父類
super(type, obj)  # 一樣指代父類,super接受兩個參數,第一個是類型,第二個是實例對象。

super() == super(type, obj)

super(type, obj) 通常寫爲super(self.__class__, self) 按照上面Animal的例子的話,就爲super(Cat, self)

利用super完成加強方法的例子:

class Animal:

    def shout(self):
        print('Animal shout')

class Cat(Animal):

    def shout(self):
        super(Cat, self).shout()
        # super().shout()
        print('miaomiao')
c = Cat()
c.shout()

靜態方法和類方法均可以被覆蓋,原理都相同,都是在屬性字典__dict__中搜索。

6.2 繼承中的初始化

class Animal:

    def __init__(self, name):
        self.name = name

    def shout(self):
        print('Animal shout')

class Cat(Animal):

    def __init__(self, age):
        self.age = age 

    def shout(self):
        print('miaomiao')

c = Cat('daxin',20)
c.shout()
print(c.name)

請問這種狀況是能夠執行嗎?答案是不行的,由於Cat重寫了__init__方法,因此在c實例化時,只能訪問Cat類的__init__方法,因此,就須要顯示的調用父類的__init__方法。

class Animal:

    def __init__(self, name):
        self.name = name

    def shout(self):
        print('Animal shout')

class Cat(Animal):

    def __init__(self, name, age):
        super(Cat, self).__init__(name)   # 等於把實例和name變量,傳遞給Animal.__init__(self,name)
        # super().__init__(name)    #  效果相同
        self.age = age 

    def shout(self):
        print('miaomiao')

c = Cat('daxin',20)
c.shout()

須要注意的是,若是有同名屬性,那麼後執行的會覆蓋先執行的。

def __init__(self, name, age):
    super(Cat, self).__init__(name)
    self.name = age     # 覆蓋父類初始化的name屬性

另外在私有屬性繼承的狀況下,請注意真正的變量名稱(由於會更名)。

7 多繼承

        一個類繼承自多個類就是多繼承,它將具備多個類的特徵。面向對象的設計的開閉原則(實體應該對擴展開放,而對修改封閉),就能夠利用繼承來設計,即多用'繼承',少修改(並非通常的多繼承,後面會詳述)。它的定義個數以下

class MyClass(A,B,...):pass

        Python中的繼承關係,分爲多繼承和單繼承,如圖所示:
duojicheng

7.1 多繼承弊端

        多繼承很好的模擬了世界,由於事物不多是單一繼承,可是捨棄簡單,必然引入複雜性,帶來了衝突。舉個簡單的例子:若是一個孩子繼承了來自父母雙方的特稱,那麼到底眼睛像爸爸仍是媽媽呢,孩子究竟像誰多一點呢?

多繼承的實現或致使編譯器設計的複雜度增長,因此如今不少語言也捨棄了類的多繼承。

        C++支持多繼承,而Java捨棄了多繼承。有些人說Java支持的多繼承的,其實他說的是接口,在Java中,一個類能夠實現多個接口,一個接口也能夠繼承多個接口。Java的接口很純粹,只是方法的生命,繼承者必須實現這些方法,就具備了這些能力,就能幹什麼。
        多繼承帶來的問題,最主要的就是二義性,例如貓和狗都繼承自動物類,如今若是一個類繼承了貓和狗類,貓和狗都有shout方法,子類究竟繼承誰的shout呢?

實現多繼承的語言,要解決二義性,主要有兩種方法深度優先廣度優先

7.2 MRO

        MRO:方法解析順序,Python使用MRO來解決多繼承帶來的二義性問題。由於Python 2.x的舊式類和新式類等歷史緣由(舊式類不會繼承object對想),MRO有三個搜索算法:

  1. 經典算法(2.2以前):按照定義從左至右,深度優先策略。左圖的MRO爲:MyClass、D、B、A、C
  2. 新式類算法(2.2版本):經典算法的升級,深度優先,重複的只保留最後一個。左圖的MRO爲:MyClass、D、B、C、A、object
  3. C3算法(2.3以後):在類被建立出來的時候,就計算一個MRO有序列表。Python3惟一支持的算法。左圖的MRO爲:MyClass、D、B、C、A、object

        經典算法有很大的問題。若是C中有覆蓋A的方法,就不會訪問到C,由於深度優先,會先訪問A。新式類算法算法,依然採用了深度優先,解決了重複問題,可是同經典算法同樣,麼有解決繼承和單調性的問題。

單調性:若是在D的解析順序中,B排在A的前面,那麼在D的全部子類裏,也必須知足這個順序。

        C3算法,解決了繼承的單調性,它阻止建立以前版本產生的二義性代碼,求得MRO是爲了線性化,且肯定了順序。關於MRO和C3能夠參考:Python的多重繼承問題-MRO和C3算法

class A:pass
class B(A):pass
class C(A):pass
class D(B):pass
class MyClass(D,C):pass

print(MyClass.mro())  # [<class '__main__.MyClass'>, <class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

注意:

  1. MyClass.mro() 和 MyClass.__mro__結果相同,一個是方法,一個是屬性。
  2. D的解析順序中,B在A的前面,那麼在D的全部資料,都將保持這個順序。
  3. 序列是有序的,當執行一個方法,子類不存在時,會按照mro列表開始尋找。

7.3 多繼承的建議

當類不少,繼承很複雜的狀況下,繼承路徑太多,很難說清什麼樣的繼承路線。Python語法是容許多繼承的,可是Python代碼是解釋執行,只有執行時,才發現錯誤,若是團隊協做開發,而且引入多繼承,那麼代碼將有可能會變得不可控。因此在Python平常開發規範中建議:

  1. 避免多繼承
  2. 因爲Python語言自己過於靈活,因此最好遵循必定的團隊開發規範。

7.4 Mixin

從一個需求開始瞭解Mixin。現有以下繼承關係:
Mixin
假設已經有了三個類:

class Animal:

    def __init__(self, name):
        self.name = name

    def talk(self):  # 抽象方法,沒有真正被實現,須要子類本身去實現
        raise NotImplementedError()   # raise表示拋出異常,NotImplementedError表示沒有被實現。

class Human(Animal):
    pass

class Monkey(Animal):
    pass

解釋:

  1. 沒有實現的方法稱爲抽象方法,擁有抽象方法的類,叫作抽象類(抽象類不是用來實例化的,而是用來繼承的,因此又叫作抽象基類)
  2. 子類直接執行talk方法時會產生異常(方法沒有被實現)

    Python中若是採用上面的方式定義抽象方法,子類能夠不實現,可是到子類使用該方法的時候纔會報錯。

        Animal類是抽象基類,基類的方法能夠不具體實現,由於它未必適合全部子類,在子類中通常須要重寫。Human類和Monkey類屬於Animal的子類,如今須要爲Human類添加說話的功能,該怎麼辦呢?若是在Humman類上直接添加,雖然能夠,可是卻違反了OCP原則,因此咱們只能繼承了

Mixin-2

下面對代碼進行改寫

class Animal:

    def __init__(self, name):
        self.name = name

    def talk(self):  # 抽象方法,沒有真正被實現,須要子類本身去實現
        raise NotImplementedError()

class Human(Animal):
    pass

class Monkey(Animal):
    pass

class TalkHuman(Human):

    def talk(self):
        print('{} say something'.format(self.name))

daxin = TalkHuman('daxin')
daxin.talk()

疑問:看似完成了需求,可是若是Human又要唱歌、跳舞、吃飯等方法呢?每次都要繼承嗎?這樣類會不會太多了?可否用其餘的方法呢?

7.4.1 利用裝飾器新增功能

        前面咱們利用裝飾器爲函數新增了功能,在Python中一切皆對象,函數和類都是對象,那麼咱們是否能夠利用裝飾器爲類添加新功能呢?答案固然是能夠的。使用裝飾器爲Human類添加talk方法:

class Animal:

    def __init__(self, name):
        self.name = name

    def talk(self):  # 抽象方法,沒有真正被實現,須要子類本身去實現
        raise NotImplementedError()

def talkhuman(cls):
    # def talk(self):
    #     print('{} say I Love You'.format(self.name))
    # cls.talk = talk
    cls.talk = lambda self: print('{} say I Love You'.format(self.name))
    return cls

@talkhuman  # Human = talk(human)
class Human(Animal):
    pass

class Monkey(Animal):
    pass

daxin = Human('daxin')
daxin.talk()

        使用柯里化很容易就能夠寫出爲類添加方法的裝飾器,這種裝飾器還有一個好處,哪裏須要talk功能,直接裝飾就好。有多個功能的話,那就寫多個裝飾器。

class Animal:

    def __init__(self, name):
        self.name = name

    def talk(self):  # 抽象方法,沒有真正被實現,須要子類本身去實現
        raise NotImplementedError()

def talkhuman(cls):
    cls.talk = lambda self: print('{} say I Love You'.format(self.name))
    return cls

def sleephuman(cls):
    cls.sleep = lambda self: print('{} will sleep with you'.format(self.name))
    return cls

@sleephuman  
@talkhuman  
class Human(Animal):
    pass

class Monkey(Animal):
    pass

daxin = Human('daxin')
daxin.talk()
daxin.sleep()

7.4.2 Mixin類

先來看代碼:

class Animal:

    def __init__(self, name):
        self.name = name

    def talk(self):  # 抽象方法,沒有真正被實現,須要子類本身去實現
        raise NotImplementedError()

class TalkMixin:
    def talk(self):
        print('{} say I Love You too'.format(self.name))

class Human(Animal):
    pass

class Monkey(Animal):
    pass

class TalkMan(TalkMixin, Human):
    pass

daxin = TalkMan('daxin')
daxin.talk()

PS: 感受就是寫了一個類給別人繼承了?

        Mixin體現的就是一種組合的設計模式,本質上就是多繼承實現的。核心思想就是把其它類混合進來,同時帶來了類的屬性和方法。這裏的Mixin看起來和裝飾器的效果是同樣的,也沒什麼特別的,可是Mixin是類,它能夠繼承。

class Animal:

    def __init__(self, name):
        self.name = name

    def talk(self):  # 抽象方法,沒有真正被實現,須要子類本身去實現
        raise NotImplementedError()

class TalkMixin:
    def talk(self):
        print('{} say I Love You'.format(self.name))

class SingMixin(TalkMixin):  # 經過繼承來添加新的功能
    def sing_a_song(self):
        print('{} want sing a song'.format(self.name))
        super(SingMixin, self).talk()
        print('Go out,Now')

class Human(Animal):
    pass

class Monkey(Animal):
    pass

class TalkMan(TalkMixin, Human):
    pass

class SingMan(SingMixin,Human):
    pass

daxin = SingMan('daxin')
daxin.sing_a_song()

使用原則:

  1. Mixin類中不該該顯示的出現__init__初始化方法
  2. Mixin類一般不能獨立工做,由於它是準備混入別的類中的部分功能實現
  3. Mixin類的祖先類也應該是Mixin類
  4. 使用時Mixin類一般在繼承列表的第一個位置

小結:

  1. 在面向對象的設計中,一個複雜的類,每每須要不少功能,而這些功能又來自於不一樣類的提供,這就須要不少的類組合在一塊兒。
  2. 從設計模式的角度來看,多組合,少繼承。
  3. Mixin和裝飾器均可以使用,看我的喜愛,若是還須要繼承,就是用Mixin的方式。
相關文章
相關標籤/搜索