第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]