1. 程式人生 > >第039講:類和物件:拾遺

第039講:類和物件:拾遺

0. 請寫下這一節課你學習到的內容:格式不限,回憶並複述是加強記憶的好方式!

(一)組合

上節課我們學習了繼承和多繼承,但是我們有時候發現,有些情況你用繼承也不合適,用多繼承也不是,例如:現在現在要求定義一個類,叫水池,水池裡要有烏龜和魚。那大家就很苦惱了,用多繼承就顯得很奇葩了,因為如果把水池繼承烏龜和魚,那顯然就是不同物種。那怎樣才能把它們組成一個和諧的類呢,這就是我們今天首先要講的內容:組合。

組合的用法很簡單,舉例說明:

class Turtle:
        def __init__(self, x):
                self.num = x
class Fish:
        def __init__(self, x):
                self.num = x
class Pool:
        def __init__(self, x, y):
                self.turtle = Turtle(x)
                self.fish = Fish(y)
        def print_num(self):
                print("水池裡有烏龜 %d 只,小魚 %d 條!" %(self.turtle.num, self.fish.num))

>>> pool = Pool(1, 10)
>>> pool.print_num()
水池裡有烏龜 1 只,小魚 10 條!

所謂的組合,就是把類的例項化放到新類裡面,那麼它就把舊類給組合進去了,不用使用繼承了,沒有什麼風險了。組合一般來說就是把幾個沒有繼承關係,沒有直線關係的幾個類放在一起,就是組合。要實現縱向關係之間的類,就使用繼承。

Python 的特性還支援另外一種很流行的程式設計模式,叫做 Mix-in,叫做混入的意思,有興趣的可以參見-> Python Mixin 程式設計機制

(二)類、類物件和例項物件

我們一開始就說了,類、類物件和例項物件是三個不同的物種。先看程式碼:

>>> class C:
	count = 0

	
>>> a = C()
>>> b = C()
>>> c = C()
>>> a.count
0
>>> b.count
0
>>> c.count
0
>>> c.count += 10
>>> c.count
10
>>> a.count
0
>>> b.count
0
>>> C.count
0
>>> C.count += 100
>>> C.count
100
>>> a.count
100
>>> b.count
100
>>> c.count
10

我們這裡有一個 C 類,只有一個屬性 count ,初始化為0。例項化一個 a,一個 b,一個 c,顯然 a.count = 0,b.count = 0,c.count = 0。如果對 c.count += 10,現在 c.count = 10,但是 a.count = 0,b.count = 0。因為 C 是一個類,在寫完 C 之後就變成了一個類物件,因為Python無處不物件,所有的東西都是物件,方法也是物件,所以我們這裡 C.count = 0 也是等於 0 。此時我們對這個類物件加等於100 , C.count += 100,此時 a.count = 100,b.count = 100,但是  c.count = 10。為什麼會這樣呢?

其實是因為 c.count += 10 這裡 c.count 被賦值的時候,我們是對例項化物件 c 的屬性進行賦值,相當於我們生成了一個 count 來覆蓋類物件的 count,如圖:

類定義到類物件,還有例項物件a,b,c,需要注意的是,類中定義的屬性都是靜態屬性,就像 C 裡面的count,類屬性和類物件是相互繫結的,並不會依賴於下面的例項物件,所以當 c.count += 10 的時候,並不會影響到 C,只是改變了 c 自身,因為在 c.count += 10 的時候,是例項物件 c 多了一個count 的屬性,也就是例項屬性,它把類屬性給覆蓋了。這在以後還會繼續講解,在此之前,我們先談一下:如果屬性的名字和方法相同時,屬性會把方法覆蓋掉。舉例說明:

>>> class C:
	def x(self):
		print("X-man")

		
>>> c = C()
>>> c.x()
X-man
>>> c.x = 1
>>> c.x
1
>>> c.x()
Traceback (most recent call last):
  File "<pyshell#48>", line 1, in <module>
    c.x()
TypeError: 'int' object is not callable

c.x = 1 是例項化後的 c ,建立了一個 x 的屬性,這時如果要呼叫 它的函式 x() 就會報錯,出錯資訊說:整型是不能被呼叫的。

這就是初學者容易發生的一個問題,如果屬性的名字和方法名相同,屬性會覆蓋方法。為了避免名字上的衝突,大家應該遵守一些約定俗成的規矩:

  • 不要試圖在一個類裡邊定義出所有能想到的特徵和方法,應該使用繼承和組合機制來進行擴充套件。
  • 用不同詞性命名,如屬性名用名詞,方法名用動詞。

(三)到底什麼是繫結?

Python 嚴格要求方法需要有例項才能被呼叫,這種限制其實就是Python 所謂的繫結概念。


測試題

0. 什麼是組合(組成)? 

答:Python 繼承機制很有用,但容易把程式碼複雜化以及依賴隱含繼承。因此,經常的時候,我們可以使用組合來代替。在Python裡組合其實很簡單,直接在類定義中把需要的類放進去例項化就可以了。

例子:

// 烏龜類
class Turtle:
    def __init__(self, x):
        self.num = x
// 魚類
class Fish:
    def __init__(self, x):
        self.num = x
// 水池類
class Pool:
    def __init__(self, x, y):
        self.turtle = Turtle(x)        // 組合烏龜類進來
        self.fish = Fish(y)        // 組合魚類進來
     
    def print_num(self):
        print("水池裡總共有烏龜 %d 只,小魚 %d 條!" % (self.turtle.num, self.fish.num))

>>> pool = Pool(1, 10)
>>> pool.print_num()

1. 什麼時候用組合,什麼時候用繼承?

答:根據實際應用場景確定。簡單的說,組合用於“有一個”的場景中,繼承用於“是一個”的場景中。例如,水池裡有一個烏龜,天上有一個鳥,地上有一個小甲魚,這些適合使用組合。青瓜是瓜,女人是人,鯊魚是魚,這些就應該使用繼承啦。

2. 類物件是在什麼時候產生?

答:當你這個類定義完的時候,類定義就變成類物件,可以直接通過“類名.屬性”或者“類名.方法名()”引用或使用相關的屬性或方法。

3. 如果物件的屬性跟方法名字相同,會怎樣?

答:如果物件的屬性跟方法名相同,屬性會覆蓋方法。

class C:
        def x(self):
                print('Xman')

>>> c = C()
>>> c.x()
Xman
>>> c.x = 1
>>> c.x
1
>>> c.x()
Traceback (most recent call last):
  File "<pyshell#20>", line 1, in <module>
    c.x()
TypeError: 'int' object is not callable

4. 請問以下類定義中哪些是類屬性,哪些是例項屬性?

class C:
        num = 0
        def __init__(self):
                self.x = 4
                self.y = 5
                C.count = 6

答:num 和 count 是類屬性(靜態變數),x 和 y 是例項屬性。大多數情況下,你應該考慮使用例項屬性,而不是類屬性(類屬性通常僅用來跟蹤與類相關的值)。

5. 請問以下程式碼中,bb 物件為什麼呼叫 printBB() 方法失敗?

class BB:
        def printBB():
                print("no zuo no die")

>>> bb = BB()
>>> bb.printBB()
Traceback (most recent call last):
  File "<pyshell#8>", line 1, in <module>
    bb.printBB()
TypeError: printBB() takes 0 positional arguments but 1 was given

答:因為 Python 嚴格要求方法需要有例項才能被呼叫,這種限制其實就是 Python 所謂的繫結概念。所以 Python 會自動把 bb 物件作為第一個引數傳入,所以才會出現 TypeError:“需要 0 個引數,但實際傳入了 1 個引數“。

正確的做法應該是:

class BB:
        def printBB(self):
                print("no zuo no die")

>>> bb = BB()
>>> bb.printBB()
no zuo no die

動動手

0. 思考這一講我學習的內容,請動手在一個類中定義一個變數,用於跟蹤該類有多少個例項被建立

(當例項化一個物件,這個變數+1,當銷燬一個物件,這個變數自動-1)。

class C:
        count = 0
        
        def __init__(self):
                C.count += 1

        def __del__(self):
                C.count -= 1

>>> a = C()
>>> b = C()
>>> c = C()
>>> C.count
3
>>> del a
>>> C.count
2
>>> del b, c
>>> C.count
0

1. 定義一個棧(Stack)類,用於模擬一種具有後進先出(LIFO)特性的資料結構。

至少需要有以下方法:

方法名 含義
isEmpty() 判斷當前棧是否為空(返回 True 或 False)
push() 往棧的頂部壓入一個數據項
pop() 從棧頂彈出一個數據項(並在棧中刪除)
top() 顯示當前棧頂的一個數據項
bottom() 顯示當前棧底的一個數據項
class Stack:
    def __init__(self, start=[]):
        self.stack = []
        for x in start:
            self.push(x)

    def isEmpty(self):
        return not self.stack
    
    def push(self, obj):
        self.stack.append(obj)

    def pop(self):
        if not self.stack:
            print('警告:棧為空!')
        else:
            return self.stack.pop()

    def top(self):
        if not self.stack:
            print('警告:棧為空!')
        else:
            return self.stack[-1]

    def bottom(self):
        if not self.stack:
            print('警告:棧為空!')
        else:
            return self.stack[0]