假裝用某米賽爾號的角度看Python面向物件程式設計
類和物件
下面我們正式建立自己的類, 這裡我們使用Python自定義某米賽爾號的精靈, 程式碼如下:
class Elf: 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 = Elf() >>> y = Elf() >>> x.setName('小火猴') >>> y.setName('皮皮') >>> x.getName() 小火猴 >>> y.getName() 皮皮 >>> x.getInfo() <__main__.Elf instance at 0xXXXXXXXX> >>> y.getInfo() <__main__.Elf instance at 0xXXXXXXXX>
建立物件和呼叫一個函式很相似, 使用類名作為關鍵字建立一個類的物件, 實際上, Elf() 的括號裡是可以有引數的, 後面我們會討論到。我們有兩隻精靈, 一隻是小火猴, 一隻是皮皮, 並且對他們執行 getName() , 名字正確返回。觀察 getInfo() 的輸出, 返回的是包含地址的具體物件資訊, 可以看到兩個物件的地址, 是不一樣的。Python 中的self作用和 C++ 中的 *this 指標類似, 在呼叫 精靈物件 的 setName() 和 getName() 函式時, 函式都會自動把該物件的地址作為第一個引數傳入(該資訊包含在引數 self 中), 這就是為什麼我們呼叫函式時不需要寫 self , 而在函式定義時需要把引數作為第一個引數。傳入物件地址是相當必要的, 如果不傳入地址, 程式就不知道要訪問類的哪一個物件。
類的每個物件都會有各自的資料屬性, Elf 類中有資料屬性 name, 這是通過setName() 函式中的語句 self.name = name建立的。這個語句中的兩個 name 是不一樣的, 它們的作用域不一樣。第一個 name 通過 self 語句宣告的作用域是類 Elf() 的作用域, 將其作為物件 x 的資料屬性進行儲存, 而後面的 name 的作用域是函式的區域性作用域, 與引數中的 name 相同。而後面 getName() 函式返回的是物件中的 name。
__init__()方法
從更深層邏輯去說, 我們捕捉到精靈的那一刻應該就有名字, 而並非捕捉後去設定。所以這裡我們需要的是一個初始化手段。Python中的__init__() 方法用於初始化類的例項物件。__init__() 函式的作用一定程度上與C++的建構函式相似, 但並不等於。C++ 的建構函式是使用該函式去建立一個類的例項物件, 而Python執行__init__() 方法時例項物件已被構造出來。__init__()方法會在物件構造出來後自動執行, 所以可以用於初始化我們所需要的資料屬性。修改Elf 類的程式碼, 程式碼如下:
class Elf: 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
在此處我們增加了幾個資料的屬性: 性別、等級、能力和精靈屬性。連同前面的名字, 都放在__init__()方法進行初始化。資料屬性是可以使用任意資料型別的, 小火猴屬性是火, 而精靈可能會有倆個屬性, 假設小火猴經過兩次進化稱為烈焰猩猩屬性為地面, 火系。為了保持資料型別的一致性, 所以我們使用元組儲存, 並讓小火猴的第二個屬性為 None。由於小火猴的屬性是固定的, 所以在__init__() 的輸入引數不需要 type。而精靈的能力會隨著等級的不同而不同, 所以在初始化中也需要實現這一點。我們建立例項物件測試程式碼:
>>> x = Elf('小火猴', 'male', 5) >>> y = Elf('皮皮', '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 = Elf('小火猴', '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 不支援直接私有方式, 但可以使用一些小技巧達到私有特性的目的。為了讓方法的資料屬性或方法變為私有, 只需要在它的名字前面加上雙下劃線即可, 修改Elf 類程式碼:
# 自定義類 class Elf: 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 = Elf('小火猴', 'male', 5) >>> print(x.type) Traceback (most recent call last): File "seer.py", line 25, in <module> print(x.type) AttributeError: 'Elf' 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: 'Elf' object has no attribute 'test'
現在在程式外部直接訪問私有資料是不允許的, 我們只能通過設定好的節後函式去調取物件資訊。不過通過雙下劃綫實現的私有實際上是"偽私有化" , 實際上我們還是可以做到從外部訪問這些私有屬性。
>>> print(x._Elf__type) ('fire', None)
Python 使用的是一種name_mangling 技術, 將__membername 替換成_class__membername , 在外部使用原來的私有成員時, 會提示無法找到, 而上面執行的 x.Elf__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, 再次修改類 Elf 的程式碼, 通過迭代器能輸出物件的全部資訊。
class Elf: 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繼承中, 子類不能訪問基類的私有成員。
這是最後一次修改類Elf的程式碼:
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 Elf(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 = Elf('小火猴', 'male', 5) >>> print(x.getGender()) male >>> for info in x: print(info) 小火猴 ('fire', None) male 5 [20, 10, 10, 10, 10, 10]
我們定義了Elf 類的基類pokemon, 將精靈共有的行為都放到基類中, 子類僅僅需要向基類傳輸資料屬性即可。這樣就可以很輕鬆地定義其他基於pokemon類的子類。因為某米賽爾號精靈有數千只, 使用繼承的方法可以大大減少程式碼量, 且當需要對全部精靈進行整體改變時僅需改變pokemanl類的__init__()即可, 並向基類傳輸資料, 這裡注意要加self引數, Elf 類沒有繼承基類的私有資料屬性, 因此在子類只有一個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'