1. 程式人生 > >6. 面向物件

6. 面向物件

1. 面向物件的程式設計的由來

程式設計正規化

程式設計即寫程式or寫程式碼,具體是指程式設計師用特定的語法+資料結構+演算法編寫程式碼,目的是用來告訴計算機如何執行任務 。
如果把程式設計的過程比喻為練習武功,那麼程式設計正規化指的就是武林中的各種流派,而在程式設計的世界裡最常見的兩大流派便是:面向過程與面向物件。
“功夫的流派沒有高低之分,只有習武的人才有高低之分“,在程式設計世界裡更是這樣,面向過程與面向物件在不同的場景下都各有優劣,誰好誰壞不能一概而論,下面就讓我們來詳細瞭解它們。

面向過程的程式設計

概念:
核心是“過程”二字,“過程”指的是解決問題的步驟,即先幹什麼再幹什麼……,基於面向過程設計程式就好比在設計一條流水線,是一種機械式的思維方式。若程式一開始是要著手解決一個大的問題,面向過程的基本設計思路就是把這個大的問題分解成很多個小問題或子過程,這些子過程在執行的過程中繼續分解,直到小問題足夠簡單到可以在一個小步驟範圍內解決。

優點是:
複雜的問題流程化,進而簡單化(一個複雜的問題,分成一個個小的步驟去實現,實現小的步驟將會非常簡單)
舉個典型的面向過程的例子, 寫一個數據遠端備份程式, 分三步,本地資料打包,上傳至雲伺服器,測試備份檔案可用性。

import os

def data_backup(folder):
    print("找到備份目錄: %s" %folder)
    print('正在備份......')
    zip_file='/tmp/backup20181103.zip'
    print('備份成功,備份檔案為: %s' %zip_file)
    return zip_file

def
cloud_upload(file):
print("\nconnecting cloud storage center...") print("cloud storage connected.") print("upload file...%s...to cloud..." %file) link='http://www.xxx.com/bak/%s' %os.path.basename(file) print('close connection.....') return link def data_backup_test(link): print("\n下載檔案: %s , 驗證檔案是否無損"
%link) def main(): #步驟一:本地資料打包 zip_file = data_backup("c:\\users\\alex\歐美100G高清無碼") #步驟二:上傳至雲伺服器 link=cloud_upload(zip_file) #步驟三:測試備份檔案的可用性 data_backup_test(link) if __name__ == '__main__': main()

缺點是:

一套流水線或者流程就是用來解決一個問題,比如生產汽水的流水線無法生產汽車,即便是能,也得是大改,改一個元件,與其相關的元件都需要修改,牽一髮而動全身,擴充套件性極差。
比如我們修改了步驟二的函式cloud_upload的邏輯,那麼依賴於步驟二結果才能正常執行的步驟三的函式data_backup_test相關的邏輯也需要修改,這就造成了連鎖反應,而這一弊端會隨著程式的增大而變得越發的糟糕,我們程式的維護難度將會越來越大。

import os

def data_backup(folder):
    print("找到備份目錄: %s" %folder)
    print('正在備份......')
    zip_file='/tmp/backup20181103.zip'
    print('備份成功,備份檔案為: %s' %zip_file)
    return zip_file

def cloud_upload(file): #加上異常處理,在出現異常的情況下,沒有link返回
    try:
        print("\nconnecting cloud storage center...")
        print("cloud storage connected.")
        print("upload file...%s...to cloud..." % file)
        link = 'http://www.xxx.com/bak/%s' % os.path.basename(file)
        return link
    except Exception:
        print('upload error')
    finally:
        print('close connection.....')

def data_backup_test(link): #加上對引數link的判斷
    if link:
        print("\n下載檔案: %s , 驗證檔案是否無損" %link)
    else:
        print('\n連結不存在')
def main():
    #步驟一:本地資料打包
    zip_file = data_backup("c:\\users\\alex\歐美100G高清無碼")

    #步驟二:上傳至雲伺服器
    link=cloud_upload(zip_file)

    #步驟三:測試備份檔案的可用性
    data_backup_test(link)

if __name__ == '__main__':
    main()

應用場景:

面向過程的程式設計思想一般用於那些功能一旦實現之後就很少需要改變的場景, 如果你只是寫一些簡單的指令碼,去做一些一次性任務,用面向過程的方式是極好的,著名的例子有Linux核心,git,以及Apache HTTP Server等。但如果你要處理的任務是複雜的,且需要不斷迭代和維護 的, 那還是用面向物件最方便了。

2. 面向物件的程式設計

概念:

核心是“物件”二字,要理解物件為何物,必須把自己當成上帝,在上帝眼裡,世間存在的萬物皆為物件,不存在的也可以創造出來。程式設計師基於面向物件設計程式就好比如來設計西遊記,如來要解決的問題是把經書傳給東土大唐,如來並沒有考慮問題的解決流程,而是設計出了負責取經的師傅四人:唐僧,沙和尚,豬八戒,孫悟空,負責騷擾的一群妖魔鬼怪,以及負責保駕護航的一眾神仙,這些全都是物件,然後取經開始,就是師徒四人與妖魔鬼怪神仙互動著直到完成取經任務。所以說基於面向物件設計程式就好比在創造一個世界,世界是由一個個物件組成,而你就是這個世界的上帝。

我們從西遊記中的任何一個人物物件都不難總結出:物件是特徵與技能的結合體。比如孫悟空的特徵是:毛臉雷公嘴,技能是:七十二變、火眼金睛等。

與面向過程機械式的思維方式形成鮮明對比,面向物件更加註重對現實世界而非流程的模擬,是一種“上帝式”的思維方式。

優點是:

解決了面向過程可擴充套件性低的問題,這一點我們將在5.2小節中為大家驗證,需要強調的是,對於一個軟體質量來說,面向物件的程式設計並不代表全部,面向物件的程式設計只是用來解決擴充套件性問題。

缺點是:
程式設計的複雜度遠高於面向過程,不瞭解面向物件而立即上手並基於它設計程式,極容易出現過度設計的問題,而且在一些擴充套件性要求低的場景使用面向物件會徒增程式設計難度,比如管理linux系統的shell指令碼程式就不適合用面向物件去設計,面向過程反而更加適合。

應用場景:
當然是應用於需求經常變化的軟體中,一般需求的變化都集中在使用者層,網際網路應用,企業內部軟體,遊戲等都是面向物件的程式設計大顯身手的好地方。

3. 類與物件

類即類別、種類,是面向物件設計最重要的概念,物件是特徵與技能的結合體,而類則是一系列物件相似的特徵與技能的結合體

那麼問題來了,先有的一個個具體存在的物件(比如一個具體存在的人),還是先有的人類這個概念,這個問題需要分兩種情況去看

在現實世界中:先有物件,再有類

世界上肯定是先出現各種各樣的實際存在的物體,然後隨著人類文明的發展,人類站在不同的角度總結出了不同的種類,如人類、動物類、植物類等概念

也就說,物件是具體的存在,而類僅僅只是一個概念,並不真實存在

在程式中:務必保證先定義類,後產生物件

這與函式的使用是類似的,先定義函式,後呼叫函式,類也是一樣的,在程式中需要先定義類,後呼叫類

不一樣的是,呼叫函式會執行函式體程式碼返回的是函式體執行的結果,而呼叫類會產生物件,返回的是物件

按照上述步驟,我們來定義一個類(我們站在老男孩學校的角度去看,在座的各位都是學生)

在現實世界中:先有物件,再有類

#在現實世界中,站在老男孩學校的角度:先有物件,再有類
物件1:李坦克
    特徵:
        學校=oldboy
        姓名=李坦克
        性別=男
        年齡=18
    技能:
        學習
        吃飯
        睡覺

物件2:王大炮
    特徵:
        學校=oldboy
        姓名=王大炮
        性別=女
        年齡=38
    技能:
        學習
        吃飯
        睡覺

物件3:牛榴彈
    特徵:
        學校=oldboy
        姓名=牛榴彈
        性別=男
        年齡=78
    技能:
        學習
        吃飯
        睡覺

現實中的老男孩學生類
    相似的特徵:
        學校=oldboy
    相似的技能:
        學習
        吃飯
        睡覺

在程式中:先定義類,後產生物件

#在程式中,務必保證:先定義(類),後使用(產生物件)
PS:
  1. 在程式中特徵用變數標識,技能用函式標識
  2. 因而類中最常見的無非是:變數和函式的定義

#程式中的類
class OldboyStudent:
    school='oldboy'
    def learn(self):
        print('is learning')

    def eat(self):
        print('is eating')

    def sleep(self):
        print('is sleeping')



#注意:
  1.類中可以有任意python程式碼,這些程式碼在類定義階段便會執行
  2.因而會產生新的名稱空間,用來存放類的變數名與函式名,可以通過OldboyStudent.__dict__檢視
  3.對於經典類來說我們可以通過該字典操作類名稱空間的名字(新式類有限制),但python為我們提供專門的.語法
  4.點是訪問屬性的語法,類中定義的名字,都是類的屬性

#程式中類的用法
.:專門用來訪問屬性,本質操作的就是__dict__
OldboyStudent.school #等於經典類的操作OldboyStudent.__dict__['school']
OldboyStudent.school='Oldboy' #等於經典類的操作OldboyStudent.__dict__['school']='Oldboy'
OldboyStudent.x=1 #等於經典類的操作OldboyStudent.__dict__['x']=1
del OldboyStudent.x #等於經典類的操作OldboyStudent.__dict__.pop('x')


#程式中的物件
#呼叫類,或稱為例項化,得到物件
s1=OldboyStudent()
s2=OldboyStudent()
s3=OldboyStudent()

#如此,s1、s2、s3都一樣了,而這三者除了相似的屬性之外還各種不同的屬性,這就用到了__init__
#注意:該方法是在物件產生之後才會執行,只用來為物件進行初始化操作,可以有任意程式碼,但一定不能有返回值
class OldboyStudent:
    ......
    def __init__(self,name,age,sex):
        self.name=name
        self.age=age
        self.sex=sex
    ......


s1=OldboyStudent('李坦克','男',18) #先呼叫類產生空物件s1,然後呼叫OldboyStudent.__init__(s1,'李坦克','男',18)
s2=OldboyStudent('王大炮','女',38)
s3=OldboyStudent('牛榴彈','男',78)


#程式中物件的用法
#執行__init__,s1.name='牛榴彈',很明顯也會產生物件的名稱空間
s2.__dict__
{'name': '王大炮', 'age': '女', 'sex': 38}

s2.name #s2.__dict__['name']
s2.name='王三炮' #s2.__dict__['name']='王三炮'
s2.course='python' #s2.__dict__['course']='python'
del s2.course #s2.__dict__.pop('course')

PS:

  1. 站的角度不同,定義出的類是截然不同的,詳見面向物件實戰之需求分析
  2. 現實中的類並不完全等於程式中的類,比如現實中的公司類,在程式中有時需要拆分成部門類,業務類……
  3. 有時為了程式設計需求,程式中也可能會定義現實中不存在的類,比如策略類,現實中並不存在,但是在程式中卻是一個很常見的類
#python為類內建的特殊屬性
類名.__name__# 類的名字(字串)
類名.__doc__# 類的文件字串
類名.__base__# 類的第一個父類(在講繼承時會講)
類名.__bases__# 類所有父類構成的元組(在講繼承時會講)
類名.__dict__# 類的字典屬性
類名.__module__# 類定義所在的模組
類名.__class__# 例項對應的類(僅新式類中)

!!!補充說明:從程式碼級別看面向物件 !!!
資料與專門操作該資料的功能組合到一起

#1、在沒有學習類這個概念時,資料與功能是分離的
def exc1(host,port,db,charset):
    conn=connect(host,port,db,charset)
    conn.execute(sql)
    return xxx


def exc2(host,port,db,charset,proc_name)
    conn=connect(host,port,db,charset)
    conn.call_proc(sql)
    return xxx

# 每次呼叫都需要重複傳入一堆引數
exc1('127.0.0.1',3306,'db1','utf8','select * from tb1;')
exc2('127.0.0.1',3306,'db1','utf8','儲存過程的名字')




# 2、我們能想到的解決方法是,把這些變數都定義成全域性變數
HOST=‘127.0.0.1’
PORT=3306
DB=‘db1CHARSET=‘utf8def exc1(host,port,db,charset):
    conn=connect(host,port,db,charset)
    conn.execute(sql)
    return xxx


def exc2(host,port,db,charset,proc_name)
    conn=connect(host,port,db,charset)
    conn.call_proc(sql)
    return xxx

exc1(HOST,PORT,DB,CHARSET,'select * from tb1;')
exc2(HOST,PORT,DB,CHARSET,'儲存過程的名字')


#3、但是2的解決方法也是有問題的,按照2的思路,我們將會定義一大堆全域性變數,這些全域性變數並沒有做任何區分,即能夠被所有功能使用,然而事實上只有HOSTPORTDBCHARSET是給exc1exc2這兩個功能用的。言外之意:我們必須找出一種能夠將資料與操作資料的方法組合到一起的解決方法,這就是我們說的類了

class MySQLHandler:
    def __init__(self,host,port,db,charset='utf8'):
        self.host=host
        self.port=port
        self.db=db
        self.charset=charset
    def exc1(self,sql):
        conn=connect(self.host,self.port,self.db,self.charset)
        res=conn.execute(sql)
        return res


    def exc2(self,sql):
        conn=connect(self.host,self.port,self.db,self.charset)
        res=conn.call_proc(sql)
        return res


obj=MySQLHandler('127.0.0.1',3306,'db1')
obj.exc1('select * from tb1;')
obj.exc2('儲存過程的名字')


#改進
class MySQLHandler:
    def __init__(self,host,port,db,charset='utf8'):
        self.host=host
        self.port=port
        self.db=db
        self.charset=charset
        self.conn=connect(self.host,self.port,self.db,self.charset)
    def exc1(self,sql):
        return self.conn.execute(sql)

    def exc2(self,sql):
        return self.conn.call_proc(sql)


obj=MySQLHandler('127.0.0.1',3306,'db1')
obj.exc1('select * from tb1;')
obj.exc2('儲存過程的名字')

4. 屬性查詢

類有兩種屬性:資料屬性和函式屬性

  1. 類的資料屬性是所有物件共享的

  2. 類的函式屬性是繫結給物件用的

#類的資料屬性是所有物件共享的,id都一樣
print(id(OldboyStudent.school))

print(id(s1.school))
print(id(s2.school))
print(id(s3.school))

'''
4377347328
4377347328
4377347328
4377347328
'''



#類的函式屬性是繫結給物件使用的,obj.method稱為繫結方法,記憶體地址都不一樣
#ps:id是python的實現機制,並不能真實反映記憶體地址,如果有記憶體地址,還是以記憶體地址為準
print(OldboyStudent.learn)
print(s1.learn)
print(s2.learn)
print(s3.learn)
'''
<function OldboyStudent.learn at 0x1021329d8>
<bound method OldboyStudent.learn of <__main__.OldboyStudent object at 0x1021466d8>>
<bound method OldboyStudent.learn of <__main__.OldboyStudent object at 0x102146710>>
<bound method OldboyStudent.learn of <__main__.OldboyStudent object at 0x102146748>>
'''

在obj.name會先從obj自己的名稱空間裡找name,找不到則去類中找,類也找不到就找父類…最後都找不到就丟擲異常

練習:編寫一個學生類,產生一堆學生物件,要求有一個計數器(屬性),統計總共例項了多少個物件

5. 繫結到物件的方法的特殊之處

#改寫
class OldboyStudent:
    school='oldboy'
    def __init__(self,name,age,sex):
        self.name=name
        self.age=age
        self.sex=sex
    def learn(self):
        print('%s is learning' %self.name) #新增self.name

    def eat(self):
        print('%s is eating' %self.name)

    def sleep(self):
        print('%s is sleeping' %self.name)


s1=OldboyStudent('李坦克','男',18)
s2=OldboyStudent('王大炮','女',38)
s3=OldboyStudent('牛榴彈','男',78)

類中定義的函式(沒有被任何裝飾器裝飾的)是類的函式屬性,類可以使用,但必須遵循函式的引數規則,有幾個引數需要傳幾個引數

OldboyStudent.learn(s1) #李坦克 is learning
OldboyStudent.learn(s2) #王大炮 is learning
OldboyStudent.learn(s3) #牛榴彈 is learning

類中定義的函式(沒有被任何裝飾器裝飾的),其實主要是給物件使用的,而且是繫結到物件的,雖然所有物件指向的都是相同的功能,但是繫結到不同的物件就是不同的繫結方法

強調:繫結到物件的方法的特殊之處在於,繫結給誰就由誰來呼叫,誰來呼叫,就會將‘誰’本身當做第一個引數傳給方法,即自動傳值(方法init也是一樣的道理)

s1.learn() #等同於OldboyStudent.learn(s1)
s2.learn() #等同於OldboyStudent.learn(s2)
s3.learn() #等同於OldboyStudent.learn(s3)

注意:繫結到物件的方法的這種自動傳值的特徵,決定了在類中定義的函式都要預設寫一個引數self,self可以是任意名字,但是約定俗成地寫出self。

類即型別

  提示:python的class術語與c++有一定區別,與 Modula-3更像。

  python中一切皆為物件,且python3中類與型別是一個概念,型別就是類

#型別dict就是類dict
>>> list
<class 'list'>

#例項化的到3個物件l1,l2,l3
>>> l1=list()
>>> l2=list()
>>> l3=list()

#三個物件都有繫結方法append,是相同的功能,但記憶體地址不同
>>> l1.append
<built-in method append of list object at 0x10b482b48>
>>> l2.append
<built-in method append of list object at 0x10b482b88>
>>> l3.append
<built-in method append of list object at 0x10b482bc8>

#操作繫結方法l1.append(3),就是在往l1新增3,絕對不會將3新增到l2l3
>>> l1.append(3)
>>> l1
[3]
>>> l2
[]
>>> l3
[]
#呼叫類list.append(l3,111)等同於l3.append(111)
>>> list.append(l3,111) #l3.append(111)
>>> l3
[111] 

6. 物件之間的互動

class Garen:        #定義英雄蓋倫的類,不同的玩家可以用它例項出自己英雄;
    camp='Demacia'  #所有玩家的英雄(蓋倫)的陣營都是Demacia;
    def __init__(self,nickname,aggressivity=58,life_value=455): #英雄的初始攻擊力58...;
        self.nickname=nickname  #為自己的蓋倫起個別名;
        self.aggressivity=aggressivity #英雄都有自己的攻擊力;
        self.life_value=life_value #英雄都有自己的生命值;
    def attack(self,enemy):   #普通攻擊技能,enemy是敵人;
        enemy.life_value-=self.aggressivity #根據自己的攻擊力,攻擊敵人就減掉敵人的生命值。

我們可以仿照garen類再建立一個Riven類

class Riven:
    camp='Noxus'  #所有玩家的英雄(銳雯)的陣營都是Noxus;
    def __init__(self,nickname,aggressivity=54,life_value=414): #英雄的初始攻擊力54;
        self.nickname=nickname  #為自己的銳雯起個別名;
        self.aggressivity=aggressivity #英雄都有自己的攻擊力;
        self.life_value=life_value #英雄都有自己的生命值;
    def attack(self,enemy):   #普通攻擊技能,enemy是敵人;
        enemy.life_value-=self.aggressivity #根據自己的攻擊力,攻擊敵人就減掉敵人的生命值。

'''例項出倆英雄'''
g1=Garen('草叢倫')
r1=Riven('銳雯雯')

'''互動:銳雯雯攻擊草叢倫,反之一樣'''
g1.life_value      # ==> 455
r1.attack(g1)
g1.life_value      # ==> 401

補充:
  garen_hero.Q()稱為向garen_hero這個物件傳送了一條訊息,讓他去執行Q這個功能,類似的有:
  garen_hero.W()
  garen_hero.E()
  garen_hero.R()