面向對象(上):從生活中的類比說起-day9

面向對象(上):從生活中的類比說起

寫在前面

你好,我是禪墨!

滴滴~~ 打卡第九天

Python 在 1989 年被一位程序員打發時間創立之後,一步步攻城掠地飛速發展,從最基礎的腳本程序,到後來可以編寫系統程序、大型工程、數據科學運算、人工智能,早已脫離了當初的設計,因此一些其他語言的優秀設計之處依然需要引入。我們必須花費一定的代價掌握面向對象編程,才能跨越學習道路中的瓶頸期,走向下一步。

接下來,我將用兩天來重學面向對象編程,從基礎到實戰。第一天,將快速但清晰地疏通最基礎的知識,確保能夠迅速領略面向對象的基本思想;第二天,我們從零開始寫一個搜索引擎,將前面所學知識融會貫通。這些內容可能和你以往看到的所有教程都不太一樣,我會儘可能從一個初學者的角度來審視這些難點。同時我們面向實戰、面向工程,不求大而全,但是對最核心的思想會有足夠的勾勒。我可以保證內容清晰易懂,但想要真正掌握,仍要求你能用心去閱讀和思考。真正的提高,永遠要靠自己才能做到。

對象,你找到了嗎?

我們先來學習,面向對象編程中最基本的概念。

爲了方便你理解其中的抽象概念,我先打個比方帶你感受一下。生物課上,我們學過“界門綱目科屬種”的概念,核心思想是科學家們根據各種動植物、微生物的相似之處,將其分化爲不同的類型方便研究。生活中我們也是如此,習慣對身邊的事物進行分類:

  • 貓和狗都是動物;
  • 直線和圓都是平面幾何的圖形;
  • 《哈利波特》和《冰與火之歌》(即《權力的遊戲》)都是小說。

那回到我們的 Python 上,又對應哪些內容呢?這裏,我們先來看一段最基本的 Python 面向對象的應用代碼,不要被它的長度嚇到,你無需立刻看懂所有代碼,跟着節奏來,我會一點點爲你剖析。


class Document():
    def __init__(self, title, author, context):
        print('init function called')
        self.title = title
        self.author = author
        self.__context = context # __開頭的屬性是私有屬性

    def get_context_length(self):
        return len(self.__context)

    def intercept_context(self, length):
        self.__context = self.__context[:length]

harry_potter_book = Document('Harry Potter', 'J. K. Rowling', '... Forever Do not believe any thing is capable of thinking independently ...')

print(harry_potter_book.title)
print(harry_potter_book.author)
print(harry_potter_book.get_context_length())

harry_potter_book.intercept_context(10)

print(harry_potter_book.get_context_length())

print(harry_potter_book.__context)

########## 輸出 ##########

init function called
Harry Potter
J. K. Rowling
77
10

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-5-b4d048d75003> in <module>()
     22 print(harry_potter_book.get_context_length())
     23 
---> 24 print(harry_potter_book.__context)

AttributeError: 'Document' object has no attribute '__context'

參照着這段代碼,我先來簡單解釋幾個概念。

  • 類:一羣有着相似性的事物的集合,這裏對應 Python 的 class。
  • 對象:集合中的一個事物,這裏對應由 class 生成的某一個 object,比如代碼中的 harry_potter_book。
  • 屬性:對象的某個靜態特徵,比如上述代碼中的 title、author 和 __context。
  • 函數:對象的某個動態能力,比如上述代碼中的 intercept_context () 函數。

當然,這樣的說法既不嚴謹,也不充分,但如果你對面向對象編程完全不瞭解,它們可以讓你迅速有一個直觀的瞭解。

這裏我想多說兩句。回想起當年參加數學競賽時,我曾和一個大佬交流數學的學習,我清楚記得我們對數學有着相似的觀點:很多數學概念非常抽象,如果純粹從數理邏輯而不是更高的角度去解題,很容易陷入僵局;而具體、直觀的想象和類比,纔是迅速打開數學大門的鑰匙。雖然這些想象和類比不嚴謹也不充分,很多時候甚至是錯誤或者異想天開的,但它們確實能幫我們快速找到正確的大門。

就像很多人都有過的一個疑惑,“學霸是怎樣想到這個答案的?”。德國數學家克萊因曾說過,“推進數學的,主要是那些有卓越直覺的人,而不是以嚴格的證明方法見長的人。”編程世界同樣如此,如果你不滿足於只做一個 CRUD“碼農”,而是想成爲一個優秀的工程師,那就一定要積極鍛鍊直覺思考和快速類比的能力,尤其是在找不到 bug 的時候。這纔是編程學習中能給人最快進步的方法和路徑。

言歸正傳,繼續回到我們的主題,還是通過剛剛那段代碼,我想再給類下一個更爲嚴謹的定義。

類,一羣有着相同屬性和函數的對象的集合。

雖然有循環論證之嫌(lol),但是反覆強調,還是希望你能對面向對象的最基礎的思想,有更真實的瞭解。清楚記住這一點後,接下來,我們來具體解讀剛剛這段代碼。爲了方便你的閱讀學習,我把它重新放在了這段文字下方。


class Document():
    def __init__(self, title, author, context):
        print('init function called')
        self.title = title
        self.author = author
        self.__context = context # __開頭的屬性是私有屬性

    def get_context_length(self):
        return len(self.__context)

    def intercept_context(self, length):
        self.__context = self.__context[:length]

harry_potter_book = Document('Harry Potter', 'J. K. Rowling', '... Forever Do not believe any thing is capable of thinking independently ...')

print(harry_potter_book.title)
print(harry_potter_book.author)
print(harry_potter_book.get_context_length())

harry_potter_book.intercept_context(10)

print(harry_potter_book.get_context_length())

print(harry_potter_book.__context)

########## 輸出 ##########

init function called
Harry Potter
J. K. Rowling
77
10

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-5-b4d048d75003> in <module>()
     22 print(harry_potter_book.get_context_length())
     23 
---> 24 print(harry_potter_book.__context)

AttributeError: 'Document' object has no attribute '__context'

可以看到,class Document 定義了 Document 類,再往下能看到它有三個函數,這三個函數即爲 Document 類的三個函數。

其中,init 表示構造函數,意即一個對象生成時會被自動調用的函數。我們能看到, harry_potter_book = Document(…)這一行代碼被執行的時候,'init function called’字符串會被打印出來。而 get_context_length() 和 intercept_context() 則爲類的普通函數,我們調用它們來對對象的屬性做一些事情。

class Document 還有三個屬性,title、author 和 __context 分別表示標題、作者和內容,通過構造函數傳入。這裏代碼很直觀,我們可以看到, intercept_context 能修改對象 harry_potter_book 的 __context 屬性。

這裏唯一需要強調的一點是,如果一個屬性以 __ (注意,此處有兩個 _) 開頭,我們就默認這個屬性是私有屬性。私有屬性,是指不希望在類的函數之外的地方被訪問和修改的屬性。所以,你可以看到,title 和 author 能夠很自由地被打印出來,但是 print(harry_potter_book.__context)就會報錯。

能不能再給力點?

掌握了最基礎的概念,其實我們已經能做很多很多的事情了。不過,在工程實踐中,隨着複雜度繼續提升,你可能會想到一些問題:

  • 如何在一個類中定義一些常量,每個對象都可以方便訪問這些常量而不用重新構造?
  • 如果一個函數不涉及到訪問修改這個類的屬性,而放到類外面有點不恰當,怎麼做才能更優雅呢?
  • 既然類是一羣相似的對象的集合,那麼可不可以是一羣相似的類的集合呢?

前兩個問題很好解決,不過,它們涉及到一些常用的代碼規範,這裏我放了一段代碼示例。同樣的,你無需一口氣讀完這段代碼,跟着節奏慢慢學習即可。


class Document():
    
    WELCOME_STR = 'Welcome! The context for this book is {}.'
    
    def __init__(self, title, author, context):
        print('init function called')
        self.title = title
        self.author = author
        self.__context = context
    
    # 類函數
    @classmethod
    def create_empty_book(cls, title, author):
        return cls(title=title, author=author, context='nothing')
    
    # 成員函數
    def get_context_length(self):
        return len(self.__context)
    
    # 靜態函數
    @staticmethod
    def get_welcome(context):
        return Document.WELCOME_STR.format(context)


empty_book = Document.create_empty_book('What Every Man Thinks About Apart from Sex', 'Professor Sheridan Simove')


print(empty_book.get_context_length())
print(empty_book.get_welcome('indeed nothing'))

########## 輸出 ##########

init function called
7
Welcome! The context for this book is indeed nothing.

第一個問題,在 Python 的類裏,你只需要和函數並列地聲明並賦值,就可以實現這一點,例如這段代碼中的 WELCOME_STR。一種很常規的做法,是用全大寫來表示常量,因此我們可以在類中使用 self.WELCOME_STR ,或者在類外使用 Entity.WELCOME_STR ,來表達這個字符串。

而針對第二個問題,我們提出了類函數、成員函數和靜態函數三個概念。它們其實很好理解,前兩者產生的影響是動態的,能夠訪問或者修改對象的屬性;而靜態函數則與類沒有什麼關聯,最明顯的特徵便是,靜態函數的第一個參數沒有任何特殊性。

具體來看這幾種函數。一般而言,靜態函數可以用來做一些簡單獨立的任務,既方便測試,也能優化代碼結構。靜態函數還可以通過在函數前一行加上 @staticmethod 來表示,代碼中也有相應的示例。這其實使用了裝飾器的概念,我們會在後面的章節中詳細講解。

而類函數的第一個參數一般爲 cls,表示必須傳一個類進來。類函數最常用的功能是實現不同的 init 構造函數,比如上文代碼中,我們使用 create_empty_book 類函數,來創造新的書籍對象,其 context 一定爲 ‘nothing’。這樣的代碼,就比你直接構造要清晰一些。類似的,類函數需要裝飾器 @classmethod 來聲明。

成員函數則是我們最正常的類的函數,它不需要任何裝飾器聲明,第一個參數 self 代表當前對象的引用,可以通過此函數,來實現想要的查詢 / 修改類的屬性等功能。

繼承,富二代的夢想

接下來,我們來看第三個問題,既然類是一羣相似的對象的集合,那麼可不可以是一羣相似的類的集合呢?

答案是,當然可以。只要抽象得好,類可以描述成任何事物的集合。當然你要小心、嚴謹地去定義它,不然一不小心就會引起第三次數學危機 XD。

類的繼承,顧名思義,指的是一個類既擁有另一個類的特徵,也擁有不同於另一個類的獨特特徵。在這裏的第一個類叫做子類,另一個叫做父類,特徵其實就是類的屬性和函數。


class Entity():
    def __init__(self, object_type):
        print('parent class init called')
        self.object_type = object_type
    
    def get_context_length(self):
        raise Exception('get_context_length not implemented')
    
    def print_title(self):
        print(self.title)

class Document(Entity):
    def __init__(self, title, author, context):
        print('Document class init called')
        Entity.__init__(self, 'document')
        self.title = title
        self.author = author
        self.__context = context
    
    def get_context_length(self):
        return len(self.__context)
    
class Video(Entity):
    def __init__(self, title, author, video_length):
        print('Video class init called')
        Entity.__init__(self, 'video')
        self.title = title
        self.author = author
        self.__video_length = video_length
    
    def get_context_length(self):
        return self.__video_length

harry_potter_book = Document('Harry Potter(Book)', 'J. K. Rowling', '... Forever Do not believe any thing is capable of thinking independently ...')
harry_potter_movie = Video('Harry Potter(Movie)', 'J. K. Rowling', 120)

print(harry_potter_book.object_type)
print(harry_potter_movie.object_type)

harry_potter_book.print_title()
harry_potter_movie.print_title()

print(harry_potter_book.get_context_length())
print(harry_potter_movie.get_context_length())

########## 輸出 ##########

Document class init called
parent class init called
Video class init called
parent class init called
document
video
Harry Potter(Book)
Harry Potter(Movie)
77
120

我們同樣結合代碼來學習這些概念。在這段代碼中,Document 和 Video 它們有相似的地方,都有相應的標題、作者和內容等屬性。我們可以從中抽象出一個叫做 Entity 的類,來作爲它倆的父類。

首先需要注意的是構造函數。每個類都有構造函數,繼承類在生成對象的時候,是不會自動調用父類的構造函數的,因此你必須在 init() 函數中顯式調用父類的構造函數。它們的執行順序是 子類的構造函數 -> 父類的構造函數。

其次需要注意父類 get_context_length() 函數。如果使用 Entity 直接生成對象,調用 get_context_length() 函數,就會 raise error 中斷程序的執行。這其實是一種很好的寫法,叫做函數重寫,可以使子類必須重新寫一遍 get_context_length() 函數,來覆蓋掉原有函數。

最後需要注意到 print_title() 函數,這個函數定義在父類中,但是子類的對象可以毫無阻力地使用它來打印 title,這也就體現了繼承的優勢:減少重複的代碼,降低系統的熵值(即複雜度)。

到這裏,你對繼承就有了比較詳細的瞭解了,面向對象編程也可以說已經入門了。當然,如果你想達到更高的層次,大量練習編程,學習更多的細節知識,都是必不可少的。

最後,我想再爲你擴展一下抽象函數和抽象類,我同樣會用一段代碼來輔助講解。


from abc import ABCMeta, abstractmethod

class Entity(metaclass=ABCMeta):
    @abstractmethod
    def get_title(self):
        pass

    @abstractmethod
    def set_title(self, title):
        pass

class Document(Entity):
    def get_title(self):
        return self.title
    
    def set_title(self, title):
        self.title = title

document = Document()
document.set_title('Harry Potter')
print(document.get_title())

entity = Entity()

########## 輸出 ##########

Harry Potter

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-7-266b2aa47bad> in <module>()
     21 print(document.get_title())
     22 
---> 23 entity = Entity()
     24 entity.set_title('Test')

TypeError: Can't instantiate abstract class Entity with abstract methods get_title, set_title

你應該發現了,Entity 本身是沒有什麼用的,只需拿來定義 Document 和 Video 的一些基本元素就夠了。不過,萬一你不小心生成 Entity 的對象該怎麼辦呢?爲了防止這樣的手誤,必須要介紹一下抽象類。

抽象類是一種特殊的類,它生下來就是作爲父類存在的,一旦對象化就會報錯。同樣,抽象函數定義在抽象類之中,子類必須重寫該函數才能使用。相應的抽象函數,則是使用裝飾器 @abstractmethod 來表示。

我們可以看到,代碼中entity = Entity()直接報錯,只有通過 Document 繼承 Entity 才能正常使用。

這其實正是軟件工程中一個很重要的概念,定義接口。大型工程往往需要很多人合作開發,比如在 Facebook 中,在 idea 提出之後,開發組和產品組首先會召開產品設計會,PM(Product Manager,產品經理) 寫出產品需求文檔,然後迭代;TL(Team Leader,項目經理)編寫開發文檔,開發文檔中會定義不同模塊的大致功能和接口、每個模塊之間如何協作、單元測試和集成測試、線上灰度測試、監測和日誌等等一系列開發流程。

抽象類就是這麼一種存在,它是一種自上而下的設計風範,你只需要用少量的代碼描述清楚要做的事情,定義好接口,然後就可以交給不同開發人員去開發和對接。

總結

到目前爲止,我們一直在強調一件事情:面向對象編程是軟件工程中重要的思想。正如動態規劃是算法中的重要思想一樣,它不是某一種非常具體的技術,而是一種綜合能力的體現,是將大型工程解耦化、模塊化的重要方法。在實踐中要多想,尤其是抽象地想,才能更快掌握這個技巧。

回顧一下今天的內容,我希望你能自己回答下面兩個問題,作爲今天內容的總結,寫在留言區裏。

第一個問題,面向對象編程四要素是什麼?它們的關係又是什麼?

第二個問題,講了這麼久的繼承,繼承究竟是什麼呢?你能用三個字表達出來嗎?

寫在後面

我是禪墨,歡迎給我留言!
微信公衆號:興趣路人甲

這裏是引用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章