《Python程式設計:從入門到實踐》筆記。
本章主要是對上一章Python類的補充。
1. 從一個類派生出所有類
上一篇文章說道Python類的定義與繼承一般是如下形式:
class A: # 或者寫成class A():
pass
class B(A):
pass
複製程式碼
其實,對於類A
,它並不是算是一個真正意義上的基類,而是和Java類似,Python中所有的類最終都繼承自object
類(首字母小寫,比較特殊),所以對於A
的定義可以寫成如下形式:
class A(object):
pass
複製程式碼
只是通常把object
給省略了。
2. 訪問限制
從上一篇中我們知道,類的屬性可以被直接訪問,如果需要對訪問做一些限制,我們可以通過定義相應的方法。在Python中,對於一般的屬性,用C++或Java的話來說,它們都是公有屬性,外部可以直接訪問,比如像下面的這個name
屬性:
class A:
def __init__(self, name):
self.name = name
複製程式碼
但如果我們在這個屬性前面加兩個下劃線,將其變成如下形式:
class A:
def __init__(self, name):
self.__name = name
複製程式碼
那麼name
就變成了一個私有屬性,它只能在物件的內部被訪問,如果想以如下形式訪問則會報錯:
# 程式碼:
class A:
-- snip --
a = A("test")
print(a.__name)
# 結果:
AttributeError: 'A' object has no attribute '__name'
複製程式碼
那是不是真的就訪問不到這個屬性了呢?說不清是有幸還是不幸,Python沒有所謂的真正的私有屬性,Python中類的所有屬性都能被訪問。Python直譯器只是將__name
換了個名稱,變成了:
self._A__name
複製程式碼
即在前面加了一個單下劃線和類名。
# 程式碼:
class A:
-- snip --
a = A("test")
print(a._A__name)
# 結果:
test
複製程式碼
強烈不建議這樣訪問屬性! 而且,不同版本的Python直譯器會將這樣的屬性改成不同的名字。
3. 使用裝飾器(decorator)
在上一點中說到了通過方法來訪問類的屬性,這種方式一般叫做get/set方法
,最後在呼叫時呼叫的是類的方法,現在我們使用Python內建的@property
裝飾器來訪問類的屬性,最後在呼叫時是呼叫的屬性,實際上它是將類的方法通過裝飾器變為屬性。以下是通過裝飾器和通過get/set方法
來訪問屬性的程式碼比較:
class Teacher:
def __init__(self, name):
self.name = name
def get_name(self):
return self.name
def set_name(self, name):
# 可以加上一些限制
self.name = name
class Student:
def __init__(self, name):
self._name = name
@property
def name(self):
return self._name
@name.setter
def name(self, name):
# 可以加上一些限制
self._name = name
t = Teacher("Miss")
s = Student("Boy")
print(t.get_name())
print(s.name)
t.set_name("Miss Lee")
s.name = "Kevin"
複製程式碼
從上述程式碼也可以看出,定義的時候,兩者的程式碼量區別其實不大,但是在呼叫的時候,明顯使用裝飾器更方便些。
4. 類中其它型別的屬性
類中除了普通的屬性,以及上述的私有屬性,還有前後都有雙下劃線的屬性,例如__xxx__
,它們是特殊變數,可以被直接訪問,不是私有屬性,所以一般不要起__name__
,__score__
這樣的屬性名,對於方法也是如此,不光有想__init__()
這樣的方法,還有很多前後都有雙下劃線的方法,比如__del__()
,它是類的解構函式。在以後的文章中還會介紹許多這種方法。
不光有雙下劃線的屬性,還有單下劃線的比如 _name
,前單下劃線,它表示的意思是:雖然能被訪問,但請將其看做私有屬性,不要隨便訪問。
5. 對於多型的補充
子類可以被看成是父類的型別,但父類不能被看成是子類的型別。比如:
# 程式碼:
class Animal:
pass
class Dog(Animal):
pass
a = Animal()
d = Dog()
print(isinstance(d, Animal))
print(isinstance(a, Dog))
# 結果:
True
False
複製程式碼
也就是說,如果我們定義了這樣一個函式:
def animal_run(animal):
animal.run()
複製程式碼
它接收Animal
及其子類的所有物件,只要該類的run()
方法正確編寫,Python都能在解釋時正確呼叫相應類的run()
方法,即呼叫方只管呼叫animal_run()
函式,不用管類的run()
方法的細節,不管是現有的類還是新擴充套件出的子類,只要保證run()
正確實現了,那麼animal_run()
就是正確的。這就是著名的“開閉原則”
:對擴充套件開放,對修改封閉。
6. 靜態語言與動態語言
仍以上面的animal_run()
函式為例。對於像Java這樣的靜態語言,傳入的引數必須是Animal
及其子類,否則就無法呼叫run()
方法。而對於像Python這樣的動態語言,傳入的不一定要求是Animal
及其子類,只要這個物件有run()
方法就行了。這就是動態語言的“鴨子型別”
,只要“看起來像鴨子,走起道來像鴨子”,那它就能被看做是鴨子。Python的“file-like object”
就是一種“鴨子型別”,對於真正的檔案物件,都有一個read()
方法,用於返回檔案內容。但對於其他物件,只要正確實現了read()
方法,即使它不是檔案物件,它也能被看做是檔案。
7. 多重繼承與MixIn設計
前一篇文章中的繼承是單繼承,但Python和C++一樣,支援多重繼承;Java只支援單繼承,她通過介面類來實現多重繼承的效果。首先需要搞清楚多重繼承為什麼存在。仍然以Animal
類為例,動物裡有哺乳動物,卵生動物,有能飛的動物和不能飛的動物,這是兩種大的分類方式。如果我們要派生出一個能飛的哺乳動物(比如蝙蝠),如果按照單一繼承,可以按如下方式:
也可以先從Animal
繼承出Runnable
和Flyable
兩個類,再繼承出哺乳類和卵生類(相當於將上圖的二三層換了位置),但從這種單繼承可以看出,如果分類增多,類的數量將呈指數級增加。故而一般採用多重繼承的方式:
class Animal:
pass
class Mammalia(Animal):
pass
class Flyable:
def fly(self):
print("Flying...")
class Bat(Mammalia, Flyable):
pass
複製程式碼
這樣Bat
類將具有Mammalia
和Flyable
兩個父類的所有屬性與方法。一般在Java中,以able
為結尾類的都作為介面。
在設計類的繼承的時候,一般主線都是單一繼承的,像上述例子中的從Animal
派生出Manmalia
,但如果後續的類中要混入一些額外的功能,但這功能又不是這個子類所獨有的,比如上述的Flyable
,那麼就可以通過多重繼承,從Manmalia
和Runnable
派生出Bat
類,這就是MinIn設計
,Java中採用介面來實現這種設計。
為了更好的看出繼承關係,一般將Runnable
和Flyable
類的名字改為RunnableMixIn
和FlyableMixIn
,同時,還可以定義出肉食動物CarnivorousMixIn
和植食動物HerbivoresMixIn
,讓子類同時擁有好幾個MinIn
:
class Dog(Mammalia, RunnableMixIn, CarnivorousMixIn):
pass
複製程式碼
所以在設計類時,我們應該優先考慮通過多重繼承來組合多個MinIn
,而不是直接考慮更多層次的繼承關係。
最後,本篇較多內容是根據廖雪峰老師的部落格再理解而來的,感謝廖雪峰老師!
迎大家關注我的微信公眾號"程式碼港" & 個人網站 www.vpointer.net ~