前言
前兩天一位小伙伴問了這樣一個問題:雖然已經使用python一年多了,也用python寫過很多腳本,代碼量從幾十行到上千行的也有,但從未使用過類(class),似乎用函數(def)就能解決所有問題,使用類有什么好處?我什么時候該用類呢?
關於這個問題,算是困惑了許多剛接觸python的同學,那么本文就嘗試從多個角度來解讀這個問題。首先還是先來看看官方給出類與函數的解釋。
類提供了一種組合數據和功能的方法。 創建一個新類意味着創建一個新的對象類型,從而允許創建一個該類型的新實例 。 每個類的實例可以擁有保存自己狀態的屬性。 一個類的實例也可以有改變自己狀態的(定義在類中的)方法。
函數的本質就是一段有特定功能、可以重復使用的代碼,這段代碼已經被提前編寫好了,並且為其起一個“好聽”的名字。在后續編寫程序過程中,如果需要同樣的功能,直接通過起好的名字就可以調用這段代碼。
很顯然,這樣的答案並沒有讓人搞明白類和函數到底不一樣在哪里。但是里面提到了類是創建一個對象,所以類是面向對象程序設計(Object Oriented Programming)。也就是我們常說的OOP。而OOP高度關注的是代碼的組織,可重用性和封裝。
第一個例子
上面的官方解釋上去還是很抽象,那么我們開始說人話。簡單來說當Python中沒有可以完全表達我們要表示的內容的數據類型時,那么就需要使用一個類。來看下面的例子。
如果我正在計算某人的年齡,則只需使用int 因為它可以滿足我的需求。如果我們需要在游戲中表示像敵人之類的東西,則可以創建一個類則可以創建一個類Enemy,其中包含諸如health和armor的數據,並包含諸如fire_weapon射擊時的功能。然后,我們還可以創建另一個類FlyingEnemy,Enemy該類從該類繼承所有內容,但又具有一個fly方法,因此具有其他功能。
第二個例子
我們再來看一個例子。假設我們需要編寫一個音樂播放器。在這個播放器中,我們有關於不同類型數據的信息,如歌曲、專輯、藝術家和播放列表。還有一些可以播放歌曲、播放專輯、播放藝術家或播放播放列表的功能。我們將每種數據存儲在字典中,不同類型的數據有不同的字段名,因為每個play函數需要做不同的事情,所以我們就有四個不同的函數:
some_song = {
"title": "Yellow Submarine",
"artist": the_beatles, # 指向到包含該藝術家的詞典
"album": yellow_submarine_album, # 指向包含此相冊的dict的鏈接
"duration": insert_time_object_here,
"filepath": "path/to/file/on/disk"
}
# 其他數據類型的結構也類似
# 一些函數
def play_song(song):
# 獲取歌的路徑
path = song["filepath"]
# 播放路徑
call_some_library_function(path)
def play_album(album):
# 找到專輯里所有的歌曲
# 分別調用play_song
def play_artist(artist):
# 找到這位藝術家所有的專輯
# 分別調用play_album
def play_playlist(playlist):
# 找到播放列表中的所有歌曲
# 分別調用play_song
這樣寫有什么不好?我們有四個非常相似的函數,每個函數都與特定類型的數據相關。你必須把它們叫做不同的東西,而不僅僅是play,你必須確保你把正確的數據傳遞給它們。雖然這四種不同的類型都可以“播放”,但是沒有一種通用的方法可以在不知道它是什么的情況下播放任何東西。那么在OOP下,怎么實現呢:
class Song:
def __init__(self, title, artist, album, duration, filepath):
self.title = title
self.artist = artist
self.album = album
self.duration = duration
self.filepath = filepath
def play(self):
path = self.filepath
call_some_library_function(path)
這樣就定義了如何創建一個新的Song對象。該方法將字段值作為參數,並將它們作為對象的屬性賦值。self是一個特殊參數(名稱不保留;它可以被稱為任何東西),它是對對象本身的引用。是一種從同一對象的其他方法內部訪問屬性和方法的方法。當我們從對象外部訪問它們時(要使用play方法時將執行此操作),則可以使用在該范圍內為對象指定的任何名稱。
那么在之前:
# some_song是上面定義的歌
play_song(some_song)
在使用class之后:
# self參數沒有在這里傳遞;它會自動添加
some_song = Song("Yellow Submarine",
the_beatles,
yellow_submarine_album,
insert_time_object_here,
"path/to/file/on/disk"
)
some_song.play()
為什么這樣更好?如果我們有一個對象,則不必知道它是什么就可以播放,因為現在播放任何內容的語法都是相同的:anyobject.play()即對象“知道”如何使用“自己的”數據進行處理的設計思想。無需從外部檢查對象是否具有某些字段並決定如何處理這些內部字段,而是調用play對象提供的方法,並在每個類內部定義該類型的對象應如何實現此功能。
第三個例子
我們繼續看下面兩段代碼來實現輸出一些學生的成績,首先是使用類:
class Student(object):
def __init__(self, name, age, gender, level, grades=None):
self.name = name
self.age = age
self.gender = gender
self.level = level
self.grades = grades or {}
def setGrade(self, course, grade):
self.grades[course] = grade
def getGrade(self, course):
return self.grades[course]
def getGPA(self):
return sum(self.grades.values())/len(self.grades)
# 定義一些學生
john = Student("John", 12, "male", 6, {"math":3.3})
jane = Student("Jane", 12, "female", 6, {"math":3.5})
# 現在我們可以很容易地得到分數
print(john.getGPA())
print(jane.getGPA())
再來看看用函數怎么實現
def calculateGPA(gradeDict):
return sum(gradeDict.values())/len(gradeDict)
students = {}
name, age, gender, level, grades = "name", "age", "gender", "level", "grades"
john, jane = "john", "jane"
math = "math"
students[john] = {}
students[john][age] = 12
students[john][gender] = "male"
students[john][level] = 6
students[john][grades] = {math:3.3}
students[jane] = {}
students[jane][age] = 12
students[jane][gender] = "female"
students[jane][level] = 6
students[jane][grades] = {math:3.5}
print(calculateGPA(students[john][grades]))
print(calculateGPA(students[jane][grades]))
這兩段代碼都實現了輸出學生的成績,但是在使用函數的時候,我們需要記住學生是誰,成績存儲在哪里,似乎不是很困難(如果需要輸出的學生更多呢),但是OOP避免了這一點。並且代碼也更加pythonic。
結束語
最后,讓我們回到剛開始的問題上來,上面說了這么多類的好處所以我們就應該更多的去使用類嗎?並不是!
其實從某種意義上來說,類並不比函數更好。只是在某些情況下使用類能夠更好的幫助我們寫代碼。所以如果發現自己使用各種數據集調用some_function(data),那么將其用類表示為data.some_function()可能提高我們的效率。至於到底在何時使用類,我們來看看其他程序員的理解
- 當我們擁有一堆共享狀態的函數,或者將相同的參數傳遞給每個函數時,我們可以重新考慮代碼使用類。
- 類的“可重用性”意味着我們可以在其他應用程序中重用之前的代碼。如果我們在自己的文件中編寫了類,則只需將其放在另一個項目中即可使其工作。
- 函數對於小型項目非常有用,但是一旦項目開始變大,僅使用函數就可能變得混亂。類是組織和簡化代碼的一種非常好的方法
- 通常,如果在函數內部找到自寫函數,則應考慮編寫類。如果我們在一個類中只有一個函數,那么請堅持只寫一個函數。
- 如果需要在函數調用之間保留一些狀態,那么最好使用帶有該函數的類作為方法