物件導向程式設計基礎
活在當下的程式設計師應該都聽過“物件導向程式設計”一詞,也經常有人問能不能用一句話解釋下什麼是“物件導向程式設計”,我們先來看看比較正式的說法。
把一組資料結構和處理它們的方法組成物件(object),把相同行為的物件歸納為類(class),通過類的封裝(encapsulation)隱藏內部細節,通過繼承(inheritance)實現類的特化(specialization)和泛化(generalization),通過多型(polymorphism)實現基於物件型別的動態分派。
這樣一說是不是更不明白了。所以我們還是看看更通俗易懂的說法,下面這段內容來自於知乎。
說明:以上的內容來自於網路,不代表作者本人的觀點和看法,與作者本人立場無關,相關責任不由作者承擔。
之前我們說過“程式是指令的集合”,我們在程式中書寫的語句在執行時會變成一條或多條指令然後由CPU去執行。當然為了簡化程式的設計,我們引入了函式的概念,把相對獨立且經常重複使用的程式碼放置到函式中,在需要使用這些功能的時候只要呼叫函式即可;如果一個函式的功能過於複雜和臃腫,我們又可以進一步將函式繼續切分為子函式來降低系統的複雜性。但是說了這麼多,不知道大家是否發現,所謂程式設計就是程式設計師按照計算機的工作方式控制計算機完成各種任務。但是,計算機的工作方式與正常人類的思維模式是不同的,如果程式設計就必須得拋棄人類正常的思維方式去迎合計算機,程式設計的樂趣就少了很多,“每個人都應該學習程式設計”這樣的豪言壯語就只能說說而已。當然,這些還不是最重要的,最重要的是當我們需要開發一個複雜的系統時,程式碼的複雜性會讓開發和維護工作都變得舉步維艱,所以在上世紀60年代末期,“軟體危機”、“軟體工程”等一系列的概念開始在行業中出現。
當然,程式設計師圈子內的人都知道,現實中並沒有解決上面所說的這些問題的“銀彈”,真正讓軟體開發者看到希望的是上世紀70年代誕生的Smalltalk程式語言中引入的物件導向的程式設計思想(物件導向程式設計的雛形可以追溯到更早期的Simula語言)。按照這種程式設計理念,程式中的資料和運算元據的函式是一個邏輯上的整體,我們稱之為“物件”,而我們解決問題的方式就是建立出需要的物件並向物件發出各種各樣的訊息,多個物件的協同工作最終可以讓我們構造出複雜的系統來解決現實中的問題。
說明:當然物件導向也不是解決軟體開發中所有問題的最後的“銀彈”,所以今天的高階程式設計語言幾乎都提供了對多種程式設計正規化的支援,Python也不例外。
類和物件
簡單的說,類是物件的藍圖和模板,而物件是類的例項。這個解釋雖然有點像用概念在解釋概念,但是從這句話我們至少可以看出,類是抽象的概念,而物件是具體的東西。在物件導向程式設計的世界中,一切皆為物件,物件都有屬性和行為,每個物件都是獨一無二的,而且物件一定屬於某個類(型)。當我們把一大堆擁有共同特徵的物件的靜態特徵(屬性)和動態特徵(行為)都抽取出來後,就可以定義出一個叫做“類”的東西。
定義類
在Python中可以使用class
關鍵字定義類,然後在類中通過之前學習過的函式來定義方法,這樣就可以將物件的動態特徵描述出來,程式碼如下所示。
class Student(object):
# __init__是一個特殊方法用於在建立物件時進行初始化操作
# 通過這個方法我們可以為學生物件繫結name和age兩個屬性
def __init__(self, name, age):
self.name = name
self.age = age
def study(self, course_name):
print('%s正在學習%s.' % (self.name, course_name))
# PEP 8要求識別符號的名字用全小寫多個單詞用下劃線連線
# 但是很多程式設計師和公司更傾向於使用駝峰命名法(駝峰標識)
def watch_av(self):
if self.age < 18:
print('%s只能觀看《熊出沒》.' % self.name)
else:
print('%s正在觀看島國愛情動作片.' % self.name)複製程式碼
說明:寫在類中的函式,我們通常稱之為(物件的)方法,這些方法就是物件可以接收的訊息。
建立和使用物件
當我們定義好一個類之後,可以通過下面的方式來建立物件並給物件發訊息。
def main(): # 建立學生物件並指定姓名和年齡 stu1 = Student('駱昊', 38) # 給物件發study訊息 stu1.study('Python程式設計') # 給物件發watch_av訊息 stu1.watch_av() stu2 = Student('王大錘', 15) stu2.study('思想品德') stu2.watch_av() if __name__ == '__main__': main() 複製程式碼
訪問可見性問題
對於上面的程式碼,有C++、Java、C#等程式設計經驗的程式設計師可能會問,我們給Student
物件繫結的name
和age
屬性到底具有怎樣的訪問許可權(也稱為可見性)。因為在很多物件導向程式語言中,我們通常會將物件的屬性設定為私有的(private)或受保護的(protected),簡單的說就是不允許外界訪問,而物件的方法通常都是公開的(public),因為公開的方法就是物件能夠接受的訊息。在Python中,屬性和方法的訪問許可權只有兩種,也就是公開的和私有的,如果希望屬性是私有的,在給屬性命名時可以用兩個下劃線作為開頭,下面的程式碼可以驗證這一點。
class Test:
def __init__(self, foo):
self.__foo = foo
def __bar(self):
print(self.__foo)
print('__bar')
def main():
test = Test('hello')
# AttributeError: 'Test' object has no attribute '__bar'
test.__bar()
# AttributeError: 'Test' object has no attribute '__foo'
print(test.__foo)
if __name__ == "__main__":
main()複製程式碼
但是,Python並沒有從語法上嚴格保證私有屬性或方法的私密性,它只是給私有的屬性和方法換了一個名字來“妨礙”對它們的訪問,事實上如果你知道更換名字的規則仍然可以訪問到它們,下面的程式碼就可以驗證這一點。之所以這樣設定,可以用這樣一句名言加以解釋,就是“We are all consenting adults here”。因為絕大多數程式設計師都認為開放比封閉要好,而且程式設計師要自己為自己的行為負責。
class Test:
def __init__(self, foo):
self.__foo = foo
def __bar(self):
print(self.__foo)
print('__bar')
def main():
test = Test('hello')
test._Test__bar()
print(test._Test__foo)
if __name__ == "__main__":
main()複製程式碼
在實際開發中,我們並不建議將屬性設定為私有的,因為這會導致子類無法訪問(後面會講到)。所以大多數Python程式設計師會遵循一種命名慣例就是讓屬性名以單下劃線開頭來表示屬性是受保護的,本類之外的程式碼在訪問這樣的屬性時應該要保持慎重。這種做法並不是語法上的規則,單下劃線開頭的屬性和方法外界仍然是可以訪問的,所以更多的時候它是一種暗示或隱喻,關於這一點可以看看我的《Python - 那些年我們踩過的那些坑》文章中的講解。
物件導向的支柱
物件導向有三大支柱:封裝、繼承和多型。後面兩個概念在下一個章節中進行詳細的說明,這裡我們先說一下什麼是封裝。我自己對封裝的理解是“隱藏一切可以隱藏的實現細節,只向外界暴露(提供)簡單的程式設計介面”。我們在類中定義的方法其實就是把資料和對資料的操作封裝起來了,在我們建立了物件之後,只需要給物件傳送一個訊息(呼叫方法)就可以執行方法中的程式碼,也就是說我們只需要知道方法的名字和傳入的引數(方法的外部檢視),而不需要知道方法內部的實現細節(方法的內部檢視)。
練習
練習1:定義一個類描述數字時鐘
class Clock(object):
"""
數字時鐘
"""
def __init__(self, hour=0, minute=0, second=0):
"""
構造器
:param hour: 時
:param minute: 分
:param second: 秒
"""
self._hour = hour
self._minute = minute
self._second = second
def run(self):
"""走字"""
self._second += 1
if self._second == 60:
self._second = 0
self._minute += 1
if self._minute == 60:
self._minute = 0
self._hour += 1
if self._hour == 24:
self._hour = 0
def __str__(self):
"""顯示時間"""
return '%02d:%02d:%02d' % \
(self._hour, self._minute, self._second)
def main():
clock = Clock(23, 59, 58)
while True:
print(clock)
sleep(1)
clock.run()
if __name__ == '__main__':
main()複製程式碼
練習2:定義一個類描述平面上的點並提供移動點和計算到另一個點距離的方法。
from math import sqrt
class Point(object):
def __init__(self, x=0, y=0):
"""
構造器
:param x: 橫座標
:param y: 縱座標
"""
self.x = x
self.y = y
def move_to(self, x, y):
"""
移動到指定位置
:param x: 新的橫座標
"param y: 新的縱座標
"""
self.x = x
self.y = y
def move_by(self, dx, dy):
"""
移動指定的增量
:param dx: 橫座標的增量
"param dy: 縱座標的增量
"""
self.x += dx
self.y += dy
def distance_to(self, other):
"""
計算與另一個點的距離
:param other: 另一個點
"""
dx = self.x - other.x
dy = self.y - other.y
return sqrt(dx ** 2 + dy ** 2)
def __str__(self):
return '(%s, %s)' % (str(self.x), str(self.y))
def main():
p1 = Point(3, 5)
p2 = Point()
print(p1)
print(p2)
p2.move_by(-1, 2)
print(p2)
print(p1.distance_to(p2))
if __name__ == '__main__':
main()複製程式碼
說明:本章中的插圖來自於Grady Booch等著作的《物件導向分析與設計》一書,該書是講解物件導向程式設計的經典著作,有興趣的讀者可以購買和閱讀這本書來了解更多的物件導向的相關知識。