從零學Python:第十六課-物件導向程式設計入門

千鋒Python唐小強發表於2020-07-15

物件導向程式設計是一種非常流行的 程式設計正規化(programming paradigm),所謂程式設計正規化就是 程式設計的方法學,也就是程式設計師對程式的認知和理解。

前面的課程中我們說過“ 程式是指令的集合”,執行程式時,程式中的語句會變成一條或多條指令,然後由CPU(中央處理器)去執行。為了簡化程式的設計,我們又講到了函式, 把相對獨立且經常重複使用的程式碼放置到函式中,在需要使用這些程式碼的時候呼叫函式即可。如果一個函式的功能過於複雜和臃腫,我們又可以進一步 將函式進一步拆分為多個子函式來降低系統的複雜性。

不知大家是否發現,我們所謂的程式設計其實是寫程式的人按照計算機的工作方式透過程式碼控制機器完成任務。但是,計算機的工作方式與人類正常的思維模式是不同的,如果程式設計就必須拋棄人類正常的思維方式去迎合計算機,程式設計的樂趣就少了很多,而“每個人都應該學習程式設計”這樣的豪言壯語也就只能喊喊口號而已。不是說我們不能按照計算機的工作方式去編寫程式碼,但是當我們需要開發一個複雜的系統時,這種方式會讓程式碼過於複雜,從而導致開發和維護工作都變得舉步維艱,這也就是上世紀60年代末,出現了“軟體危機”、“軟體工程”這些概念的原因。

隨著軟體複雜性的增加,解決“軟體危機”就成了軟體開發者必須直面的問題。誕生於上世紀70年代的Smalltalk語言讓軟體開發者看到了希望,因為它引入了一種新的程式設計正規化叫物件導向程式設計。在物件導向程式設計的世界裡,程式中的 資料和運算元據的函式是一個邏輯上的整體,我們稱之為 物件物件可以接收訊息,解決問題的方法就是 建立物件並向物件發出各種各樣的訊息;透過訊息傳遞,程式中的多個物件可以協同工作,這樣就能構造出複雜的系統並解決現實中的問題。當然,物件導向程式設計的雛形還可以向前追溯到更早期的Simula語言,但這不是我們現在要討論的重點。

說明: 今天我們使用的很多高階程式設計語言都支援物件導向程式設計,但是物件導向程式設計也不是解決軟體開發中所有問題的“銀彈”,或者說在軟體開發這個行業目前還找不到這種所謂的“銀彈”。

類和物件

如果要用一句話來概括物件導向程式設計,我認為下面的說法是相當精準的。

物件導向程式設計:把一組資料和處理資料的方法組成 物件,把行為相同的物件歸納為 ,透過 封裝隱藏物件的內部細節,透過 繼承實現類的特化和泛化,透過 多型實現基於物件型別的動態分派。

這句話對初學者來說可能難以理解,但是我們先為大家圈出幾個關鍵詞:物件(object)、類(class)、封裝(encapsulation)、繼承(inheritance)、多型(polymorphism)。

我們先說說類和物件這兩個詞。在物件導向程式設計中, 類是一個抽象的概念,物件是一個具體的概念。我們把同一類物件的共同特徵抽取出來就是一個類,比如我們經常說的人類,這是一個抽象概念,而我們每個人就是人類的這個抽象概念下的具體的實實在在的存在,也就是一個物件。簡而言之, 類是物件的藍圖和模板,物件是類的例項

在物件導向程式設計的世界中, 一切皆為物件物件都有屬性和行為每個物件都是獨一無二的,而且 物件一定屬於某個類。物件的屬性是物件的靜態特徵,物件的行為是物件的動態特徵。按照上面的說法,如果我們把擁有共同特徵的物件的屬性和行為都抽取出來,就可以定義出一個類。

從零學Python:第十六課-物件導向程式設計入門

定義類

在Python中,可以使用class關鍵字加上類名來定義類,透過縮排我們可以確定類的程式碼塊,就如同定義函式那樣。在類的程式碼塊中,我們需要寫一些函式,我們說過類是一個抽象概念,那麼這些函式就是我們對一類物件共同的動態特徵的提取。寫在類裡面的函式我們通常稱之為 方法,方法就是物件的行為,也就是物件可以接收的訊息。方法的第一個引數通常都是self,它代表了接收這個訊息的物件本身。



class 
Student:


    def study (self, course_name):
       print( f'學生正在學習 {course_name}.')

    def play (self):
       print( f'學生正在玩遊戲.')

建立和使用物件

在我們定義好一個類之後,可以使用構造器語法來建立物件,程式碼如下所示。

stu1 = Student()

stu2 = Student()
print(stu1)    # <__main__.Student object at 0x10ad5ac50>
print(stu2)    # <__main__.Student object at 0x10ad5acd0>
print(hex(id(stu1)), hex(id(stu2)))    # 0x10ad5ac50 0x10ad5acd0

在類的名字後跟上圓括號就是所謂的構造器語法,上面的程式碼建立了兩個學生物件,一個賦值給變數stu1,一個複製給變數stu2。當我們用print函式列印stu1和stu2兩個變數時,我們會看到輸出了物件在記憶體中的地址(十六進位制形式),跟我們用id函式檢視物件標識獲得的值是相同的。現在我們可以告訴大家,我們定義的變數其實儲存的是一個物件在記憶體中的邏輯地址(位置),透過這個邏輯地址,我們就可以在記憶體中找到這個物件。所以stu3 = stu2這樣的賦值語句並沒有建立新的物件,只是用一個新的變數儲存了已有物件的地址。

接下來,我們嘗試給物件發訊息,即呼叫物件的方法。剛才的Student類中我們定義了study和play兩個方法,兩個方法的第一個引數self代表了接收訊息的學生物件,study方法的第二個引數是學習的課程名稱。Python中,給物件發訊息有兩種方式,請看下面的程式碼。

# 透過“類.方法”呼叫方法,第一個引數是接收訊息的物件,第二個引數是學習的課程名稱

Student .study(stu1, 'Python程式設計')    # 學生正在學習 Python程式設計.
# 透過“物件.方法”呼叫方法,點前面的物件就是接收訊息的物件,只需要傳入第二個引數
stu1 .study( 'Python程式設計')             # 學生正在學習 Python程式設計 .

Student .play(stu2)    # 學生正在玩遊戲 .
stu2 .play()           # 學生正在玩遊戲.

初始化方法

大家可能已經注意到了,剛才我們建立的學生物件只有行為沒有屬性,如果要給學生物件定義屬性,我們可以修改Student類,為其新增一個名為__init__的方法。在我們呼叫Student類的構造器建立物件時,首先會在記憶體中獲得儲存學生物件所需的記憶體空間,然後透過自動執行__init__方法,完成對記憶體的初始化操作,也就是把資料放到記憶體空間中。所以我們可以透過給Student類新增__init__方法的方式為學生物件指定屬性,同時完成對屬性賦初始值的操作,正因如此,__init__方法通常也被稱為初始化方法。

我們對上面的Student類稍作修改,給學生物件新增name(姓名)和age(年齡)兩個屬性。



class 
Student:

    """學生"""

    def __init__ (self, name, age):
        """初始化方法"""
       self.name = name
       self.age = age

    def study (self, course_name):
        """學習"""
       print( f' {self.name}正在學習 {course_name}.')

    def play (self):
        """玩耍"""
       print( f' {self.name}正在玩遊戲.')

修改剛才建立物件和給物件發訊息的程式碼,重新執行一次,看看程式的執行結果有什麼變化。

# 由於初始化方法除了
self之外還有兩個引數

# 所以呼叫Student類的構造器建立物件時要傳入這兩個引數
stu1 = Student( '駱昊', 40)
stu2 = Student( '王大錘', 15)
stu1.study( 'Python程式設計')    # 駱昊正在學習Python程式設計.
stu2.play()                    # 王大錘正在玩遊戲.

列印物件

上面我們透過__init__方法在建立物件時為物件繫結了屬性並賦予了初始值。在Python中,以兩個下劃線__(讀作“dunder”)開頭和結尾的方法通常都是有特殊用途和意義的方法,我們一般稱之為 魔術方法魔法方法。如果我們在列印物件的時候不希望看到物件的地址而是看到我們自定義的資訊,可以透過在類中放置__repr__魔術方法來做到,該方法返回的字串就是用print函式列印物件的時候會顯示的內容,程式碼如下所示。



class 
Student:

    """學生"""

    def __init__ (self, name, age):
        """初始化方法"""
       self.name = name
       self.age = age

    def study (self, course_name):
        """學習"""
       print( f' {self.name}正在學習 {course_name}.')

    def play (self):
        """玩耍"""
       print( f' {self.name}正在玩遊戲.')

    def __repr__ (self):
        return f' {self.name}: {self.age}'


stu1 = Student( '駱昊', 40)
print(stu1)         # 駱昊: 40
students = [stu1, Student('王小錘', 16), Student('王大錘', 25)]
print(students)    # [駱昊: 40, 王小錘: 16, 王大錘: 25]

物件導向的支柱

物件導向程式設計有三大支柱,就是我們之前給大家劃重點的時候圈出的三個詞:封裝、繼承和多型。後面兩個概念在下一節課中會詳細說明,這裡我們先說一下什麼是封裝。我自己對封裝的理解是: 隱藏一切可以隱藏的實現細節,只向外界暴露簡單的呼叫介面。我們在類中定義的物件方法其實就是一種封裝,這種封裝可以讓我們在建立物件之後,只需要給物件傳送一個訊息就可以執行方法中的程式碼,也就是說我們在只知道方法的名字和引數(方法的外部檢視),不知道方法內部實現細節(方法的內部檢視)的情況下就完成了對方法的使用。

舉一個例子,假如要控制一個機器人幫我倒杯水,如果不使用物件導向程式設計,不做任何的封裝,那麼就需要向這個機器人發出一系列的指令,如站起來、向左轉、向前走5步、拿起面前的水杯、向後轉、向前走10步、彎腰、放下水杯、按下出水按鈕、等待10秒、鬆開出水按鈕、拿起水杯、向右轉、向前走5步、放下水杯等,才能完成這個簡單的操作,想想都覺得麻煩。按照物件導向程式設計的思想,我們可以將倒水的操作封裝到機器人的一個方法中,當需要機器人幫我們倒水的時候,只需要向機器人物件發出倒水的訊息就可以了,這樣做不是更好嗎?

在很多場景下,物件導向程式設計其實就是一個三步走的問題。第一步定義類,第二步建立物件,第三步給物件發訊息。當然,有的時候我們是不需要第一步的,因為我們想用的類可能已經存在了。之前我們說過,Python內建的list、set、dict其實都不是函式而是類,如果要建立列表、集合、字典物件,我們就不用自定義類了。當然,有的類並不是Python標準庫中直接提供的,它可能來自於第三方的程式碼,如何安裝和使用三方程式碼在後續課程中會進行討論。在某些特殊的場景中,我們會用到名為“內建物件”的物件,所謂“內建物件”就是說上面三步走的第一步和第二步都不需要了,因為類已經存在而且物件已然建立過了,直接向物件發訊息就可以了,這也就是我們常說的“開箱即用”。

經典案例

例子1:定義一個類描述數字時鐘。

import time



# 定義數字時鐘類
class Clock(object):
    "" "數字時鐘" ""

   def __init__( self, hour= 0, minute= 0, second= 0):
        "" "初始化方法
       :param hour: 時
       :param minute: 分
       :param second: 秒
       " ""
        self.hour = hour
        self.min = minute
        self.sec = second

   def run( self):
        "" "走字" ""
        self.sec += 1
        if self.sec == 60:
            self.sec = 0
            self.min += 1
            if self.min == 60:
                self.min = 0
                self.hour += 1
                if self.hour == 24:
                    self.hour = 0

   def show( self):
        "" "顯示時間" ""
        return f'{ self.hour: 0> 2d}:{ self.min: 0> 2d}:{ self.sec: 0> 2d}'


# 建立時鐘物件
clock = Clock( 23, 59, 58)
while True:
   # 給時鐘物件發訊息讀取時間
   print(clock.show())
   # 休眠 1秒鐘
   time.sleep( 1)
   # 給時鐘物件發訊息使其走字
   clock.run()

例子2:定義一個類描述平面上的點,要求提供計算到另一個點距離的方法。



class 
Point
(object):

    """屏面上的點"""

    def __init__ (self, x= 0, y= 0):
        """初始化方法
       :param x: 橫座標
       :param y: 縱座標
       """
       self.x, self.y = x, y

    def distance_to (self, other):
        """計算與另一個點的距離
       :param other: 另一個點
       """
       dx = self.x - other.x
       dy = self.y - other.y
        return (dx * dx + dy * dy) ** 0.5

    def __str__ (self):
        return f'( {self.x}, {self.y})'


p1 = Point( 3, 5)
p2 = Point( 6, 9)
print(p1, p2)
print(p1.distance_to(p2))

簡單的總結

物件導向程式設計是一種非常流行的程式設計正規化,除此之外還有 指令式程式設計函數語言程式設計等程式設計正規化。由於現實世界是由物件構成的,而物件是可以接收訊息的實體,所以 物件導向程式設計更符合人類正常的思維習慣。類是抽象的,物件是具體的,有了類就能建立物件,有了物件就可以接收訊息,這就是物件導向程式設計的基礎。定義類的過程是一個抽象的過程,找到物件公共的屬性屬於資料抽象,找到物件公共的方法屬於行為抽象。抽象的過程是一個仁者見仁智者見智的過程,對同一類物件進行抽象可能會得到不同的結果,如下圖所示。

從零學Python:第十六課-物件導向程式設計入門



來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69923331/viewspace-2704749/,如需轉載,請註明出處,否則將追究法律責任。

相關文章