1. 程式人生 > >第25天面向物件程式設計詳解之繼承

第25天面向物件程式設計詳解之繼承

面向物件補充知識

面向物件概念

面向物件核心二字在與物件,物件就是特徵和技能的結合體,基於該思想程式設計就好比在建立一個世界,世界上的任何事物都是物件,你就好比是這個世界的上帝,這是一種基於上帝式的思維方式。
優點:擴充套件性強
缺點:程式設計的複雜度要遠遠高於面向過程

問題一:既然面向物件這麼好,我們之後的程式設計是不是都要用面向物件呢?

  不是的,衡量一個軟體的標準除了擴充套件性之外,其實還有很多的方面,如效能,可維護性,可移植性等等,但是面向物件程式設計設計之初就是為了解決擴充套件性的,所以在其他的一些軟體質量的考核上面並沒有想象中的那麼好,因此,如果我們的軟體從一寫出來很長時間都不會再去改動的話,用面向物件程式設計就不太合適了。

類的概念

物件是特徵和技能的結合體,類就是一系列物件相同特徵和技能的結合體。現實生活中  先有物件,隨著人類文明的發展總結出了類。程式碼世界中  我們需要先定義類,然後才能通過類去建立物件。

類的建立過程: 建立一個老男孩選課系統

現實世界的分析:

步驟一:要從需求中分析出物件的特徵和技能

分析物件的特徵與技能是要根據特定的環境下進行分析的,因為我們分析的特徵與技能是希望之後可以使用的,而不是說只是用來看看而已的。

例如:下面分析的一個物件特徵也沒錯,但是就是沒有結合特定的環境下分析的特徵與技能,在選課系統中,對於特徵,我們需要的是一個人的資訊,便於之後查詢,如名字,年齡,性別等等,但是對於外貌的特徵我們是不需要的。對與技能,我們需要的是物件是如何選課的,而不是它是怎麼吃飯和怎麼喝水的。因此在分析需求的時候一定要根據特定的環境來分析物件的特徵和技能。

老男孩選課系統分析
物件1:
    特徵
        兩個耳朵
        一個眼睛
    技能
        吃
        喝
老男孩選課系統分析
學生類
物件1:
    特徵
        學校 school = 'Oldboy'
        姓名 name = '張鐵蛋'
        性別 gender = 'male'
        年齡 age = 12
    技能
        選課
物件2:
    特徵
        學校 school = 'Oldboy'
        姓名 name = '王鐵錘'
        性別 gender = 'female'
        年齡 age = 10
    技能
        選課
教師類
物件3:
    特徵
        學校 school 
= 'Oldboy' 姓名 name = 'egon' 性別 gender = 'male' 年齡 age = 18 級別 level = 10 薪資 salary = 100000 技能 修改分數
屬性的分析

步驟二:尋找相似的特徵和技能

學生類:
相似的特徵
    學校 school = 'Oldboy'
相似的技能
    選課

程式碼世界的分析:

步驟三:根據相似的特徵和技能定義類

當我們尋找出相似的特徵與技能之後我們就可以根據現實世界中的類別來通過class關鍵字建立自己的類
# 根據我們現實世界中分析出來的虛擬碼來定義我們的類
#學生類:
class OldBoyStudent:
    # 相似的特徵
    # 學校  school = 'Oldboy'
    school = 'Oldboy'
    # 相似的技能
    # 選課
    def choose_course(self):
        print('choose_course')

步驟四:根據類建立物件

# 建立一個學生物件
stu1 = OldBoyStudent()

雖然還有很多的細節沒有實現,當物件建立完成之後就代表著我們已經成功的把現實中的內容遷移到了我們程式碼世界中。

類的用途

用途一:
    類本質上就是一個名稱空間,我們可以對該名稱空間進行增刪改查
用途二:
    呼叫類產生物件,執行了兩個步驟
    1. 產生一個空物件obj
    2. 觸發類中__init__方法,OldBoyStudent.__init__(obj)

 用途一:

名稱空間就是名字和空間地址的一一對映關係,在python中我們可以使用自動觸發函式__dict__去檢視當前物件的名稱空間中都有哪些名字,說到這裡,可能你就會意識到,既然名稱空間的儲存是一個字典,那麼對於這個字典的增刪改查是不是就是對名稱空間的增刪改查呢,沒錯,就是這樣的。
例如對於上面的例子,我們要檢視類中的名稱空間
#學生類:
class OldBoyStudent:
    # 相似的特徵
    # 學校  school = 'Oldboy'
    school = 'Oldboy'
    # 相似的技能
    # 選課
    def choose_course(self):
        print('choose_course')
# 建立一個學生物件
# 檢視的兩種方法
print(OldBoyStudent.__dict__['school'])
print(OldBoyStudent.school)
# 增加就是在__dict__中新增一對鍵值對
OldBoyStudent.name = 'egon'  # OldBoyStudent.__dict__['name']  = 'egon'

用途二

物件的建立

在我們上面的例子中發現,類中只有一個屬性school和一個方法choose_course,那麼對於一個物件而言它的名字,年齡,和性別就需要我們每次建立完成之後重新去定義它的屬性。
# 建立學生物件,並且新增屬性stu1 = OldBoyStudent()stu1.name = 'egon'stu1.age = 18stu1.gender = 'male'當我需要再建立一個物件的時候,還需要重新去定義這個屬性,這太麻煩了,因此,python幫我們封裝了一個函式__init__函式,我們可以在建立的時候直接通過傳遞屬性引數進行賦值就可以了
class OldBoyStudent:    school = 'Oldboy'    def __init__(self, name, age, gender):        self.name=name        self.age=age        self.gender=gender             def choose_course(self):        print('choose_course')# 建立學生物件,並且新增屬性stu1 = OldBoyStudent('egon', 11, 'male')

理解:面向物件是更高程度的一種封裝

問題一:當我們有很多此呼叫這個函式的時候,我們都需要去傳遞這樣的一組資料,很麻煩

def exec1(address, port, db, charset, sql):
    print(address, port, db, charset, sql)

# 當我們有很多此呼叫這個函式的時候,我們都需要去傳遞這樣的一組資料,很麻煩
exec1('127.0.0.1', 3306, 'db1', 'utf-8', 'select * from db1')
exec1('127.0.0.1', 3306, 'db1', 'utf-8', 'select * from db1')
exec1('127.0.0.1', 3306, 'db1', 'utf-8', 'select * from db1')
exec1('127.0.0.1', 3306, 'db1', 'utf-8', 'select * from db1')

解決方法一:將函式exec1的引數設定成預設引數,這樣的話雖然解決了上面存在的問題,但是如果一旦出現另一組資料的話一樣是這樣的情況,並沒有太大的進步。

def exec1(address='127.0.0.1', port=3306, db='db1', charset='utf-8', sql='select * from db1'):
    print(address, port, db, charset, sql)
# 雖然說對於第一種的呼叫我們簡化了很多,但是一旦出現另一組資料和之前一樣比較麻煩
exec1()
exec1()
exec1()
exec1()
exec1('192.168.0.1', 3307, 'db2', 'utf-8', 'select * from db1')
exec1('192.168.0.1', 3307, 'db2', 'utf-8', 'select * from db1')
exec1('192.168.0.1', 3307, 'db2', 'utf-8', 'select * from db1')
exec1('192.168.0.1', 3307, 'db2', 'utf-8', 'select * from db1')

解決方法二:將資料定義成變數,通過變數進行傳遞引數,雖然說稍微簡單了一點點,但是當出現兩組或者幾組資料的時候將會變的非常混亂,不僅如此,資料的耦合性非常強。

def exec1(address, port, db, charset, sql):
    print(address, port, db, charset, sql)

HOST='127.0.0.1'
PORT=3306
DB='db1'
CHARSET='utf-8'
SQL='select * from db1'
# 這樣比之前直接傳入資料會稍微簡單一點,但是對於定義的一些變數,我們並不需要它可以被其他的程式所使用
# 因此,我們需要把變數和函式繫結起來
exec1(HOST, PORT, DB, CHARSET, SQL)
exec1(HOST, PORT, DB, CHARSET, SQL)
exec1(HOST, PORT, DB, CHARSET, SQL)
exec1(HOST, PORT, DB, CHARSET, SQL)

解決方法三:通過函式將資料和方法進行繫結,這也是一種面向物件程式設計的一種思想,但是我們一般並不會這樣去寫。

# 通過函式的方式把變數和函式繫結到一塊,其他的函式自然就使用不到此函式內的變數和方法
# 並且通過字典的形式將之前的變數包括到一塊,可以簡化我們函式傳遞的引數
def func():
    obj_dict = {
        'HOST': '127.0.0.1',
        'PORT': 3306,
        'DB': 'db1',
        'CHARSET': 'utf-8',
        'SQL': 'select * from db1',
    }
    def exec1(obj_dict):
        print(obj_dict['HOST'], obj_dict['PORT'], obj_dict['DB'], obj_dict['CHARSET'], obj_dict['SQL'])

    exec1(obj_dict)
func()

解決方法四:通過類的形式來將變數和方法進行繫結

# 並且通過字典的形式將之前的變數包括到一塊,可以簡化我們函式傳遞的引數
class Mysql:
    def __init__(self, host, port, db, charset, sql):
        self.host=host
        self.port=port
        self.db=db
        self.charset=charset
        self.sql=sql

    def exec1(self):
        print(self.host, self.port, self.db, self.charset, self.sql)


mysql_obj = Mysql('127.0.0.1', 3306, 'db1', 'utf-8', 'mysql')
mysql_obj.exec1()
mysql_obj.exec1()
mysql_obj.exec1()
# python3中統一了類與型別的概念
l = list([1, 2, 3])
l.append(4)
list.append(l, 5)
print(l)

# 結果
# [1, 2, 3, 4, 5]
類與型別的概念

總結:

面向物件是一種更高程度的封裝
    在之前沒有面向物件的時候,我們會發現如果需要傳遞引數的時候,我們無非有兩種方式,一種是傳遞資料,一種就是傳遞功能,但是沒有說我可以通過傳遞一個變數,這個變數既有資料又有功能的。也就是說一旦我們需要的引數較多,而且呼叫的次數較為頻繁的時候,我們難免就會產生大量的重複操作,因為我們沒有一個變數可以將其進行封裝。
    而物件呢就是高度封裝了一系列的方法和屬性的變數,我們可以通過傳遞一個物件,就可以獲得它所有的方法和屬性,簡化了我們傳遞引數時的操作。
面向物件的精髓雖在:
  # 掌握了一種方法,能夠把專門的資料和專門的方法整合到一塊,
  # 當我們拿到一個物件的時候不僅僅能夠拿到對應的資料,也能拿到相應的配套方法。

繼承

人生三問

什麼是繼承  繼承是一種遺傳關係,子類可以重用父類中的屬性。  在程式中繼承是一種新建子類的方式,新建立的類稱為子類或者派生類,被繼承的類稱為父類\基類\超類。
為什麼要用繼承
  減少類與類之間的程式碼冗餘的問題怎麼使用繼承  先抽象再繼承只有在python2中才會分新式類和經典類,python3都是新式類新式類:但凡繼承了object類的子類,以及該子類的子子類,...都稱為新式類。經典類:但凡沒有繼承object類的子類,以及該子類的子子類,....都稱為經典類。

繼承概覽:

# 在python3中預設是繼承object類的
# 在python2中預設是沒有繼承的,如果想要繼承要把object傳進去

class Parent1:
    pass

class Parent2:
    pass

class Sub1(Parent1):
    pass

class Sub2(Parent1, Parent2):
    pass

print(Sub1.__bases__)   # __bases__顯示的是當前子類繼承的父類
print(Sub2.__bases__)
print(Parent2.__bases__)  # 在python3中預設是繼承object類的
print(Parent1.__bases__)
# 結果:
# (<class '__main__.Parent1'>,)
# (<class '__main__.Parent1'>, <class '__main__.Parent2'>)
# (<class 'object'>,)
# (<class 'object'>,)

屬性的查詢順序

情況一:單繼承問題查詢順序

情況二:非菱形多繼承問題

情況三:菱形多繼承問題

python2中是深度優先查詢(經典類)

python3中是廣度優先查詢(新式類)

 

總結:只有在python2中的菱形問題才會出現深度查詢。

案例:繼承是如何解決程式碼冗餘問題的

首先建立了一個學生類和一個教師類

# 根據之前寫的Oldboy選課系統來說
class OldBoyStudent:
    school = 'OldBoy'

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

    def choose_course(self):
        print('choose_course')


class OldBoyTeacher:
    school = 'OldBoy'

    def __init__(self, name, age, gender, level, salary):
        self.name=name
        self.age=age
        self.gender=gender
        self.level=level
        self.salary=salary

    def change_score(self, stu, score):
        print('change score')
學生類和教師類

問題一:我們發現這兩個類中有些重複的程式碼,如他們的共有屬性school = 'OldBoy',為了簡化程式碼,我們需要抽象一個父類,將共有屬性放進去,然後通過繼承讓兩個類可以獲得相應的屬性值。

# 建立一個類然後讓學生類和教師類繼承
class OldBoyPerson:    school = 'OldBoy'# 根據之前寫的Oldboy選課系統來說class OldBoyStudent(OldBoyPerson):    # school = 'OldBoy'  因為父類中有屬性,所以這裡就不需要了    def __init__(self, name, age, gender):        self.name=name        self.age=age        self.gender=gender    def choose_course(self):        print('choose_course')class OldBoyTeacher(OldBoyPerson):    # school = 'OldBoy'    def __init__(self, name, age, gender, level, salary):        self.name=name        self.age=age        self.gender=gender        self.level=level        self.salary=salary    def change_score(self, stu, score):        print('change score')

問題二:這樣確實是減少了子類中的公共屬性,但是我們發現在__init__方法中也有一部分是重複的,對於這樣的重複的選項我們應該怎麼去減少呢

步驟一:先將學生類和教師類中相同的引數的name, age, gender提取出來放在父類中的__init方法中

步驟二: 通過不同的方法讓子類中的init方法中接受到傳遞過來的額外的引數

方法一:指名道姓的通過類去找到相應的方法實現

class OldBoyPerson:
    school = 'OldBoy'
    def __init__(self, name, age, gender):
        self.name=name
        self.age=age
        self.gender=gender


# 根據之前寫的Oldboy選課系統來說
class OldBoyStudent(OldBoyPerson):
    def choose_course(self):
        print('choose_course')


class OldBoyTeacher(OldBoyPerson):
    def __init__(self, name, age, gender, level, salary):
        # 此處就是指名道姓的要呼叫父類中的__init__方法
        # 此時父類中的__init__方法就是一個普通的方法,我們需要把四個引數全部傳進去
        # 這樣就達到了繼承的效果了
        OldBoyPerson.__init__(self, name, age, gender) 
        self.level=level
        self.salary=salary

    def change_score(self, stu, score):
        print('change score')
方法一與繼承無關,只是可以達到繼承的目的而已

方法二:嚴格依照繼承的方法去繼承用到函式super

class OldBoyPerson:
    school = 'OldBoy'
    def __init__(self, name, age, gender):
        self.name=name
        self.age=age
        self.gender=gender


# 根據之前寫的Oldboy選課系統來說
class OldBoyStudent(OldBoyPerson):
    def choose_course(self):
        print('choose_course')


class OldBoyTeacher(OldBoyPerson):
    def __init__(self, name, age, gender, level, salary):
        # 引數一是當前類,引數二當前物件
        super(OldBoyTeacher, self).__init__(name, age, gender)
        self.level=level
        self.salary=salary

    def change_score(self, stu, score):
        print('change score')
super函式嚴格按照繼承關係

難點:

# super(OldBoyTeacher, self)在python3中不需要傳遞引數,它會建立一個特殊的物件
# 該物件是強調: super()函式會嚴格按照類的mro列表的順序依次查詢屬性

例題:

#A沒有繼承B,
class A:
    def test(self):
        print('A.test')  # 首先列印
        # 當執行到super函式的時候,會安好mro列表的順序去查詢
        # 當前mro列表已經執行到A,所以下一個查詢地方是B因此會執行B類的test方法
        super().test()   
class B:
    def test(self):
        print('from B')  # 所以列印了
class C(A,B):
    pass

c=C()  # 首先建立物件
# 1. 物件中沒有此方法
# 2. 去C類中查詢,沒有找到
# 3. 去父類A中查詢,有test,開始執行test函式
c.test()
print(C.mro())