假裝用某米賽爾號的角度看Python面向對象編程

類和對象

下面我們正式創建自己的類, 這裏我們使用Python自定義某米賽爾號的精靈, 代碼如下:

class Charmander:
    def setName(self, name):
        self.name = name
    def getName(self):
        return self.name
    def getInfo(self):
        return self       

類的定義就像函數定義, 用 class 語句替代了 def 語句, 同樣需要執行 class 的整段代碼這個類纔會生效。進入類定義部分後, 會創建出一個新的局部作用域, 後面定義的類的數據屬性方法都是屬於此作用域的局部變量。上面創建的類很簡單, 只有一些簡單的方法。當捕捉到精靈時, 首先要爲其起名字, 所以我們先編寫函數 setName() 和 getName()。似乎函數中 self 參數有點奇怪, 我們嘗試建立具體的對象來探究該參數的作用。

>>> x = Charmander()
>>> y = Charmander()
>>> x.setName('小火猴')
>>> y.setName('皮皮')

>>> x.getName()
小火猴
>>> y.getName()
皮皮

>>> x.getInfo()
<__main__.Charmander instance at 0xXXXXXXXX>

>>> y.getInfo()
<__main__.Charmander instance at 0xXXXXXXXX>

創建對象和調用一個函數很相似, 使用類名作爲關鍵字創建一個類的對象, 實際上, Charmander() 的括號裏是可以有參數的, 後面我們會討論到。我們有兩隻精靈, 一隻是小火猴, 一隻是皮皮, 並且對他們執行 getName() , 名字正確返回。觀察 getInfo() 的輸出, 返回的是包含地址的具體對象信息, 可以看到兩個對象的地址, 是不一樣的。Python 中的self作用和 C++ 中的 *this 指針類似, 在調用 Charmandar 的 setName() 和 getName() 函數時, 函數都會自動把該對象的地址作爲第一個參數傳入(該信息包含在參數 self 中), 這就是爲什麼我們調用函數時不需要寫 self , 而在函數定義時需要把參數作爲第一個參數。傳入對象地址是相當必要的, 如果不傳入地址, 程序就不知道要訪問類的哪一個對象。

類的每個對象都會有各自的數據屬性, Charmander 類中有數據屬性 name, 這是通過setName() 函數中的語句 self.name = name創建的。這個語句中的兩個 name 是不一樣的, 它們的作用域不一樣。第一個 name 通過 self 語句聲明的作用域是類 Charmander() 的作用域, 將其作爲對象 x 的數據屬性進行存儲, 而後面的 name 的作用域是函數的局部作用域, 與參數中的 name 相同。而後面 getName() 函數返回的是對象中的 name。

__init__()方法

從更深層邏輯去說, 我們捕捉到精靈的那一刻應該就有名字, 而並非捕捉後去設置。所以這裏我們需要的是一個初始化手段。Python中的__init__() 方法用於初始化類的實例對象。__init__() 函數的作用一定程度上與C++的構造函數相似, 但並不等於。C++ 的構造函數是使用該函數去創建一個類的實例對象, 而Python執行__init__() 方法時實例對象已被構造出來。__init__()方法會在對象構造出來後自動執行, 所以可以用於初始化我們所需要的數據屬性。修改Charmander 類的代碼, 代碼如下:

class Charmander:
    def __init__(self, name, gender, level):
        self.tyoe = ('fire', None)
        self.gender = gender
        self.name = name
        self.level = level
        self.status = [10+2*level, 5+1*level, 5+1*level, 5+1*level, 5+1*level, 5+1*level]
        
    # 精靈體力, 攻擊, 防禦, 特攻, 特防, 速度
    def getName(self):
        return self.name
    def getGender(self):
        return self.gender
    def getType(self):
        return self.type
    def getStatus(self):
        return self.status

在此處我們增加了幾個數據的屬性: 性別、等級、能力和精靈屬性。連同前面的名字, 都放在__init__()方法進行初始化。數據屬性是可以使用任意數據類型的, 小火猴屬性是火, 而精靈可能會有倆個屬性, 假設小火猴經過兩次進化稱爲烈焰猩猩屬性爲地面, 火系。爲了保持數據類型的一致性, 所以我們使用元組存儲, 並讓小火猴的第二個屬性爲 None。由於小火猴的屬性是固定的, 所以在__init__() 的輸入參數不需要 type。而精靈的能力會隨着等級的不同而不同, 所以在初始化中也需要實現這一點。我們創建實例對象測試代碼:

>>> x = Charmander('小火猴', 'male', 5)
>>> y = Charmander('皮皮', 'female', 6)
>>> print( x.getName(), x.getGender(), x.getStatus() )
小火猴 male [20, 10, 10, 10, 10, 10]

>>> print( y.getName(), y.getGender(), y.getStatus() )
皮皮 female [22, 11, 11, 11, 11, 11]

這時候創建對象就需要參數了, 實際上這是__init__() 函數的參數。__init__() 自動將數據屬性進行了初始化, 然後調用相關函數能夠返回我們需要的對象的數據屬性。

對象的方法

1.方法引用
類的方法和對象的方法是一樣的, 我們在定義類的方時程序沒有爲類的方法分配內存, 而在創建具體實例對象的程序纔會爲對象的每個數據屬性和方法分配內存, 我們已經知道定義類的方法是 def 定義的, 具體定義格式與普通函數相似, 只不過類的方法的第一個參數要爲 self 參數。我們可以用普通函數實現對對象函數的引用:

>>> x = Charmander('小火猴', 'male', 5)
>>> getStatus1 = x.getStatus
>>> getStatus1()
[20, 10, 10, 10, 10, 10]

雖然看上去似乎是調用了一個普通函數, 但是 getStatus1() 這個函數是引用 x.getStatus() 的, 意味着程序還是隱性地加入了 self 參數。

2.私有化
先敲代碼:

>>> x.type
('fire', None)
>>> x.getType()
('fire', None)

雖然這樣似乎很方便, 但違反了類的封裝原則。對象的狀態對於類外部應該是不可以訪問的。爲何要這樣做, 我們查看Python 的模塊源碼時會發現源碼裏定義了很多類, 模塊中算法通過使用類是很常見的, 如果我們使用算法時能隨意訪問對象中的數據屬性, 那麼很有可能在不經意間修改算法中已經調好的參數, 這是十分尷尬的。儘管我們不會可以那麼去做, 但這種無意的改動是常有的事。一般封裝好的類都會有足夠的函數接口供程序員用, 程序員沒有必要訪問對象的具體數據類型。

爲防止程序員無意間修改了對象的狀態, 我們需要對類的數據屬性和方法進行私有化。Python 不支持直接私有方式, 但可以使用一些小技巧達到私有特性的目的。爲了讓方法的數據屬性或方法變爲私有, 只需要在它的名字前面加上雙下劃線即可, 修改Charmander 類代碼:

# 自定義類
class Charmander:
    def __init__(self, name, gender, level):
        self.__type = ('fire', None)
        self.__gender = gender
        self.__name = name
        self.__level = level
        self.__status = [10+2*level, 5+1*level, 5+1*level, 5+1*level, 5+1*level, 5+1*level]
        
    # 精靈體力, 攻擊, 防禦, 特攻, 特防, 速度
    def getName(self):
        return self.__name
    def getGender(self):
        return self.__gender
    def getType(self):
        return self.__type
    def getStatus(self):
        return self.__status
    def level_up(self):
        self.__status = [s+1 for s in self.__status]
        self.__status[0] += 1 # HP每級增加2點, 其餘增加1點
    def __test(self):
        pass
>>> x = Charmander('小火猴', 'male', 5)
>>> print(x.type)
Traceback (most recent call last):
  File "seer.py", line 25, in <module>
    print(x.type)
AttributeError: 'Charmander' object has no attribute 'type'
>>> print(x.getName())
小火猴
>>> x.test()
Traceback (most recent call last):
  File "ser.py", line 28, in <module>
    x.test()
AttributeError: 'Charmander' object has no attribute 'test'

現在在程序外部直接訪問私有數據是不允許的, 我們只能通過設定好的節後函數去調取對象信息。不過通過雙下劃綫實現的私有實際上是"僞私有化", 實際上我們還是可以做到從外部訪問這些私有屬性。

>>> print(x._Charmander__type)
('fire', None)

Python 使用的是一種 name_mangling 技術, 將__membername 替換成 _class__membername, 在外部使用原來的私有成員時, 會提示無法找到, 而上面執行的 x.Charmander__type 是可以訪問。簡而言之, 確保其他人無法訪問對象的方法和數據屬性是不可能的, 但是使用這種 name_mangling 技術是一種程序員不應該從外部訪問這些私有成員的強有力信號。

可以看到代碼中還增加了一個函數level_up(), 這個函數用於處理精靈升級是能力的提升, 我們不應該在外部修改 x 對象的 status , 所以應準備好接口去處理能力發生變化的情景, 函數 level_up() 僅僅是一個簡單的例子, 而據說在工業代碼中, 這樣的函數接口是大量的, 程序需要對它們進行歸類並附上相應的文檔說明。

3.迭代器
Python容器對象(列表、元組、字典和字符串等)都可以可以用 for 遍歷,

for element in [1, 2, 3]:
    print(element)

這種風格十分簡潔, for 語句在容器對象上調用了 iter(), 該函數返回一個定義了 next() 方法的迭代器對象, 它在容器中逐一訪問元素。當容器遍歷完畢, __next__() 找不到後續元素時, next() 找不到後續元素時, next()會引發一個 StopIteration 異常, 告知for循環終止。

>>> L = [1, 2, 3]
>>> it = iter(L)
>>> it
<list_iterator object at 0x0302C530>
>>> it.__next__()
1
>>> it.__next__()
2
>>> it.__next__()
2
>>> it.__next__()
3
>>> it.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

當知道迭代器協議背後的機制後, 我們便可以吧迭代器加入到自己的類中。我們需要定義一個__iter__()方法, 它返回一個有 next() 方法的對象, 如果類定義了next(), __iter__()可以只返回self, 再次修改類 Charmander 的代碼, 通過迭代器能輸出對象的全部信息。

class Charmander:
    def __init__(self, name, gender, level):
        self.__type = ('fire', None)
        self.__gender = gender
        self.__name = name
        self.__level = level
        self.__status = [10+2*level, 5+1*level, 5+1*level, 5+1*level, 5+1*level, 5+1*level]
        self.__info = [self.__name, self.__type, self.__gender, self.__level, self.__status]
        self.__index = -1
        
    # 精靈體力, 攻擊, 防禦, 特攻, 特防, 速度
    def getName(self):
        return self.__name
    
    def getGender(self):
        return self.__gender
    
    def getType(self):
        return self.__type

    def getStatus(self):
        return self.__status
        
    def level_up(self):
        self.__status = [s+1 for s in self.__status]
        self.__status[0] += 1
    
    def __iter__(self):
        print('名字 屬性 性別 等級 能力')
        return self
    
    def next(self):
        if self.__index == len(self.__info) - 1:
            raise StopIteration
        self.__index += 1
        return self.__info[self.__index]

繼承

面向對象編程的好處之一就是代碼的複用, 實現這種重用的方法之一就是通過繼承機制。繼承是兩個類或多個類之間的父子關係, 子類繼承了基類的所有公有數據屬性和方法, 並且可以通過編寫子類的代碼擴充子類的功能。可以說, 如果人類可以做到兒女繼承了父母的所有才學並加以拓展, 那麼人類的發展至少是現在的數萬倍。繼承實現了數據屬性和方法的重用, 減少了代碼的冗餘度。

那麼我們如何實現繼承呢??如果我們需要的類中具有公共的成員, 且具有一定的遞進關係, 那麼就可以使用繼承, 且讓結構最簡單的類作爲基類。一般來說, 子類是父類的特殊化, 如下關係:

                哺乳類動物 ————> 貓科動物 ————> 東北虎

東北虎類 繼承 貓科動物類, 貓科動物類 繼承 哺乳動物類, 貓科動物類編寫了所有貓科動物公有的行爲的方法而特定貓類則增加了該貓科動物特有的行爲。不過繼承也有一定弊端, 可能基類對於子類也有一定特殊的地方, 如果某種特定貓科動物不具有絕大多數貓科動物的行爲, 當程序員嗎,沒有理清類之間的關係是, 可能使子類具有不該有的方法。另外, 如果繼承鏈太長的話, 任何一點小的變化都會引起一連串變化, 我們使用的繼承要注意控制繼承鏈的規模。

繼承語法: class 子類名(基類名1, 基類名2,...), 基類卸載括號裏, 如果有多個基類, 則全部寫在括號裏, 這種情況稱爲多繼承。在Python中繼承有以下一些特點:

1) 在繼承中積累初始化方法__init__()函數不會被自動調用。如果希望子類調用基類的__init__() 方法, 需要在子類的 __init__() 方法中顯示調用它。這與C++差別很大。

2) 在調用基類的方法時, 需要加上基類的類名前綴, 且帶上 self 參數變量, 注意在類中調用該類中定義的方法時不需要self參數。

3) Python總是首先查找對應類的方法, 如果在子類中沒有對應的方法, Python纔會在繼承鏈的基類中按順序查找。

4) 在Python繼承中, 子類不能訪問基類的私有成員。

這是最後一次修改類Charmander的代碼:

class pokemon:
    def __init__(self, name, gender, level, type, status):
        self.__type = type
        self.__gender = gender
        self.__name = name
        self.__level = level
        self.__status = status
        self.__info = [self.__name, self.__type, self.__gender, self.__level, self.__status]
        self.__index = -1
        
    # 精靈體力, 攻擊, 防禦, 特攻, 特防, 速度
    def getName(self):
        return self.__name
    
    def getGender(self):
        return self.__gender
    
    def getType(self):
        return self.__type

    def getStatus(self):
        return self.__status
        
    def level_up(self):
        self.__status = [s+1 for s in self.__status]
        self.__status[0] += 1
    
    def __iter__(self):
        print('名字 屬性 性別 等級 能力')
        return self
    
    def next(self):
        if self.__index == len(self.__info) - 1:
            raise StopIteration
        self.__index += 1
        return self.__info[self.__index]
        
        
class Charmander(pokemon):
    def __init__(self, name, gender, level):
        self.__type = ('fire', None)
        self.__gender = gender
        self.__name = name
        self.__level = level
        self.__status = [10+2*level, 5+1*level, 5+1*level, 5+1*level, 5+1*level, 5+1*level]
        pokemon.__init__(self, self.__name, self.__gender, self.__level, self.__type, self.__status)
>>> x = Charmander('小火猴', 'male', 5)
>>> print(x.getGender())
male
>>> for info in x:
        print(info)
小火猴 ('fire', None) male 5 [20, 10, 10, 10, 10, 10]        

我們定義了Charmander 類的基類pokemon, 將精靈共有的行爲都放到基類中, 子類僅僅需要向基類傳輸數據屬性即可。這樣就可以很輕鬆地定義其他基於pokemon類的子類。因爲某米賽爾號精靈有數千只, 使用繼承的方法可以大大減少代碼量, 且當需要對全部精靈進行整體改變時僅需改變pokemanl類的__init__()即可, 並向基類傳輸數據, 這裏注意要加self參數, Charmander 類沒有繼承基類的私有數據屬性, 因此在子類只有一個self.__type, 不會出現因繼承所造成的重名情況。爲了能更加清晰地描述這個問題, 這裏再舉一個例子:

class animal:
    def __init__(self):
        self.__age = age
    def print2(self):
        pritn(self.__age)

class dog(animal):
    def__init__(self, age):
        animal.__init__(self, age)
    def print2(self):
        print(self.__age)
>>> a_animal = animal(10)
>>> a_animal.print2()
10
>>> a_dog = dog(10)
>>> a_dog.print2()
Traceback (most recent call last):
  File "seer.py", line 13, in <module>
    a_dog.print2()
  File "seer.py", line 11, in print2
    print(self.__age)
AttributeError: 'dog' object has no attribute '_dog__age'

That's all !

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章