­

【python系統學習13】類(class)與對象(object)

目錄:


類(class)和實例

整數、字元串、浮點數等,不同的數據類型就屬於不同的類。
想想看當初學數據類型,我們用type驗證數據類型後列印的結果

忘了就再來看看:

strs = '字元串'
ints = 1
floats = 2.3

print(type(strs)) # <class 'str'>
print(type(ints)) # <class 'int'>
print(type(floats)) # <class 'float'>

以上,class就是類。

顧名思義class ‘str’就表示是字元串類。
同理,剩下倆個就是整數類、浮點數類…

類之所以為類,是因為每一個類之下都包含無數相似的不同個例。
類,是對某個群體的統稱。 比如:人類、犬類

實例

在Python的術語里,我們把類的個例就叫做實例 (instance),可理解為「實際的例子」。

比如上邊程式碼中的’字元串’、1、2.3。這三個就分別是字元串類的實例、整數類的實例、浮點數類的實例。

類是某個群體,實例是群體中某個具體的個體。
群體里的每個個體都有著相同/相似的特徵和行為。也就是說實例之間會有相同/相似的特徵和行為。

比如,字元串中的實例舉幾個例子:

str1 = '我是字元串'
str2 = '1'
str3 = '2.3'
str4 = 'True'

以上四個都是字元串,因為他們都是用英文引號包裹。這就是他們的相同特徵。

即使他們四個的內容值完全不一樣。

而也因此,值不一樣就是他們實例之間的區別、是區別於其他實例個體的特徵。

小測試

請問:狗、秋田犬、忠犬八公、list、[1,2]
以上這五個元素,哪個是類、哪個是實例?

答案見源碼同目錄下files中同名py文件

對象(object)

佛說:萬事萬物,皆可為對象。

咳咳,佛說,我說的不是男女對象那個對象!

這裡所謂Python中的對象,等於類和實例的集合:類可以看作是對象,實例也可以看作是對象。

比如列表list是個類對象,[1,2]是個實例對象,它們都是對象。

再比如說人類是個類對象,也可以說小紅是個實例對象(這裡小紅依舊不是你的女對象!!!清醒點!!螞蟻競走了十年了!!!)。

屬性和方法

區別於其他類的依據,細分可以分成兩種:

  1. 第一種是描述事物是怎樣的,有什麼特徵 – 這就是所說的【屬性】
  2. 第二種是描述事物能做什麼,有哪些行為和作用 – 也就是所說的【方法】

Python中每個類都有自己獨特的屬性(attribute)和方法(method),是這個類的所有實例都共享的。換言之,每個實例都可以調用類中所有的屬性和方法。
不過各個類的屬性和方法,是需要我們自行創建的。除了python中已有的數據類型其屬性和方法是內置建好的。

比如:列表的內置方法有append、pop等。而這些方法任何列表實例值都可以使用

listObject = [1,3,'列表實例里的第三個元素']  # 一個列表實例
listObject.append('我是列表實例利用類上的append方法添加進來的元素'# 調用列表類的內置方法append
print(listObject) # [13'列表實例里的第三個元素''我是列表實例利用類上的append方法添加進來的元素']

類的創建

上節,函數用def關鍵字定義。

本節,類的創建用class關鍵字定義。

偽程式碼

class 首字母大寫的類變數名:
  自定義屬性名 = 屬性值
  def 自定義方法名(self,參數1,可以沒有參數2):
    方法函數體內容

具體的含義:

  1. class關鍵字創建,class+類名+英文冒號
  2. 類名首字母大寫,是自定義命名,大寫字母開頭,不能和python關鍵字衝突。
  3. 類的程式碼體要放在縮進里。
  4. 屬性名自定義,不能和python關鍵字衝突。屬性值直接用等號賦值給自定義屬性名即可
  5. 實例方法名自定義,不能和python關鍵字衝突。方法(也就是函數)通過def關鍵字定義,和函數的定義語句很類似,
  6. 實例方法的第一個參數必須傳self,固定值。(下詳)
  7. 類中創建的屬性和方法可以被其所有的實例調用
  8. 實例的數目在理論上是無限的。我們可以同時「新建」多個實例【類被稱為「實例工廠」的由來】

示例程式碼

# 創建一個男朋友類對象
class MyBoyfriend:
  sex = 'male'
  def caring(self):
    print('好了,不哭了~')
  
boyfriend = MyBoyfriend() # 調用類對象,得到男朋友實例對象。
print(type(MyBoyfriend)) <class 'type'>
print(boyfriend) <__main__.MyBoyfriend object at 0x109922400> MyBoyfriend類的是一個實例對象。後面的一串字元(0x109922400)表示這個對象的記憶體地址。
print(type(boyfriend)) <class '__main__.MyBoyfriend'> 表示boyfriend類屬於MyBoyfriend類。

屬性(attribute)

在類中賦值的變數叫做這個類的「屬性」

方法(method)

在類中定義的函數叫做這個類的「方法」。

類中帶self的參數的方法是實例方法,是類方法的一種形式,也是最常用的一種方法。

類的實例化

還是以上邊創建男朋友類的程式碼為例,類的實例化過程就是MyBoyfriend()這句。 最後創建的實例對象被賦值給變數boyfriend

調用類對象的過程叫作類的實例化,即用某個類創建一個實例對象。

實現形式/偽程式碼是

實例變數名 = 類名() # 類名後+小括弧調用

實例對象調用類屬性和方法

實例化類對象後,得到一個實例對象。因為類的方法和屬性,實例對象都有。所以實例對象boyfriend可以調用類中的屬性sex和方法caring

調用類的屬性

實例名.屬性

調用類的方法

實例名.方法(傳參) # 參數不用考慮self

示例程式碼

# 調用類的屬性
print(boyfriend.sex) # 列印"male"

# 調用類的方法
boyfriend.caring() # 列印「好了,不哭了~」

值得說明的是,調用類方法時,傳參不用考慮self參數的存在。 fa

特殊參數:self

可以看到上例,為什麼實例調用方法時不用傳參,在定義時那個參數self又是什麼意思呢?

這個參數self的特殊之處:「在定義時不能丟,在調用時要忽略。」

1、代指實例化對象的作用

其實這個self有點像JS中構造函數內部的this。self是所有實例化對象的替身。指的是任何用這個類生成的實例化對象:

# self
class SelfMean:
  content = '類SelfMean中的屬性'
  def oneFn(self):
    print(self.content) # self在此指向實例化的對象"selfMean"
selfMean = SelfMean()
selfMean.oneFn() # 列印類中content的值 - "類SelfMean中的屬性"

注意上例中,在oneFn函數內部,使用類中的屬性content時,不能直接當變數使用,否則如下寫法就會報錯:

class SelfMean:
  content = '類SelfMean中的屬性'
  def oneFn(self):
    print(content) # 不用self調用類屬性,就會報錯NameError: name 'content' is not defined
selfMean = SelfMean()
selfMean.oneFn()

變數未經定義就使用,就會報這種NameError的問題。詳見第三章錯誤類型總結篇

有人會疑問,程式碼中oneFn上邊不是定義了content還給她賦值了嗎?怎麼能說未定義呢?
函數的作用域中也說了,自己oneFn函數內部找不到的變數,會向上找父級作用域、層層向上查詢乃至全局作用域的變數啊,他自己雖然沒有、但是他爸爸有啊!為啥還說未定義呢?

同志啊,醒醒。這裡是類啊!不是函數。類內部的變數是定義給實例化對象的屬性的啊。換句話說,上述程式碼實例化對象轉換成字典的模樣長下邊這樣:

selfMean = {
  'content''類SelfMean中的屬性',
  'oneFn': 一個函數體在這裡~
}

所以你如果不用字典的方式(1、selfMean.content;2、selfMean[‘content’])調用這個屬性,他是拿不出來的。

而在類內部,實例化對象還沒出生,你不知道他未來的名字叫selfMean1、還是叫selfMean37。也就沒有辦法用selfMean.content或selfMean37[‘content’]的方式去調用它。
所以在這之前,需要有一個統一的self,來代指未來的實例化對象,並達到提前在類內部使用實例化對象的屬性和方法的目的
使用方式就是self.屬性self.方法名(除self以外的傳參)。看下例:

class SelfMean:
  content = '類SelfMean中的屬性'
  def oneFn(self):
    print(self.content) # 提前在類中使用了實例化對象的屬性 content,等價於selfMean.content
    self.twoFn('哈哈哈哈哈~'# 提前在類中使用了實例化對象的方法 twoFn、並傳參
  def twoFn(self, txt):
    print('實例化對象的第二個方法,列印內容:', txt)
selfMean = SelfMean()
selfMean.oneFn()

2、定義方法必傳self

看上邊的程式碼中,oneFn和twoFn都有self參數,並且都是第一個參數,這並不是巧合。

只要在類中定義的方法,第一個參數就必須是self。不過調用方法時,可以忽略它,不用給self傳參。

3、調用方法傳參時self可忽略

我們調用實例方法的時候,傳參不用考慮self。以此往後類推就行:

class SelfParams:
  content = '類SelfParams中的屬性'
  def twoFn(self,name,sex,age,weight):
    print(self.content)
    print(name,sex,age,weight) # 2、依次列印傳遞過來的位置參數的值:小石頭 female 19 91
selfParams = SelfParams()
selfParams.twoFn('小石頭','female',19,91# 1、調用方法時忽略self參數,依次按位置傳遞name,sex,age,weight的參數

初始化方法(構造函數)

1、定義初始化方法

定義初始化方法的格式是def __init__(self),是由init關鍵字加左右兩邊的【雙】下劃線(__)組成。

雙下劃線是英文輸入法下,shift + 0右邊的那個鍵打出來的。

2、初始化方法的作用

無需調用自執行

一、當每個實例對象創建時(即類調用時),該方法內的程式碼無須調用就會自動運行。無需”實例名.init“的方式調用

# init
class InitTest:
  def __init__(self):
    a = 321
    b = 345

    print("初始化就會執行init里的程式碼: ", a+b)
initTest = InitTest()

# 運行後直接列印:初始化就會執行init里的程式碼:  666

可見 ,觸發類對象的調用時,就直接觸發了__init__方法的調用。

為類屬性設置初始值

一般情況下,我們都會在初始化方法內部完成類屬性的創建,為類屬性設置初始值,這樣類中的其他方法就能直接、隨時調用。

上述程式碼改寫如下:

class InitTest:
  def __init__(self):
    # 為類屬性設置初始值,要寫在__init__函數內部的最上方,否則會報錯
    self.a = 321 # 注意self的存在。
    self.b = 345 # 同上

    # 定義完屬性的初始值以後,才能在下變寫__init__裡邊要處理的其他邏輯
    print("初始化就會執行__init__里的程式碼")
    self.plusAd()

  def plusAd(self):
    print(self.a + self.b) # 使用類屬性

initTest = InitTest()
# 列印結果如下
# 初始化就會執行init里的程式碼
# 666

3、初始化方法接收其他參數

除了設置固定常量,初始化方法同樣可以接收其他參數,並把這些參數賦值給類的屬性並被類中其他方法使用:

# 初始化方法接收參數
class InitParams:
  def __init__(self,aP,bP):
    self.a = aP
    self.b = bP

    print('初始化執行並設置屬性、把參數aP和bP的值給了屬性a和b')
    self.plusAd()

  def plusAd(self):
    print(self.a + self.b) # 其他方法也能用同一個屬性

  def sub(self):
    print(self.a - self.b) # 其他方法也能用公用的屬性

initParams1 = InitParams(124# __init__需要的參數在類調用時傳遞
# 列印結果:
# 初始化執行並設置屬性、把參數aP和bP的值給了屬性a和b
# 16
# 8

initParams2 = InitParams(305# __init__需要的參數在類調用時傳遞,可以多次調用,傳不同的參數,進而得到不一樣的結果
# 列印結果:
# 初始化執行並設置屬性、把參數aP和bP的值給了屬性a和b
# 35
# 25 

當初始化方法__init__有多個參數的時候,在實例化(類調用)的時候就要傳入相應的值。

上例第一次調用,12傳給了aP、4傳給了bP。
上例第二次調用,30傳給了aP、5傳給了bP。

這也是初始化方法的好處,傳參一次可被多次調用,

番外 – 面向對象

面向過程

面向過程編程:首先分析出解決問題所需要的步驟(即「第一步做什麼,第二步做什麼,第三步做什麼」),然後用函數實現各個步驟,再依次調用。一個示例:

# 全局變數定義
globalA = 12
globalB = 20
globalCount = 0

# 加
def plus():
  global globalCount
  globalCount = globalA + globalB
  sub()

# 減
def sub():
  global globalCount
  globalCount = globalCount - globalA/2
  print(globalCount)

# 主流程
def main():
  plus()

# 啟動流程
main()

我們根據「全局變數數據整理——主流程——加法計算——減法計算」這個過程封裝了三個函數,再依次調用,按規定順序執行程式。

面向對象

面向對象編程,會將程式看作是一組對象的集合。通過調用對象的屬性和方法,來拼湊完成一段功能。

# 面向對象
class Calculator:
  def __init__(self,a,b):
    # 公共屬性定義
    self.globalA = a
    self.globalB = b
    self.globalCount = 0

  # 是加法方法,能做加法
  def plus(self):
    self.globalCount = self.globalA + self.globalB
    print(self.globalCount)

  # 是減法方法,能做減法
  def sub(self):
    self.globalCount = self.globalCount - self.globalA/2
    print(self.globalCount)

calculator = Calculator(12,20)
calculator.plus()
calculator.sub()

用這種思維設計程式碼時,考慮的不是程式具體的執行過程(即先做什麼後做什麼),而是考慮先創建某個類,在類中設定好屬性和方法,即是什麼,和能做什麼。

接著,再以類為模版創建一個實例對象,用這個實例去調用類中定義好的屬性和方法(plus、sub)即可。

面向對象的好處

  1. 參數的傳遞會比普通函數要省事很多。(不必考慮全局變數和局部變數,因為類中的方法可以直接調用屬性。)
  2. 程式碼的可復用性也更高。(類能封裝更多的東西,既能包含操作數據的方法,又能包含數據本身。)
  3. 程式碼結構會更清晰。(程式碼的可讀性、可拓展性和可維護性這幾個方面都會優於面向過程編程。)
  4. 一目了然。(面向對象編程將程式碼具體的數據和處理方法都封裝在類中,不用完全了解過程也可以調用類中的各種方法。並且還可以分開調用)
  5. 可以在 Python 中輕鬆地調用各種標準庫、第三方庫和自定義模組(別人寫好的類框架程式碼)

本文使用 mdnice 排版