面向對象(一)|面向對象概念及優點(以py為例)
本文分為如下幾個部分
- 首先說明面向對象是什麼,然後結合實際例子說明面向對象的如下幾個優點
- 方便函數管理
- 數據封裝
- 對象操作
- 最後總結一下面向對象的好處
概念
談到面向對象,很多程序員會拋出三個詞:封裝、繼承和多態;或者說抽象、一切都是對象之類的話,然而這會讓初學者更加疑惑。下面我想通過一個小例子來說明一下
面向對象一般是和面向過程做對比的,下面是一個簡單功能的面向過程和面向對象形式
a = 2n# 要實現 a+2nn# 面向過程的方法nsum(a, 3)n# 面向對象的方法(這條在R中還不可執行,只是類似這個意思)na.sum(3)n
看上去只是調用的形式不同,本質沒有什麼差別,不過當代碼量比較大的時候,面向對象就會讓我們編程的思路更加清晰。我們先根據上面的形式講解一下面向過程和面向對象的差別。
差別一:側重點不同
我們可以把調用函數理解為主謂賓的結構
- 面向過程就是我們平時調用的函數,通常是這種形式:動作(主語,賓語) ,動作是主要的,主語和賓語分別作為參數傳入進行計算
- 而面向對象的重點則在於這個主語,是這個主語調用了特定的動作,再把賓語作為參數實現運算。
差別二:定義方式不同
- 面向過程只要定義一個函數sum,指定它有兩個參數,然後return二者的和即可
- 面向對象則複雜一些。首先要定義一個類,在這個類中定義這個類別可以使用的各種方法(可以理解為函數)。然後產生一個類的實例,用這個實例調用這個方法完成計算
- 舉一個通俗的例子,這裡的類和我們生活中的類沒有什麼區別。比如定義一個「鳥」類,再指定這個類有「飛翔」這個方法(即函數、動作)。然後我們抓到一隻具體的鳥,這隻鳥就是「鳥」類的一個實例,它就可以調用「飛翔」這個方法。如果拉過來一隻狗,它不屬於「鳥」類,就不能調用「飛翔」這個方法。狗可能屬於「狗」類,經過定義就可以調用「叫」這個方法。
- 在sum的例子中,a是一個整數,它是」整數「這個類的一個實例,你也可以定義b=3則是另外一個實例。它們就具備」整數「這個類可以使用的所有方法,比如加一個數,減一個數之類的。而如果 c="hello",它就屬於「字元串」這個類,可以調用字元串的方法
- 這樣做有一個好處,相當於自動對變數進行分類,每一個變數都是一個對象,屬於一個特定的類,它可以調用的方法也都是固定的,這樣我們拿到一個字元串,就知道可以對它進行哪些處理。
差異三:調用
在實際使用中,如果要對許多對象進行相同的操作
- 面向過程就是定義一個函數,或者許多函數,然後對每一個對象都套用這個函數即可實現
- 而面向對象則是定義一個類,指定類的方法,然後每一個對象創建為這個類的實例,實例再調用方法進行計算
當這種需求多起來
- 面向過程會出現這種情況,今天對A類數據創建了一些函數,明天對B類數據創建了另外一些函數,可能第二天的函數還調用了第一天的函數,函數全部放在一起,拿到數據看哪個好用就拿來用,這樣容易亂套。而且每次拿到一個數據都要審視一下之前的這個函數可以處理這個數據嗎,處理完可以得到想要的結果嗎
- 而面向對象則每一個類型的對象的方法都放在一起進行管理,都在這個類之下進行定義,這樣我們只要看這個對象是這個類的,就自然可以調用這個類的方法。
- 有的人可能會想,這樣面向對象豈不是每一個類都要重新實現那些函數,本來可以調用之前定義好了的函數的。然而類的繼承很好地解決了這個問題。比如定義了「鳥」和「狗」類,它們都有「吃」這個方法,但是鳥類可以「飛」,狗類可以「跑」,那麼「吃」這個方法其實不需要在兩個類中分別定義一次。因為可以先定義一個"動物"類,這個類有「吃」「睡」等方法,然後「鳥」和「狗」類分別從這個類繼承下來,便獲得了「動物」這個類的所有方法,然後它們可以在自己的類中定義自己特定的方法「飛」「跑」等
接下來我們用python下的例子更具體地說明面向對象的好處
方便函數管理
定義三個類
class bird(object): # 定義鳥這個類n n def __init__(self, name): # 定義類時需要傳入的參數,這裡指創建實例時需要傳入name參數n self.name = name # 將參數賦值給self.name,成為屬性,後面定義方法時會調用n n def move(self): # 每個類實現一次move方法n print("The bird named",self.name,"is flying")n n nclass dog(object): # 定義狗這個類n n def __init__(self, name):n self.name = namen n def move(self):n print("The dog named",self.name,"is running")n nclass fish(object): # 定義魚這個類n n def __init__(self, name):n self.name = namen n def move(self):n print("The fish named",self.name,"is swimming") n
產生實例
bob = bird("Bob") # 給bob這個變數(對象)傳入「姓名」name參數njohn = bird("John") # 產生兩個bird的實例來對比ndavid = dog("David")nfabian = fish("Fabian")n
每個實例分別調用move方法
bob.move() # 1 標號(方便文字說明) njohn.move() # 2ndavid.move() # 3nfabian.move() # 4n# 得到結果如下n# The bird named Bob is flyingn# The bird named John is flyingn# The dog named David is runningn# The fish named Fabian is swimmingn
首先說明一點,創建實例時要傳入這個對象的各種屬性值,屬性用於存儲這個對象的數據,也是一個對象區別於其他對象的標準。比如上面bob這個對象的name是Bob,john則是John,他們正因為傳入的參數不同才是兩個不同的對象,用相同的方法處理後結果才有差異。我們用同一個函數處理不同的數據也體現在這裡,數據都是通過參數傳入的,定義這個對象時就把數據傳入了,之後再調用方法進行處理,其實是方法在定義時調用了這些屬性(也就是你創建實例時傳入的數據)。
在這個例子中,move方法其實就通過self.name調用了name屬性,運行後"Bob"等才會出現在結果中。實際使用時,傳入的參數肯定不會這麼簡單,可能不會是name這樣的姓名最後輸出出來而已,而是一個數據框,或者一個非常長的待處理字元串,他們都會被後面的方法調用,實現諸如去除缺失值、描述數據、繪圖、建模等方法。
從上面的結果中我們可以看到
- 234的對比表明,不同對象調用同一個方法move,卻得到不同的結果,「鳥」類都是以"the bird named"開頭,「狗」類the dog,「魚」類fish。這是因為它們調用了不同的函數。這樣,一個類似的功能,可以定義一個同名的函數方便記憶,同時在不同類中還可以分別得到妥當的處理
- 12的對比表明,同一類傳入不同數據,也可以定製出差異化的結果。這就和一個函數處理不同數據得到不同結果一樣
如果使用面向過程的方法,則要如下實現
def movebird(name):n print("The bird named",name,"is flying")n ndef movedog(name):n print("The dog named",name,"is running")n ndef movefish(name):n print("The fish named",name,"is swimming")nnbob = "Bob"njohn = "John"ndavid = "David"nfabian = "Fabian"nnmovebird(bob)nmovebird(john)nmovedog(david)nmovefish(fabian)nn# 得到結果n# The bird named Bob is flyingn# The bird named John is flyingn# The dog named David is runningn# The fish named Fabian is swimmingn
這樣我們拿到一個變數bob,還要分清楚它表示「鳥」還是「狗」,然後在決定用movebird函數還是movedog函數。一般我們不會只實現move這一種功能,倘若有一二十種,四五十個函數堆在那裡,得到的數據也有幾十個,還要一一對應就很麻煩。除此之外,這些函數可能相互調用,如果想要將這個函數用到其他項目中,還要去找它調用了哪些函數一起複制到新項目中。如果這個代碼很久沒看了,再回來看更要花費很大的精力來對應。
而面向對象定義這個變數時就是通過類產生實例,要想知道用什麼方法處理只要直接去找那個類的定義,甚至在編輯器的代碼提示中就會直接列出來供你挑選。
所以面向對象無論在編程處理的過程,還是代碼的管理上都有非常大的優勢。
數據封裝
當我們要定義很多函數要調用相同參數時,面向對象的使用會明顯更方便,我們看下面一個例子
定義類如下
class Person: n 細心的讀者可能注意到這裡的定義和前面形式不一樣,沒有object,在這裡說明一下n python對象系統分為經典類和新式類,py2中不加object默認經典類,加了是新式類n py3中則默認不加也是新式類,所以以後我們定義時都不加object(雖然加也是一樣的)n n def __init__(self, name, age, height):n self.name = namen self.age = agen self.height = heightn n def description(self):n print("Description: name is %s, age is %s, height is %s"% (self.name,self.age,self.height))n n def my(self):n print("My name is %s, and height is %s, and age is %s. "% (self.name,self.height,self.age))n
調用實例
bob = Person("Bob",24,170)nmary = Person("Mary",10,160)nbob.description()nbob.my()nmary.description()nmary.my()n# Description: name is Bob, age is 24, height is 170n# My name is Bob, and height is 170, and age is 24. n# Description: name is Mary, age is 10, height is 160n# My name is Mary, and height is 160, and age is 10.n
我們可以看到,創建對象時將數據傳入,之後調用方法則不需要再賦值,每個方法在定義時就已將參數傳入
上面的過程我們用面向過程來實現,下面提供兩種實現方式,都有一定的弊端
第一種,最直接的傳入參數
def description(name, age, height):n print("Description: name is %s, age is %s, height is %s"% (name,age,height))n ndef my(name, age, height):n print("My name is %s, and height is %s, and age is %s. "% (name,height,age))nndescription("Bob",24,170)ndescription("Mary",20,160) nmy("Bob",24,170)nmy("Mary",20,160) n# Description: name is Bob, age is 24, height is 170n# Description: name is Mary, age is 20, height is 160n# My name is Bob, and height is 170, and age is 24. n# My name is Mary, and height is 160, and age is 20. n
上面這一種每次調用函數都要傳入相同的參數,非常麻煩,如果參數更多,對象更多,則非常麻煩
第二種可以不用每次都傳入
bob = dict(name=Bob,age=24,height=170)nmary = dict(name=Mary,age=20,height=160)nndef my(dict):n print("My name is %s, and height is %s, and age is %s. "% (dict[name],dict[height],dict[age]))nndef description(dict):n print("Description: name is %s, age is %s, height is %s"% (dict[name],dict[age],dict[height]))nnmy(bob)ndescription(bob)nmy(mary)ndescription(mary)n# My name is Bob, and height is 170, and age is 24. n# Description: name is Bob, age is 24, height is 170n# My name is Mary, and height is 160, and age is 20. n# Description: name is Mary, age is 20, height is 160n
這種方法其實就是把參數變化一下,但是有一個很大的弊端,這兩個函數的定義中,默認了傳入的字典有name age height這三個鍵,如果沒有就會發生各種錯誤。所以說這個函數的應用其實非常局限,幾乎無法應用在其他地方,這種函數其實比較適合的是封裝在一個類裡面。
對象操作
有時候我們要實現一個對象的動態過程,即一個對象做了什麼,再做什麼,面向過程就很難實現了,看下面一個例子
class Individual:n n def __init__(self,full=10): # 默認值則創建實例時可以不用傳入n self.full = fulln n def fruit(self):n self.full += 1n return self # 使作用完這個方法後還可以調用其他方法n n def meat(self):n self.full += 2n return selfn n def run(self):n self.full -= 3n return selfn
上面定義了一個個體的類,他有一個屬性可以理解成能量,他的方法有吃水果、吃肉、跑步,每一個方法調用後都會改變他的能量值。下面創建一個實例來調用
anyone = Individual()nanyone.full # 10nanyone.meat() # 讓他吃一次肉nanyone.full # 12nanyone.run() # 讓他跑一次nanyone.full # 9nanyone.meat().run() # 吃完再跑nanyone.full # 8n
我們還可以進行區分,定義兩個類,繼承上面的類
class Boy(Individual):n 定義男生一天吃肉跑,再吃再跑再吃n def oneday(self):n self.meat().meat().run().meat().fruit().run().meat()n print(self.full)n nclass Girl(Individual):n 定義女生每天吃的少但是不跑n def oneday(self):n self.meat().fruit()n print(self.full)n
上面兩個類定義了男生、女生一天的動作,通過調用oneday返回他們做一天後剩餘的能量
bob = Boy()nbob.oneday() # 13nmary = Girl()nmary.oneday() # 13n
上面這個過程,如果使用面向過程的方法,則需要對每個對象定義一個專門表示他們能量的變數,再定義函數對這些值進行修改,這會造成變數和參數的極大冗餘,這裡就不再舉例子
用面向對象很多時候調用方法的時候都非常符合人的思維習慣,如上面的做完一件事再做一件事就用.連著寫下去。這樣的類還有很多,比如下面的字元串例子
- 面向對象 a.casefold().join([a,b]).strip().find(c)
- 面向過程 find(strip(join(casefold(a),[a,b])),c)
假設上面的函數都是可用的,面向對象一步一步非常清晰:大小寫轉換-連接ab-去除兩邊空格-找到c,而面向過程則層層嵌套,代碼非常不易讀,如果不嵌套則可能產生許多無用的中間變數(雖然R裡面有管道操作可以解決這個問題,但是不是所有面向過程的語言都有實現這樣的功能的)
總結面向對象編程的好處
- 將對象進行分類,分別封裝它們的數據和可以調用的方法,方便了函數、變數、數據的管理,方便方法的調用(減少重複參數等),尤其是在編寫大型程序時更有幫助。
- 用面向對象的編程可以把變數當成對象進行操作,讓編程思路更加清晰簡潔,而且減少了很多冗餘變數的出現
專欄信息
專欄主頁:Data Analysis
專欄目錄:目錄
版本說明:軟體及包版本說明
推薦閱讀:
※MATLAB高級數據結構連載5: table 2
※MATLAB App Desinger教程連載2:詳解App Designer生成的代碼
※js以變數調用json未知keys的方法?
※教學用的代碼使用面向對象技術合適嗎?