第037講:類和物件:面向物件程式設計
目錄
0. 請寫下這一節課你學習到的內容:格式不限,回憶並複述是加強記憶的好方式!
1. 當程式設計師不想把同一段程式碼寫幾次,他們發明了函式解決了這種情況。當程式設計師已經有了一個類,而又想建立一個非常相近的新類,他們會怎麼做呢?
3. 如果我們不希望物件的屬性或方法被外部直接引用,我們可以怎麼做?
0. 按照以下要求定義一個遊樂園門票的類,並嘗試計算2個成人+1個小孩平日票價。
1. 遊戲程式設計:按以下要求定義一個烏龜類和魚類並嘗試編寫遊戲。
0. 請寫下這一節課你學習到的內容:格式不限,回憶並複述是加強記憶的好方式!
經過上一節課的熱身,相信大家對類和物件已經有了初步的認識。這節課通過幾個主題來理解,面向物件程式設計。
(一)self 是什麼?
Python 的 self 相當於 C++ 的 this 指標。我們知道,類是圖紙,而由類例項化出的物件才是真正可以住人的房子,這是我們上節課大的比方。我們還知道,根據一張圖紙就可以設計出成千上萬的房子,這些房子都長得差不多,因為它們都來自於同一張圖紙,但是它們都有不同的主人,每個人都只可以回到自己的家,self 就相當於每個房子的門牌號,有了 self,就可以輕鬆的找到自己的房子,Python 的 self 引數就是同樣的道理,由同一個類可以生成無數個物件,這些物件都長得很相似,因為它們都是來源於同一個類的屬性和方法,當一個物件的方法被呼叫的時候,物件會將自身作為第一個引數傳給 self 引數,接收到這個 self 引數的時候,Python 就知道你是哪一個物件在呼叫方法了。舉例說明:
>>> class Ball: def setName(self, name): self.name = name def kick(self): print("我叫%s,該死的,誰踢我..."% self.name) >>> a = Ball() >>> a.setName("球A") >>> b = Ball() >>> b.setName("球B") >>> a.kick() 我叫球A,該死的,誰踢我... >>> b.kick() 我叫球B,該死的,誰踢我...
我們發現,在物件類定義這裡,它的這些方法都有一個 self,我們生成兩個例項化物件 a 和 b,這裡都呼叫 kick() 方法,但是實現結果不一樣,是因為 a.kick() 和 b.kick() 都有一個隱藏屬性 self,會找到各自對應的 name,這些都是由Python 在背後默默的工作,你只需要在類的定義的時候把 self 寫進第一個引數。
(二)Python 的魔法方法
你聽說過Python 的魔法方法嗎?據說,Python 的物件天生擁有一些神奇的方法,它們是面向物件的Python的一切......它們是可以給你的類增加魔力的特殊方法......如果你的物件實現了這些方法中的某一個,那麼這個方法在特殊情況下被Python所呼叫,而這一切都是自動發生的......Python的這些具有魔力的方法總是會被雙下劃線所包圍,我們今天介紹其中一個最基礎的特殊方法:__init__(self)
對於Python的其它的魔法方法,我們後面會專門進行講解。我們把__init__(self)方法稱為構造方法,__init__(self)方法的魔力體現在只要例項化一個物件的時候,那麼這個方法就會在物件被建立的時候自動呼叫。有過C++基礎的同學就會知道,這就是建構函式。
其實例項化物件的時候是可以存入引數的,這些引數會自動的存入到__init__(self)方法中,
__init__(self,param1,param2...) #(預設不重寫的形式就是__init__(self))
也就是我們這個魔法方法中,我們可以通過重寫這個方法(如上)來自定義物件的初始化操作,說起來比較複雜,舉例說明:
>>> class Ball:
def __init__(self, name):
self.name = name
def kick(self):
print("我叫%s,該死的,誰踢我..."% self.name)
>>> a = Ball("土豆") #因為重寫了__init__(self)方法,例項化物件時需要一個引數
>>> a.kick()
我叫土豆,該死的,誰踢我...
>>> b = Ball() #這裡沒有傳入引數就會報錯,可以在定義類是給name設定預設引數
Traceback (most recent call last):
File "<pyshell#84>", line 1, in <module>
b = Ball()
TypeError: __init__() missing 1 required positional argument: 'name'
(三)公有和私有
預設上來說,物件的屬性和方法都是公開的,都是共有的,我們可以通過點(.)操作符來進行訪問,舉例說明:
>>> class Person:
name = "來自江南的你"
>>> p = Person()
>>> p.name
'來自江南的你'
為了實現類似於私有變數的特徵,Python內部採用了一種叫做 name mangling(名字改編,名字重整)的技術,在Python 中定義私有變數只主要在變數名或函式名前加上“__”兩個下劃線,那麼這個函式或變數就會為私有的了。
>>> class Person:
__name = "來自江南的你"
>>> p = Person()
>>> p.__name
Traceback (most recent call last):
File "<pyshell#94>", line 1, in <module>
p.__name
AttributeError: 'Person' object has no attribute '__name'
>>> p.name
Traceback (most recent call last):
File "<pyshell#95>", line 1, in <module>
p.name
AttributeError: 'Person' object has no attribute 'name'
這時,p.__name 和 p.name 都無法訪問物件的name屬性,因為它們都找不到了,這樣在外部就會將變數名隱藏起來,理論上如果要訪問,就要從內部進行,可以這樣寫:
>>> class Person:
__name = "來自江南的你"
def getName(self):
return self.__name
>>> p = Person()
>>> p.getName()
'來自江南的你'
上面的方法只是理論上的,其實只要你琢磨一下,name mangling 技術的意思就是名字改編、名字重整,那麼應該不難發現,Python只是動了一下手腳,它把雙下劃線開頭的變數改了名字而已,它自動是改成了 _類名__變數名(單下劃線+類名+雙下劃線+變數名),如下 :
>>> class Person:
__name = "來自江南的你"
>>> p = Person()
>>> p._Person__name
'來自江南的你'
所以說,Python的私有機制是偽私有,Python是沒有許可權控制的,所以變數是可以被外部呼叫的。
測試題
0. 以下程式碼體現了面向物件程式設計的什麼特徵?
>>> "FishC.com".count('o')
1
>>> [1, 1, 2, 3, 5, 8].count(1)
2
>>> (0, 2, 4, 8, 12, 18).count(1)
0
答:體現了面向物件程式設計的多型特徵。
1. 當程式設計師不想把同一段程式碼寫幾次,他們發明了函式解決了這種情況。當程式設計師已經有了一個類,而又想建立一個非常相近的新類,他們會怎麼做呢?
答:他們會定義一個新類繼承已有的這個類,這樣子就只需要簡單新增和重寫需要的方法即可。
例如已有龜類,那麼如果要新定義一個甲魚類,我們只需要讓甲魚類繼承已有的龜類,然後重寫殼的屬性為“軟的”即可(據說甲魚的殼是軟的)。
2. self引數的作用是什麼?
答:繫結方法,據說有了這個引數,Python 再也不會傻傻分不清是哪個物件在呼叫方法了,你可以認為方法中的 self 其實就是例項物件的唯一標誌。
3. 如果我們不希望物件的屬性或方法被外部直接引用,我們可以怎麼做?
答:我們可以在屬性或方法名字前邊加上雙下劃線,這樣子從外部是無法直接訪問到,會顯示AttributeError錯誤。
>>> class Person:
__name = '小甲魚'
def getName(self):
return self.__name
>>> p = Person()
>>> p.__name
Traceback (most recent call last):
File "<pyshell#56>", line 1, in <module>
p.__name
AttributeError: 'Person' object has no attribute '__name'
>>> p.getName()
'小甲魚'
我們把getName方法稱之為“訪問器”。Python事實上是採用一種叫“name mangling”技術,將以雙下劃線開頭的變數名巧妙的改了個名字而已,我們仍然可以在外部通過“_類名__變數名”的方式訪問:
>>> p._Person__name'小甲魚'
當然我們並不提倡這種擡槓較真粗暴不文明的訪問形式……
4. 類在例項化後哪個方法會被自動呼叫?
答:__init__方法會在類例項化時被自動呼叫,我們稱之為魔法方法。你可以重寫這個方法,為物件定製初始化方案。
5. 請解釋下邊程式碼錯誤的原因:
class MyClass:
name = 'FishC'
def myFun(self):
print("Hello FishC!")
>>> MyClass.name
'FishC'
>>> MyClass.myFun()
Traceback (most recent call last):
File "<pyshell#6>", line 1, in <module>
MyClass.myFun()
TypeError: myFun() missing 1 required positional argument: 'self'
>>>
答:首先你要明白類、類物件、例項物件是三個不同的名詞。
我們常說的類指的是類定義,由於“Python無處不物件”,所以當類定義完之後,自然就是類物件。在這個時候,你可以對類的屬性(變數)進行直接訪問(MyClass.name)。
一個類可以例項化出無數的物件(例項物件),Python 為了區分是哪個例項物件呼叫了方法,於是要求方法必須繫結(通過 self 引數)才能呼叫。而未例項化的類物件直接呼叫方法,因為缺少 self 引數,所以就會報錯。
動動手
0. 按照以下要求定義一個遊樂園門票的類,並嘗試計算2個成人+1個小孩平日票價。
- 平日票價100元
- 週末票價為平日的120%
- 兒童半票
class Ticket():
def __init__(self, weekend=False, child=False):
self.exp = 100
if weekend:
self.inc = 1.2
else:
self.inc = 1
if child:
self.discount = 0.5
else:
self.discount = 1
def calcPrice(self, num):
return self.exp * self.inc * self.discount * num
>>> adult = Ticket()
>>> child = Ticket(child=True)
>>> print("2個成人 + 1個小孩平日票價為:%.2f" % (adult.calcPrice(2) + child.calcPrice(1)))
2個成人 + 1個小孩平日票價為:250.00
1. 遊戲程式設計:按以下要求定義一個烏龜類和魚類並嘗試編寫遊戲。
(初學者不一定可以完整實現,但請務必先自己動手,你會從中學習到很多知識的^_^)
- 假設遊戲場景為範圍(x, y)為0<=x<=10,0<=y<=10
- 遊戲生成1只烏龜和10條魚
- 它們的移動方向均隨機
- 烏龜的最大移動能力是2(Ta可以隨機選擇1還是2移動),魚兒的最大移動能力是1
- 當移動到場景邊緣,自動向反方向移動
- 烏龜初始化體力為100(上限)
- 烏龜每移動一次,體力消耗1
- 當烏龜和魚座標重疊,烏龜吃掉魚,烏龜體力增加20
- 魚暫不計算體力
- 當烏龜體力值為0(掛掉)或者魚兒的數量為0遊戲結束
答:參考程式碼附詳細註釋,希望先自己認真完成,你會從中學習到很多知識的。
import random as r
legal_x = [0, 10]
legal_y = [0, 10]
class Turtle:
def __init__(self):
# 初始體力
self.power = 100
# 初始位置隨機
self.x = r.randint(legal_x[0], legal_x[1])
self.y = r.randint(legal_y[0], legal_y[1])
def move(self):
# 隨機計算方向並移動到新的位置(x, y)
new_x = self.x + r.choice([1, 2, -1, -2])
new_y = self.y + r.choice([1, 2, -1, -2])
# 檢查移動後是否超出場景x軸邊界
if new_x < legal_x[0]:
self.x = legal_x[0] - (new_x - legal_x[0])
elif new_x > legal_x[1]:
self.x = legal_x[1] - (new_x - legal_x[1])
else:
self.x = new_x
# 檢查移動後是否超出場景y軸邊界
if new_y < legal_y[0]:
self.y = legal_y[0] - (new_y - legal_y[0])
elif new_y > legal_y[1]:
self.y = legal_y[1] - (new_y - legal_y[1])
else:
self.y = new_y
# 體力消耗
self.power -= 1
# 返回移動後的新位置
return (self.x, self.y)
def eat(self):
self.power += 20
if self.power > 100:
self.power = 100
class Fish:
def __init__(self):
self.x = r.randint(legal_x[0], legal_x[1])
self.y = r.randint(legal_y[0], legal_y[1])
def move(self):
# 隨機計算方向並移動到新的位置(x, y)
new_x = self.x + r.choice([1, -1])
new_y = self.y + r.choice([1, -1])
# 檢查移動後是否超出場景x軸邊界
if new_x < legal_x[0]:
self.x = legal_x[0] - (new_x - legal_x[0])
elif new_x > legal_x[1]:
self.x = legal_x[1] - (new_x - legal_x[1])
else:
self.x = new_x
# 檢查移動後是否超出場景y軸邊界
if new_y < legal_y[0]:
self.y = legal_y[0] - (new_y - legal_y[0])
elif new_y > legal_y[1]:
self.y = legal_y[1] - (new_y - legal_y[1])
else:
self.y = new_y
# 返回移動後的新位置
return (self.x, self.y)
turtle = Turtle()
fish = []
for i in range(10):
new_fish = Fish()
fish.append(new_fish)
while True:
if not len(fish):
print("魚兒都吃完了,遊戲結束!")
break
if not turtle.power:
print("烏龜體力耗盡,掛掉了!")
break
pos = turtle.move()
# 在迭代器中刪除列表元素是非常危險的,經常會出現意想不到的問題,因為迭代器是直接引用列表的資料進行引用
# 這裡我們把列表拷貝給迭代器,然後對原列表進行刪除操作就不會有問題了^_^
for each_fish in fish[:]:
if each_fish.move() == pos:
# 魚兒被吃掉了
turtle.eat()
fish.remove(each_fish)
print("有一條魚兒被吃掉了...")