1. 程式人生 > >第037講:類和物件:面向物件程式設計

第037講:類和物件:面向物件程式設計

目錄

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

測試題

0. 以下程式碼體現了面向物件程式設計的什麼特徵?

1. 當程式設計師不想把同一段程式碼寫幾次,他們發明了函式解決了這種情況。當程式設計師已經有了一個類,而又想建立一個非常相近的新類,他們會怎麼做呢?

2. self引數的作用是什麼?

3. 如果我們不希望物件的屬性或方法被外部直接引用,我們可以怎麼做?

4. 類在例項化後哪個方法會被自動呼叫?

5. 請解釋下邊程式碼錯誤的原因:

動動手

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("有一條魚兒被吃掉了...")