1. 程式人生 > >12.面向對象(繼承/super/接口/抽象類)

12.面向對象(繼承/super/接口/抽象類)

類的初始化 process 麥兜 src 做出 python3 理解 課程 level

面向對象
繼承與派生
繼承
繼承順序
繼承原理
子類調用父類的方法(super)
組合
接口
接口的概念:
接口的概念解釋和使用:
python中的接口:
抽象類

面向對象

繼承與派生

繼承

  1. 什麽是繼承?繼承是一種創建新的類的方式
class A:
    pass
class B(A):
    pass

在python中,新建的類可以繼承自一個或者多個父類,原始類稱為基類或者超類,新建的類稱為派生類或者子類

python中類的繼承分為,單繼承和多繼承。
查看繼承的方法B.__bases__

如果沒有指定基類,python的類會默認集成object類,object是所有python類的基類。

  1. 如何繼承-》如何尋找繼承關系

繼承是一種‘是’的關系:

人類、豬類、狗類都繼承動物類,因而他們都是動物

抽象:抽取類似或者說比較相似的部分

老師是這樣定義這個抽象的,並且還介紹了一個抽象類的概念abc,但是這個抽象的意思翻譯過來就是尋找對象的相似點,將對象根據特征進行歸納總結為類的過程。
在找到相似點(也就是“抽象”)之後,其實對象往往還有細分部分的一些特征,並且會有層級地可以劃分類別
例如:
動物>>>
人/豬/狗>>>
奧巴馬/梅西 // 麥兜/豬八戒 // 史努比/丁丁

由此將對象原本模糊的關註點隔離開,降低復雜度

繼承:基於抽象的結果,通過編程語言去實現,經歷抽象的過程,通過繼承去表達出抽象的結構

  1. 為什麽要用繼承?
    解決代碼重用問題:
    在了解了“繼承就是‘是’的關系”之後,我們會發現,我們定義劃分開的父子類之間,其實是有些包含的、並且共有的功能。
    例如“人”這個類,都是可以“走”、“說”的;動物這個個類,都可以有“吃”的動作。
    由此,我們就可以在父類中定義共同有的方法、屬性,並且讓所有子類都享用。
class hero:
    def __init__(self, name,aggressivity, life_value):
        self.name = name
        self.aggressivity = aggressivity
        self.life_value = life_value
    def attack(self,enemy):
        print(‘hero attack‘)

class garen(hero):
    def __init__(self,name,aggressivity,life_value,script):
        hero.__init__(self, name,aggressivity, life_value)
        self.script = script

這段代碼中,我們就定義了一個英雄類,他的子類蓋倫可以繼承一些他定義的屬性
hero.__init__(self, name,aggressivity, life_value)
這就節約了蓋倫類定義的代碼,並且給以後定義新的子類英雄提供了便利。

但實際上,這種調用方法跟繼承並沒有關系,這種類名.函數的調用方法其實可以調用所有其他類的方法。而且當子類修改父類之後,代碼還要隨之更改,非常麻煩,並不推薦這種方式。

這就是代碼的重用

當然子類也可以有自己的屬性,例如上面代碼中的self.script = script
如果說子類自己定義的屬性或者方法和父類中有重名,那麽就以子類自己定義的為準。

上面使用hero.__init__(self, name,aggressivity, life_value)就是在調用父類函數的方法作為子類定義函數的參數,在使用父類函數的時候,要記得不能再使用self,而是跟上要調用的類的名稱。

派生:
子類繼承了父類的屬性,然後衍生出自己新的屬性,
如果子類衍生出的新的屬性與父類的某個屬性名字相同,
那麽再調用子類的這個屬性,就以子類自己這裏的為準了

繼承順序

class A(object):
    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(D,E):
    # def test(self):
    #     print(‘from F‘)
    pass
f1=F()
f1.test()
print(F.__mro__) #只有新式才有這個屬性可以查看線性列表,經典類沒有這個屬性

#新式類繼承順序:F->D->B->E->C->A
#經典類繼承順序:F->D->B->A->E->C
#python3中統一都是新式類
#pyhon2中才分新式類與經典類

作業分析:

技術分享

  • 新式類的繼承順序總結:
    C3算法非廣度優先,利用MRO順序進行繼承

    “不徹底”,“從左往右”
    向父類遍歷,到達根類的前一個類就往另外的支線遍歷
  • 經典類中的集成順序總結:
    深度優先
    “徹底”,“從左往右”
    向父類遍歷,到達根類才向另外的支線遍歷

繼承原理

每定義一個淚,python會計算出一個方法解析順序(MRO)列表,這個MRO列表就是一個簡單的所有基類的線性順序列表:

PyObject* tp_mro
Tuple containing the expanded set of base types, starting with the type itself and
ending with object, in Method Resolution Order.
以元組的形式返回所有基類的擴展的集合,從他本身的類開始,到object類結束
This field is not inherited; it is calculated fresh by PyType_Ready().
這個字段並不是通過繼承得到的,而是通過by PyType_Ready()方法計算更新的

>>> class tsr:
…     pass
…
>>> str.__mro__
(<class ‘str‘>, <class ‘object‘>)
>>> str.mro()
[<class ‘str‘>, <class ‘object‘>]

為了實現繼承,python會在MRO列表上從左到右開始查找基類,直到找到第一個匹配這個屬性的類為止。
而這個MRO列表的構造是通過一個C3線性化算法來實現的。我們不去深究這個算法的數學原理,它實際上就是合並所有父類的MRO列表並遵循如下三條準則:

  1. 子類會先於父類被檢查
  2. 多個父類會根據它們在列表中的順序被檢查
  3. 如果對下一個類存在兩個合法的選擇,選擇第一個父類

子類調用父類的方法(super)

方法一:
父類名.父類方法()

不多說,參考上面的引用

方法二:
super()
當你使用super()函數時,Python會在MRO列表上繼續搜索下一個類。只要每個重定義的方法統一使用super()並只調用它一次,那麽控制流最終會遍歷完整個MRO列表,每個方法也只會被調用一次(註意註意註意:使用super調用的所有屬性,都是從MRO列表當前的位置往後找,千萬不要通過看代碼去找繼承關系,一定要看MRO列表
思考題:
技術分享
上圖的代碼以及運行結果已經放在圖中,下面的代碼使用的是糾正使用super()的代碼:

class A(object):
    def __init__(self):
        print("enter A")
        super(A, self).__init__()  # new
        print("leave A")

class B(object):
    def __init__(self):
        print("enter B")
        super(B, self).__init__()  # new
        print("leave B")

class C(A):
    def __init__(self):
        print("enter C")
        super(C, self).__init__()
        print("leave C")

class D(A):
    def __init__(self):
        print("enter D")
        super(D, self).__init__()
        print("leave D")
class E(B, C):
    def __init__(self):
        print("enter E")
        super(E, self).__init__()  # change
        print("leave E")

class F(E, D):
    def __init__(self):
        print("enter F")
        super(F, self).__init__()  # change
        print ("leave F")

f = F()

小結:

  1. super並不是一個函數,是一個類名,形如super(B, self)事實上調用了super類的初始化函數,產生了一個super對象;
  2. super類的初始化函數並沒有做什麽特殊的操作,只是簡單記錄了類類型和具體實例;
  3. super(B, self).func的調用並不是用於調用當前類的父類的func函數;
  4. Python的多繼承類是通過mro的方式來保證各個父類的函數被逐一調用,而且保證每個父類函數只調用一次(如果每個類都使用super);
  5. 混用super類和非綁定的函數是一個危險行為,這可能導致應該調用的父類函數沒有調用或者一個父類函數被調用多次。

組合

組合對比繼承來說,也是用來重用代碼,但是組合描述的是一種“有”的關系

老師有課程
學生有成績
學生有課程
學校有老師
學校有學生

#定義課程類
class Course:
    def __init__(self,name,price,period):
        self.name=name
        self.price=price
        self.period=period

#定義老師類
class Teacher:
    def __init__(name,course):
        self.name=name
        self.course=course
#定義學生類
class Student:
    def __init__(name,course):
        self.name=name
        self.course=course
#學生類和老師類中,都有關聯到課程這個屬性
#所以實例化一項課程
python=Course(‘python‘,15800,‘7m‘)
#在實例化學生和老師的時候, 將實例化後的課程作為參數傳入
t1=Teacher(‘egon‘,python)
s1=Student(‘alex‘,python)

#這樣學生和老師就獲得了課程實例的參數,並且可以引用
print(s1.course.name)
print(s1.course.period)

上述代碼就表示了一個“組合”的過程。類之間通過實例化傳參的過程,來互相獲取一些需要的數據,並且建立關系。

接口

繼承有兩種用途:
一:繼承基類的方法,並且做出自己的改變或者擴展(代碼重用)
二:聲明某個子類兼容於某基類,定義一個接口類Interface,接口類中定義了一些接口名(就是函數名)且並未實現接口的功能,子類繼承接口類,並且實現接口中的功能

接口的概念:

=================第一部分:Java 語言中的接口很好的展現了接口的含義: IAnimal.java
/*
* Java的Interface很好的體現了我們前面分析的接口的特征:
* 1)是一組功能的集合,而不是一個功能
* 2)接口的功能用於交互,所有的功能都是public,即別的對象可操作
* 3)接口只定義函數,但不涉及函數實現
* 4)這些功能是相關的,都是動物相關的功能,但光合作用就不適宜放到IAnimal裏面了 */

package com.oo.demo;
public interface IAnimal {
    public void eat();
    public void run(); 
    public void sleep(); 
    public void speak();
}

=================第二部分:Pig.java:豬”的類設計,實現了IAnnimal接口 
package com.oo.demo;
public class Pig implements IAnimal{ //如下每個函數都需要詳細實現
    public void eat(){
        System.out.println("Pig like to eat grass");
    }

    public void run(){
        System.out.println("Pig run: front legs, back legs");
    }

    public void sleep(){
        System.out.println("Pig sleep 16 hours every day");
    }

    public void speak(){
        System.out.println("Pig can not speak"); }
}

=================第三部分:Person2.java
/*
*實現了IAnimal的“人”,有幾點說明一下: 
* 1)同樣都實現了IAnimal的接口,但“人”和“豬”的實現不一樣,為了避免太多代碼導致影響閱讀,這裏的代碼簡化成一行,但輸出的內容不一樣,實際項目中同一接口的同一功能點,不同的類實現完全不一樣
* 2)這裏同樣是“人”這個類,但和前面介紹類時給的類“Person”完全不一樣,這是因為同樣的邏輯概念,在不同的應用場景下,具備的屬性和功能是完全不一樣的 */

package com.oo.demo;
public class Person2 implements IAnimal { 
    public void eat(){
        System.out.println("Person like to eat meat");
    }

    public void run(){
        System.out.println("Person run: left leg, right leg");
    }

    public void sleep(){
        System.out.println("Person sleep 8 hours every dat"); 
    }

    public void speak(){
        System.out.println("Hellow world, I am a person");
    }
}

=================第四部分:Tester03.java
package com.oo.demo;

public class Tester03 {
    public static void main(String[] args) {
        System.out.println("===This is a person==="); 
        IAnimal person = new Person2();
        person.eat();
        person.run();
        person.sleep();
        person.speak();

        System.out.println("\n===This is a pig===");
        IAnimal pig = new Pig();
        pig.eat();
        pig.run();
        pig.sleep();
        pig.speak();
    }
}

java中的interface

接口的概念解釋和使用:

這裏有一篇解釋接口的文章,其中關於什麽時候使用繼承,什麽時候用(組合)接口的方面,我看的模模糊糊。但是說不定以後能看懂:http://blog.csdn.net/xiaoxiongli/article/details/2791853
其中有一段解釋說的很好,用來描述接口的概念真的妙到毫顫,特意摘下來整理,留作以後裝逼用:

一馬平川 19:58:54

接口是對類的抽象,我如果直接跟你說接口編程,你一定不理解,或者說很難理解,因為接口本身是很抽象的東西,現在我舉例跟你說:

“電源插座就是接口”:

比方說,插座有兩孔的,有三孔的,不同的插頭需要不同的插座。
接口就描述了能適應的插頭範圍現在有一種插座是三孔的,但既可以插三孔的,也可插兩孔的,知道麽?
那麽,我們可以說,這個插座設計的好,因為他能適用更廣的範圍。
當然,這個範圍不能超出電源插座這個概念。
如果是用來插筆,做筆筒用,那也不適合。
如果電源插座不但能適用兩孔和三孔的插頭,還能適用筆的話,那麽我們可以肯定的說,這個接口設計的太差了。因為接口(插座)的設計應該是對某一類事物的抽象。而且,接口(插座)實現以後,實現該接口的類(插頭)必須符合接口的定義(插座和插口匹配),而且需要完全符合,一點不符合都不行。

所以實現某個接口的類,必須重寫接口中定義的所有方法。如果你覺得該方法不需要實現,你可以留空,但必須重寫。

看我這句話:“接口只定義了方法的原型,即參數和方法名以及返回值,集成接口的類需要實現它。”

而且,接口(插座)實現以後,實現該接口的類(插頭)必須符合接口的定義(插座和插口匹配)。其實,你會發現插座生產出來後,如果某電器的插頭和插座不匹配,那麽就無法使用該電器了。實際上,你在設計一個接口的時候,很難想到要怎麽去設計,盡管你知道集成這個接口的類是怎麽樣的。就像如果你開一個工廠生產插線板,你在不知道電器,或不完全知道電器的插頭如何設計的時候,你是很難生產出能用的插線板的。

那麽,如何設計插線板呢?或者說如何設計接口呢?

先看看插線板廠商是如何生產的吧。

某天,有人生產一個電器是4個孔的,那就用不了了。這時候,插線板廠商為了生產出一種插線板,能適用於目前的大部分電器,也能適用於將來的電器,他找到了一個機構。機構是專門指定規則的,專門制定協議的。機構叫來了大部分的重要電器廠商的頭頭,和插線板老板一起開了個會。大家為了共同的利益,決定了一份協議。
協議是這樣的:電器廠商以後生產的電器的插頭,只能生產三孔的,但為了兼容目前市場上已有的電器,也能生產兩孔的,但是盡量生產三孔的。而且孔的大小和之間的距離有明確的規定。插線板廠商的插線板也只能有兩孔的和三孔的,而且孔的大小和之間的距離也必須按照協議來生產。
於是問題解決了,而且插線板廠商老板很聰明,他發現可以生產出既可以插兩孔,又可以插三孔的插口,於是他的插線板大賣,他發財了。優秀的接口設計,給他帶來了大大的好處,但他很聰明,他沒忘記如果沒有規範協議的機構,一切都是空白。

再補充幾句吧,不然你還是難以理解。

當你想設計一個接口的時候,你最好先寫幾個將要繼承這個幾口的類,寫幾個只有框架而無實際內容的類,看看他們之間的共性,找到寫接口的點,這就正如找電器老板來開會。寫接口的時候,你需要在之前對接口進行說明,說明接口的適用範圍,以及繼承該接口的註意事項,這就好比請機構來制定協議規範。有了這些以後,你的接口在被使用的時候就不會錯用,在寫繼承該接口的類的時候,也會按照規範完全的匹配接口。

最後一句話,即使你理解了我今晚所講的每一句話,你還是不會寫接口,因為你需要實踐,實踐才會出真知。最後這句話才是至理名言,我說的基本都是空話(在你學會了寫接口後)。


第一次寫接口時,第一個感覺就是,寫接口跟沒寫一樣。定義一個接口,馬上去寫實現類!其實此時就是用著面向過程的思路寫程序,然後掛了個羊頭,說起來怎麽也有個接口了!

今天看了一位老兄寫的對於接口的心得體會,真是太有同感了!

不要為了接口而接口,當你把自己不當做是個程序員來思考時,就能把用人的思想來思考了,你不會寫程序,就不會考慮細節的實現了!此時你所關註的問題就是比較抽象的了,你看這不正符合面向對象的原則嗎?當年張三豐教張無忌打太極就是要把招式全忘了,你要定義接口前就先忘了自己是個程序員吧!

當然不可能有100%的抽象,最終你還是要回到實現細節上來的,可此時你已是學會了太極的張無忌了!

python中的接口:

單純為了代碼重用的繼承,在實際的使用中很容易造成“高耦合”的問題。
而接口繼承,根據接口的概念“給使用接口的對象一個很好的抽象”,將對象的一些特征進行歸一化處理,使外部調用者無需關心細節,可以一視同仁地處理特定接口的所有對象。

但是在python中沒有interface的概念。在python中定義接口,僅僅是看起來像接口:

第一次寫接口時,第一個感覺就是,寫接口跟沒寫一樣。定義一個接口,馬上去寫實現類!其實此時就是用著面向過程的思路寫程序,然後掛了個羊頭,說起來怎麽也有個接口了!

接口提取了一群類共同的函數,可以把接口當做一個函數的集合。

然後讓子類去實現接口中的函數。

這麽做的意義在於歸一化,什麽叫歸一化,就是只要是基於同一個接口實現的類,那麽所有的這些類產生的對象在使用時,從用法上來說都一樣。

歸一化,讓使用者無需關心對象的類是什麽,只需要的知道這些對象都具備某些功能就可以了,這極大地降低了使用者的使用難度。

抽象類

  • 定義:
    抽象類是一個特殊的類,他的特殊之處在於:只能被繼承,不能被實例化

  • 為什麽要有抽象類
    類是對相似的對象進行一個總結
    抽象類就是對有共同點的類的一種抽取總結(不完全說是總結,是因為類和類之間一定存在不同,不能完全統一總結成一類,真的是總結關系的話,應該就是子類和父類的關系了)。

從實現的角度來看,抽象類與普通類的不同在於:
抽象類只能有抽象方法(沒有實現功能),這類不能被實例化,只能被繼承,而且子類必須實現方法
從以上描述來看,抽象類非常類似接口,但是仍然是有差別的。

#_*_coding:utf-8_*_
__author__ = ‘Linhaifeng‘
#一切皆文件
import abc #利用abc模塊實現抽象類

class All_file(metaclass=abc.ABCMeta):
    all_type=‘file‘
    @abc.abstractmethod #定義抽象方法,無需實現功能
    def read(self):
        ‘子類必須定義讀功能‘    #如果子類不實現,就會報出此錯誤,以此來實現類似接口的功能
        pass

    @abc.abstractmethod #定義抽象方法,無需實現功能
    def write(self):
        ‘子類必須定義寫功能‘
        pass

# class Txt(All_file):
#     pass
#
# t1=Txt() #報錯,子類沒有定義抽象方法

class Txt(All_file): #子類繼承抽象類,但是必須定義read和write方法
    def read(self):
        print(‘文本數據的讀取方法‘)

    def write(self):
        print(‘文本數據的讀取方法‘)

class Sata(All_file): #子類繼承抽象類,但是必須定義read和write方法
    def read(self):
        print(‘硬盤數據的讀取方法‘)

    def write(self):
        print(‘硬盤數據的讀取方法‘)

class Process(All_file): #子類繼承抽象類,但是必須定義read和write方法
    def read(self):
        print(‘進程數據的讀取方法‘)

    def write(self):
        print(‘進程數據的讀取方法‘)

wenbenwenjian=Txt()

yingpanwenjian=Sata()

jinchengwenjian=Process()

#這樣大家都是被歸一化了,也就是一切皆文件的思想
wenbenwenjian.read()
yingpanwenjian.write()
jinchengwenjian.read()

print(wenbenwenjian.all_type)
print(yingpanwenjian.all_type)
print(jinchengwenjian.all_type)

由上面的代碼可以看出,抽象類基本上實現了接口的功能,但是抽象類的本質還是一個類。指的是一組淚的相似性,包括數據屬性和函數屬性,而接口只強調函數屬性的相似性。
所以,抽象類是一個介於類和接口之ijede一個概念,同時具備類和接口的部分特性,可以用來實現歸一化設計。

12.面向對象(繼承/super/接口/抽象類)