面向對象的設計思想是從自然界中來的,因為在自然界中,類(Class)和實例(Instance)的概念是很自然的。Class是一種抽象概念,比如我們定義的Class——Student,是指學生這個概念,而實例(Instance)則是一個個具體的Student,比如,Bart Simpson和Lisa Simpson是兩個具體的Student。
面向對象的抽象程度又比函數要高,因為一個Class既包含數據,又包含操作數據的方法。
數據封裝、繼承和多態是面向對象的三大特點,我們后面會詳細講解。
類和實例
類(Class
)和實例(Instance
)是面向對象最重要的概念。
類是指抽象出的模板。實例則是根據類創建出來的具體的“對象”,每個對象都擁有從類中繼承的相同的方法,但各自的數據可能不同。
在python中定義一個類:
classStudent(object): pass
關鍵字class
后面跟着類名,類名通常是大寫字母開頭的單詞,緊接着是(object)
,表示該類是從哪個類繼承下來的。通常,如果沒有合適的繼承類,就使用object
類,這是所有類最終都會繼承下來的類。
定義好了 類,就可以根據Student
類創建實例:
>>> classStudent(object): ... pass ... >>> bart = Student() # bart是Student()的實例 >>> bart <__main__.Student object at 0x101be77f0> >>> Student # Student 本身是一個類 <class'__main__.Student'>
可以自由地給一個實例變量綁定屬性,比如,給實例bart綁定一個name屬性:
>>> bart.name = "diggzhang" >>> bart.name 'diggzhang'
類同時也可以起到模板的作用,我們可以在創建一個類的時候,把一些認為公共的東西寫進類定義中去,在python中通過一個特殊的__init__
方法實現:
classStudent(object): """__init__ sample.""" def__init__(self,name,score): self.name = name self.score = score
__init__
方法的第一個參數永遠都是self
,表示創建實例本身,在__init__
方法內部,可以把各種屬性綁定到self
,因為self
指向創建的實例本身。
有了__init__
方法,在創建實例的時候,就不能傳入空的參數了,必須傳入與__init__
方法匹配的參數,但self
不需要傳,Python解釋器自己會把實例變量傳進去。如下面的類,在新建實例的時候,需要把name
和score
屬性捆綁上去:
classStudent(object): """example for __init__ function passin args.""" def__init__(self,name,score): self.name = name self.score = score
我們直接看個實例,如果我們老老實實傳name和score進去的時候,成功聲明了這個實例,但是只傳一個值的時候,報錯:
In [1]: class Student(object): ...: def __init__(self, name, score): ...: self.name = name ...: self.score = score ...: In [2]: bart = Student('diggzhang', 99) In [3]: bart.name Out[3]: 'diggzhang' In [4]: bart.score Out[4]: 99 In [5]: bart_test = Student('max') --------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-6-97f4e2f67951> in <module>() ----> 1 bart_test = Student('max') TypeError: __init__() takes exactly 3 arguments (2 given)
和普通函數相比,在類中定義的函數只有一點不同,就是第一個參數永遠是實例變量self
,並且,調用時,不用傳遞該參數。除此之外,類的方法和普通函數沒有什么區別。
面向對象編程的一個重要特點就是數據封裝。在上面的Student
類中,每個實例就擁有各自的name
和score
這些數據。我們可以通過函數來訪問這些數據,比如打印一個學生的成績:
defprint_socre(std): print("%s: %s" % (std.name, std.score)) print_socre(bart) # 實際執行效果 In [7]: defprint_socre(std): ...: print("%s: %s" % (std.name, std.score)) ...: In [8]: print_socre(bart) diggzhang: 99
既然我們創建的實例里有自身的數據,如果想訪問這些數據,就沒必要從外面的函數去訪問,可以在Student
類內部去定義這樣一個訪問數據的函數,這樣就把“數據”給封裝起來了。這些封裝數據的函數和Student
類本身關聯起來的,我們稱之為類的方法:
classStudent(object): def__init__(self,name,score): self.name = name self.score = score defprint_socre(self): print("%s: %s" % (self.name, self.score))
要定義一個類的方法,除了傳入的第一個參數是self
外,其它和普通函數一樣。如果想調用這個方法,直接在實例變量上調用,除了self
不用傳遞,其余參數正常傳入:
>>> bart.print_score()
Bart Simpson: 59
實際代碼,需要在Python3環境中測試,Python2.7會報錯(NameError: global name 'name' is not defined
)
$ python3 Python 3.5.1 (v3.5.1:37a07cee5969, Dec 5 2015, 21:12:44) [GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> classStudent(object): ... def__init__(self,name,score): ... self.name = name ... self.score = score ... defprint_score(self): ... print("%s: %s" % (self.name, self.score)) ... >>> bart = Student('zhang', 99) >>> bart.print_score() zhang: 99 >>>
數據和邏輯都被封裝起來,直接調用方法即可,但卻可以不用知道內部的細節。
總結一下。
類 是創建實例的模板,而 實例 則是一個一個具體的對象,各個實例擁有的數據都互相獨立,互不影響;
方法 就是與實例綁定的函數,和普通函數不同,方法可以直接訪問實例的數據;
通過在實例上調用方法,我們就直接操作了對象內部的數據,但無需知道方法內部的實現細節。
和靜態語言不同,Python允許對實例變量綁定任何數據,也就是說,對於兩個實例變量,雖然它們都是同一個類的不同實例,但擁有的變量名稱都可能不同:
# 用相同類創建了兩個不同實例 >>> bart = Student('Bart Simpson', 59) >>> lisa = Student('Lisa Simpson', 87) # 給其中一個實例綁定了一個變量名age >>> bart.age = 8 >>> bart.age 8 # 另一個同類實例中是沒有age的 >>> lisa.age Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'Student' object has no attribute 'age' >>>
至此,總算搞明白了什么是類,什么是對象。如何定義類,如何定義類內的方法。同類創建出的不同實例的相同和不同。
封裝
在Class
內部,可以有屬性和方法,而外部代碼可以通過直接調用實例變量的方法來操作數據,這樣,就隱藏了內部的復雜邏輯。
但是,從前面Student類的定義來看,外部代碼還是可以自由地修改一個實例的name、score屬性:
>>> bart = Student('Bart Simpson', 98) >>> bart.score 98 >>> bart.score = 59 >>> bart.score 59
如果想讓內部屬性不被外部訪問,可以把屬性的名稱前加上兩個下划線__
,在Python中,實例的變量名如果以雙下划線開頭,就變成了一個私有變量(private
),只有內部可以訪問,外部不能訪問:
classStudent(object): def__init__(self,name,score): self.__name = name self.__score = score defprint_score(self): print('%s: %s' % (self.__name, self.__score))
改完后,對於外部代碼來說,沒有什么變動,但是已經無法從外部訪問到實例變量.__name
和實例變量
:
>>> bart = Student('Bart Simpson', 98) >>> bart.__name Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'Student' object has no attribute '__name'
這樣就確保了外部代碼不能隨意修改對象內部的狀態,這樣通過訪問限制的保護,代碼更加健壯。
如果外部還需要訪問到這兩個內部狀態的話,可以給Student
類增加get_name
和get_score
這樣的方法。如果外部還有修改需求的話,就給該類再增加set_score
或set_name
方法。用這樣的方式去get set 一個內部保護量:
classStudent(object): defget_name(self): return self.__name defget_score(self): return self.__score defset_name(self,name): self.__name = name defset_score(self,score): self.__score = score # 對於set_score(self, score)我們可以借由set方法順便做參數檢查,提高代碼安全性 defset_safe_score(self,score): if score >= 0 and score <= 100: self.__score = score else: raise ValueError('bad score')
需要注意的是,Python中如果變量名以雙下划線開頭和結尾的,是特殊變量__XXX__
。特殊變量是可以直接從類內部訪問的。
有些時候,你會看到以一個下划線開頭的實例變量名,比如_name
,這樣的實例變量外部是可以訪問的,但是,按照約定俗成的規定,當你看到這樣的變量時,意思就是,“雖然我可以被訪問,但是,請把我視為私有變量,不要隨意訪問”。
雙下划線開頭的實例變量是不是一定不能從外部訪問呢?其實也不是。不能直接訪問__name
是因為Python解釋器對外把__name
變量改成了_Student__name
,所以,仍然可以通過_Student__name
來訪問__name
變量:
>>> bart._Student__name 'Bart Simpson'
但是強烈建議你不要這么干,因為不同版本的Python解釋器可能會把__name改成不同的變量名。
Python的訪問限制其實並不嚴格,主要靠自覺。
繼承和多態
在OOP程序設計中,當我們定義一個class的時候,可以從某個現有的class繼承,新的class稱為子類(Subclass),而被繼承的class稱為基類、父類或超類(Base class、Super class)。
比如,我們已經編寫了一個名為Animal的class,有一個run()方法可以直接打印一句話,然后新建一個叫Dog
的類,繼承了Animal
類:
classStudent(object): defget_name(self): return self.__name defget_score(self): return self.__score defset_name(self,name): self.__name = name defset_score(self,score): self.__score = score # 對於set_score(self, score)我們可以借由set方法順便做參數檢查,提高代碼安全性 defset_safe_score(self,score): if score >= 0 and score <= 100: self.__score = score else: raise ValueError('bad score')
對於Dog來說,Animal就是它的父類,對於Animal來說,Dog就是它的子類。
子類獲得了父類的全部功能。Dog()里繼承了run()函數,可以給自己的實例里直接用。
那么問題來了,子類和父類如果定義的時候都有個run()
,會發生什么?
classAnimal(object): defrun(self): print('running...') classDog(Animal): defrun(self): print("Dog running...") classCat(Animal): defrun(self): print("Cat running...") # 結果如下 Dog is running... Cat is running...
子類的的方法如果和父類的方法重名,子類會覆蓋掉父類。因為這個特性,就獲得了一個繼承的好處”多態”。
當我們定義一個class的時候,實際上也就是定義了一種數據類型。跟list str dict
一個意思。使用isinstance(待判斷值, 數據類型)
可以做數據類型判定。
>>> a = list() >>> b = Animal() >>> c = Dog() >>> isinstance(a, list) True >>> isinstance(a, dict) False >>> isinstance(b, Animal) True >>> isinstance(c, Dog) True
有意思的是,Dog繼承自Animal,那么Dog的實例同事也是Animal數據類型:
>>>isinstance(c, Animal) True # 但是如果繼承自父類,想跟子類去做判斷的話返回False >>>isinstance(b, Dog) False
要理解多態的好處,我們還需要再編寫一個函數,這個函數接受一個Animal類型的變量:
""" run_twice() 函數接收了一個`Animal`類型的變量 """ defrun_twice(animal): animal.run() animal.run() >>>defrun_twice(animal): ... animal.run() ... animal.run() ... """ 當我們將Animal()的實例傳入run_twice中... """ >>>run_twice(Animal()) running... running... """ 當我們將Dog()的實例傳入run_twice中... """ >>>run_twice(Dog()) running... running... >>>
看上去沒啥意思,但是仔細想想,現在,如果我們再定義一個Tortoise類型,也從Animal派生:
>>>classTortoise(Animal): ... defrun(self): ... print("Tortoise is running slowly...") ... """ 當我們調用run_twice()時,傳入Tortoise的實例 """ >>>run_twice(Tortoise()) Tortoise is running slowly... Tortoise is running slowly... >>>
Tortoise作為Animal的子類,不必對run_twice()
做任何修改。實際上,任何依賴Animal
作為參數的函數或者方法都可以不加修改地正常運行,原因在於多態。
多態的好處就是,當我們需要傳入Dog、Cat、Tortoise……時,我們只需要接收Animal類型就可以了,因為Dog、Cat、Tortoise……都是Animal類型,然后,按照Animal類型進行操作即可。由於Animal類型有run()方法,因此,傳入的任意類型,只要是Animal類或者子類,就會自動調用實際類型的run()方法,這就是多態的意思:
對於一個變量,我們只需要知道它是Animal類型,無需確切地知道它的子類型,就可以放心地調用run()方法,而具體調用的run()方法是作用在Animal、Dog、Cat還是Tortoise對象上,由運行時該對象的確切類型決定,這就是多態真正的威力:調用方只管調用,不管細節,而當我們新增一種Animal的子類時,只要確保run()方法編寫正確,不用管原來的代碼是如何調用的。這就是著名的“開閉”原則:
- 對擴展開放:允許新增Animal子類;
- 對修改封閉:不需要修改依賴Animal類型的run_twice()等函數。
對於靜態語言(例如Java)來說,如果需要傳入Animal類型,則傳入的對象必須是Animal類型或者它的子類,否則,將無法調用run()方法。
對於Python這樣的動態語言來說,則不一定需要傳入Animal類型。我們只需要保證傳入的對象有一個run()方法就可以了:
classTimer(object): defrun(self): print('Start...')
這就是動態語言的“鴨子類型”,它並不要求嚴格的繼承體系,一個對象只要“看起來像鴨子,走起路來像鴨子”,那它就可以被看做是鴨子。
Python的“file-like object“就是一種鴨子類型。對真正的文件對象,它有一個read()方法,返回其內容。但是,許多對象,只要有read()方法,都被視為“file-like object“。許多函數接收的參數就是“file-like object“,你不一定要傳入真正的文件對象,完全可以傳入任何實現了read()方法的對象。
總結一下:
繼承可以把父類的所有功能都直接拿過來,這樣就不必重零做起,子類只需要新增自己特有的方法,也可以把父類不適合的方法覆蓋重寫。
動態語言的鴨子類型特點決定了繼承不像靜態語言那樣是必須的。
獲取對象信息
當我們拿到一個對象的引用時,如何知道這個對象是什么類型、有哪些方法呢?
type()
可以檢查類型。用法超級簡單:
>>> type(123) <class'int'>>>> type('helloworld')<class 'str'>>>> type(None)<class 'NoneType'>>>> type(abs)<class 'builtin_function_or_method'>>>> type(a)<class 'list'>>>> type(Animal)<class 'type'>>>> type(Dog)<class 'type'>>>> type(Dog())<class '__main__.Dog'>>>>
type()經常被用來做類型比較:
>>>type(123) == type(456) True >>>type(123) == int True >>>type(123) == type('123') False
判斷基本數據類型可以直接寫int
,str
等,但如果要判斷一個對象是否是函數怎么辦?可以使用types模塊中定義的常量:
>>>import types >>>deffn(): ... pass ... >>>type(fn) == types.FunctionType True >>>type(abs) == types.BuiltinFunctionType True >>>type(lambda x: x)==types.LambdaType True >>>type((x for x in range(10)))==types.GeneratorType True
還有大殺器isinstance()
。
對於class
的繼承關系來說,使用type()
就很不方便。我們要判斷class
的類型,可以使用isinstance()
函數。
我們回顧上次的例子,如果繼承關系是:
object -> Animal -> Dog -> Husky
那么,isinstance()
就可以告訴我們,一個對象是否是某種類型。這玩意兒也是上手熟系列:
>>> a = Animal() >>> b = Dog() >>> isinstance(c, Animal) True >>> isinstance(c, Dog) True >>> isinstance(a, Animal) True >>> isinstance(a, Dog) False
還可以判斷一個變量是否是某些類型中的一種,比如下面的代碼就可以判斷是否是list或者tuple:
>>>isinstance([1, 2, 3], (list, tuple)) True >>>isinstance((1, 2, 3), (list, tuple)) True >>>isinstance((1, 2, 3), (tuple)) True >>>isinstance((1, 2, 3), (list)) False
最后一個大殺器dir()
。
如果要獲得一個對象的所有屬性和方法,可以使用dir()
函數,它返回一個包含字符串的list,比如,獲得一個str對象的所有屬性和方法:
dir('ABC') [........,'__add__',.....,'__len__',...,'lower','upper'...]
類似__xxx__的屬性和方法在Python中都是有特殊用途的,比如__len__方法返回長度。在Python中,如果你調用len()函數試圖獲取一個對象的長度,實際上,在len()函數內部,它自動去調用該對象的__len__()方法,所以,下面的代碼是等價的:
>>> len('ABC') 3 >>> 'ABC'.__len__() 3
我們自己寫的類,如果也想用len(myObj)的話,就自己寫一個__len__()方法:
>>> classMyDog(object): ... def__len__(self): ... return 100 ... >>> dog = MyDog() >>> len(dog) 100
dir()
返回的非雙下划線樣子的,都是普通屬性或方法,比如lower
:
>>> 'ABC'.lower() 'abc'
當然既然能列出這屬性和方法,也可以相應的修改。python准備了getattr()、setattr()、hasattr()
,可以直接操作一個對象的狀態:
>>> classMyObject(object): ... def__init__(self): ... self.x = 9 ... defpower(self): ... return self.x + self.x ... >>> obj = MyObject() >>> hasattr(obj, 'x') # 有屬性'x'嗎? True >>> obj.x 9 >>> hasattr(obj, 'y') # 有屬性'y'嗎? False >>> setattr(obj, 'y', 19) # 設置一個屬性'y' >>> hasattr(obj, 'y') # 有屬性'y'嗎? True >>> getattr(obj, 'y') # 獲取屬性'y' 19 >>> obj.y # 獲取屬性'y' 19 >>> hasattr(obj, 'power') # 有屬性'power'嗎? True >>> getattr(obj, 'power') # 獲取屬性'power' <bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>>>>> fn = getattr(obj, 'power') # 獲取屬性'power'並賦值到變量fn >>> fn # fn指向obj.power <bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>>>>> fn() # 調用fn()與調用obj.power()是一樣的 81
實際編碼過程中,可以設置一個default值,如果屬性不存在,就返回默認值:
>>> getattr(obj, 'k', 404) 404
通過內置的一系列函數,我們可以對任意一個Python對象進行剖析,拿到其內部的數據。要注意的是,只有在不知道對象信息的時候,我們才會去獲取對象信息。如果可以直接寫:
>>> getattr(obj, 'k', 404) 404
就不要寫:
sum = getattr(obj, 'x') + getattr(obj, 'y')
一個正確的用法如下:
defreadImage(fp): if hasattr(fp, 'read'): return readData(fp) return None
假設我們希望從文件流fp中讀取圖像,我們首先要判斷該fp對象是否存在read方法,如果存在,則該對象是一個流,如果不存在,則無法讀取。hasattr()就派上了用場。
請注意,在Python這類動態語言中,根據鴨子類型,有read()方法,不代表該fp對象就是一個文件流,它也可能是網絡流,也可能是內存中的一個字節流,但只要read()方法返回的是有效的圖像數據,就不影響讀取圖像的功能。
如果你成功看到這部分,你可以跟自己說:“來了,這份感覺終於來了,我的人生開始贏了。”
實例屬性和類屬性
由於Python是動態語言,根據類創建的實例可以任意綁定屬性。那就會有這種情況:
classStudent(object): name = 'Student'
類的名字是Student
,類里的屬性也叫Student
。這會導致黑人問號臉。
>>> classStudent(object): ... name = 'Student' ... >>> s = Student() # 創建實例s >>> print(s.name) # 打印name屬性,因為實例並沒有name屬性,所以會繼續查找class的name屬性 Student >>> print(Student.name) # 打印類的name屬性 Student >>> s.name = 'Michael' # 給實例綁定name屬性 >>> print(s.name) # 由於實例屬性優先級比類屬性高,因此,它會屏蔽掉類的name屬性 Michael >>> print(Student.name) # 但是類屬性並未消失,用Student.name仍然可以訪問 Student >>> del s.name # 如果刪除實例的name屬性 >>> print(s.name) # 再次調用s.name,由於實例的name屬性沒有找到,類的name屬性就顯示出來了 Student
從上面的例子可以看出,在編寫程序的時候,千萬不要把實例屬性和類屬性使用相同的名字,因為相同名稱的實例屬性將屏蔽掉類屬性,但是當你刪除實例屬性后,再使用相同的名稱,訪問到的將是類屬性
數據封裝、繼承和多態只是面向對象程序設計中最基礎的3個概念。在Python中,面向對象還有很多高級特性,允許我們寫出非常強大的功能。
接下來我們會討論多重繼承、定制類、元類等概念。
使用 slots
正常情況下,當我們定義了一個class,創建了一個class的實例后,我們可以給該實例綁定任何屬性和方法。但是,如果我們想要限制實例的屬性怎么辦?
為了達到限制的目的,Python允許在定義class的時候,定義一個特殊的__slots__變量,來限制該class實例能添加的屬性:
classStudent(object): __slots__ = ('name', 'age') # 用tuple定義允許綁定的屬性名稱 """實際執行效果""" >>>classStudent(object): ... __slots__ = ('name', 'age') ... >>>s = Student() >>>s.name = 'digg' >>>s.age = '19' >>>s.score = 99 Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'Student' object has no attribute 'score' >>>
由於’score’沒有被放到__slots__中,所以不能綁定score屬性,試圖綁定score將得到AttributeError的錯誤。
使用__slots__
要注意,__slots__
定義的屬性僅對當前類實例起作用,對繼承的子類是不起作用的:
>>>classGraduateStudent(Student): ... pass ... >>>g = GraduateStudent() >>>g.score = 9999
除非在子類中也定義__slots__,這樣,子類實例允許定義的屬性就是自身的__slots__加上父類的__slots__。
使用 @property
在綁定屬性時,如果我們直接把屬性暴露出去,雖然寫起來很簡單,但是,沒辦法檢查參數,導致可以把成績隨便改:
s = Student()
s.score = 9999
這顯然不合邏輯。為了限制score的范圍,可以通過一個set_score()方法來設置成績,再通過一個get_score()來獲取成績,這樣,在set_score()方法里,就可以檢查參數:
classStudent(object): defget_score(self): return self._socre defset_socre(self,value): if not isinstance(value, int): raise ValueError('score must be an integer!') if value < 0 or value > 100: raise ValueError('score must between 0 - 100.')
現在,對任意的Student實例進行操作,就不能隨心所欲地設置score了:
classStudent(object): defget_score(self): return self._socre defset_socre(self,value): if not isinstance(value, int): raise ValueError('score must be an integer!') if value < 0 or value > 100: raise ValueError('score must between 0 - 100.')
有沒有既能檢查參數,又可以用類似屬性這樣簡單的方式來訪問類的變量呢?對於追求完美的Python程序員來說,這是必須要做到的!
Python的裝飾器(decorator)可以給函數動態加上功能。對於類的方法,裝飾器一樣起作用。Python內置的@property裝飾器
就是負責把一個方法變成屬性調用的:
classStudent(object): @property defscore(self): return self._score @score.setter defscore(self,value): if not isinstance(value, int): raise ValueError('score must be an integer!') if value < 0 or value > 100: raise ValueError('score must between 0 - 100!') self._score = value
把一個getter方法變成屬性,只需要加上@property
就可以了。此時,@property
本身又創建了另一個裝飾器@score.setter
,負責把一個setter方法變成屬性賦值,於是,我們就擁有一個可控的屬性操作。看一下實際執行效果:
>>> classStudent(object): ... @property ... defscore(self): ... return self._score ... @score.setter ... defscore(self,value): ... if not isinstance(value, int): ... raise ValueError('score must be integer!') ... if value < 0 or value > 100: ... raise ValueError('score must between 0 - 100!') ... self._score = value ... >>> s = Student() >>> s.score = 60 >>> s.score 60 >>> s.score = 9999 Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 10, in score ValueError: score must between 0 - 100! >>>
還可以定義只讀屬性,只定義getter方法,不定義setter方法就是一個只讀屬性:
classStudent(object): @property defbirth(self): return self._birth @birth.setter defbirth(self,value): self._birth = value @property defage(self): return 2015 - self._birth
上面的birth
是可讀寫屬性,而age
就是一個只讀屬性,因為age
可以根據birth
和當前時間計算出來。
@property
廣泛應用在類的定義中,可以讓調用者寫出簡短的代碼,同時保證對參數進行必要的檢查,這樣,程序運行時就減少了出錯的可能性。
廖老師給了一個作業:
利用@property給一個Screen對象加上width和height屬性,以及一個只讀屬性resolution。
""" 作業解決方案 """ >>>classScreen(object): ... @property ... defwidth(self): ... return self._width ... @width.setter ... defwidth(self,value): ... self._width = value ... @property ... defheight(self): ... return self._height ... @height.setter ... defheight(self,value): ... self._height = value ... @property ... defresolution(self): ... return self._width * self._height ... >>>s = Screen() >>>s.width = 1024 >>>s.height = 768 >>>s.resolution 786432 >>>
多重繼承
繼承是面向對象編程的一個重要的方式,因為通過繼承,子類就可以擴展父類的功能。
之前我們的講的例子中有Animal
類,以及繼承了Animal類的Dog
類。這個繼承關系是單向的。我們可以再創建一個類,讓Dog繼承Animal同時,繼承新建的類:
classRunnable(object): defrun(self): print("I'm running...")
多重繼承:
classDog(Animal,Runnable): pass
通過多重繼承,一個子類就可以同時獲得多個父類的所有功能。
這里有個概念叫Mixin
。在設計類的繼承關系時,通常,主線都是單一繼承下來的,例如,Dog繼承自Animal。但是,如果需要“混入”額外的功能,通過多重繼承就可以實現,比如,讓Dog除了繼自Animal外,再同時繼承Runnable。這種設計通常稱之為MixIn。
MixIn的目的就是給一個類增加多個功能,這樣,在設計類的時候,我們優先考慮通過多重繼承來組合多個MixIn的功能,而不是設計多層次的復雜的繼承關系。
通過各種組合繼承類,不需要復雜而龐大的繼承鏈,只要選擇組合不同的類的功能,就可以快速構造出所需的子類。由於Python允許使用多重繼承,因此,MixIn就是一種常見的設計。
只允許單一繼承的語言(如Java)不能使用MixIn的設計。
定制類
看到類似__slots__
這種形如__xxx__
的變量或者函數名就要注意,這些在Python中是有特殊用途的。
__slots__
我們已經知道怎么用了,__len__()
方法我們也知道是為了能讓class作用於len()
函數。
除此之外,Python的class中還有許多這樣有特殊用途的函數,可以幫助我們定制類。
__str__
>>> classStudent(object): ... def__init__(self,name): ... self.name = name ... >>> print(Student('diggzhang')) <__main__.Student object at 0x1016e4828> # 這里打印了一堆丑東西 >>>
如果想改變這堆打印的的丑東西,就需要用到__str___
,在類里重新定義這個方法就可以了:
>>> classStudent(object): ... def__init__(self,name): ... self.name = name ... def__str__(self): ... return "Student name is %s" % self.name ... >>> print(Student('diggzhang')) Student name is diggzhang >>> # 但是去掉print >>> Student('diggzhang') <__main__.Student object at 0x1016e4828>
去掉print打印丑是因為直接顯示變量不歸__str__
管了,由__repr__
管,一般這倆類如果定制的話,處理辦法都一樣,於是可以來個簡單的,在定制好__str__
后直接重新賦值給__str__
:
classStudent(object): def__init__(self,name): self.name = name def__str__(self): return 'Student object (name=%s)' % self.name __repr__ = __str__
__iter__
和__next__
如果一個類想被用於for ... in
循環,類似list
或tuple
那樣,就必須實現一個__iter__()
方法,該方法返回一個迭代對象,然后,Python的for循環就會不斷調用該迭代對象的__next__()
方法拿到循環的下一個值,直到遇到StopIteration
錯誤時退出循環。
classFib(object): def__init__(self): self.a, self.b = 0, 1 # 初始化兩個計數器 def__iter__(self): return self # 實例本身即是迭代對象,故而返回自己 def__next__(self): self.a, self.b = self.b, self.a + self.b # 計算下一個值 if self.a > 100000: # 退出循環條件 raise StopIteration(); return self.a # 測試 for n in Fib(): print(n)
__getitem__
Fib實例雖然能作用於for循環,看起來和list有點像,但是,把它當成list來使用還是不行,比如,取第5個元素:
>>> Fib()[5] Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'Fib' object does not support indexing
要表現得像list那樣按照下標取出元素,需要實現__getitem__()
方法:
classFib(object): def__getitem__(self,n): a, b = 1, 1 for x in range(n): a, b = b, a + b return a
這樣,就可以按下標訪問數列的任意一項了:
>>> f = Fib() >>> f[0] 1 >>> f[1] 1 >>> f[2] 2 >>> f[3] 3 >>> f[10] 89 >>> f[100] 573147844013817084101
__getattr__
還記得之前如果訪問實例中的屬性不存在就會拋出的no attribute
錯誤嗎?
__getattr__
可以動態的返回一個屬性,當要訪問的屬性不存在的時候,Python解釋器會試圖調用__getattr__(XXX)
來嘗試獲得需要的屬性。利用這一點,可以把一個類的所有屬性和方法調用全部動態化處理。
利用到實際中的例子,如果我們要實現幾個API的話,會需要對應的URL就寫一個對應的方法去處理。API一旦改動,SDK也跟着要改。
利用完全動態的__getattr__,我們可以寫出一個鏈式調用:
classChain(object): def__init__(self,path=''): self._path = path def__getattr__(self,path): return Chain('%s/%s' % (self._path, path)) def__str__(self): return self._path __repr__ = __str__ >>> Chain().status.user.timeline.list /status/user/timeline/list
這樣,無論API怎么變,SDK都可以根據URL實現完全動態的調用,而且,不隨API的增加而改變!
還有些REST API會把參數放到URL中,比如GitHub的API:
GET /users/:user/repos
調用時,需要把:user替換為實際用戶名。如果我們能寫出這樣的鏈式調用:
Chain().users('michael').repos
__call__
一個對象實例可以有自己的屬性和方法,當我們調用實例方法時,我們用instance.method()來調用。能不能直接在實例本身上調用呢?在Python中,答案是肯定的。
任何類,只需要定義一個__call__()方法,就可以直接對實例進行調用。請看示例:
classStudent(object): def__init__(self,name): self.name = name def__call__(self): print('My name is %s.' % self.name)
調用方法如下:
>>> s = Student('Michael') >>> s() # self參數不要傳入 My name is Michael.
__call__()
還可以定義參數。對實例進行直接調用就好比對一個函數進行調用一樣,所以你完全可以把對象看成函數,把函數看成對象,因為這兩者之間本來就沒啥根本的區別。
那么,怎么判斷一個變量是對象還是函數呢?其實,更多的時候,我們需要判斷一個對象是否能被調用,能被調用的對象就是一個Callable對象,比如函數和我們上面定義的帶有__call__()
的類實例:
>>>callable(Student()) True >>>callable(max) True >>>callable([1, 2, 3]) False >>>callable(None) False >>>callable('str') False
本節介紹的是最常用的幾個定制方法,還有很多可定制的方法,請參考Python的官方文檔。
使用枚舉類
當我們需要定義常量時,一個辦法是用大寫變量通過整數來定義,例如月份:
JAN = 1
FEB = 2
MAR = 3
...
NOV = 11
DEC = 12
好處是簡單,缺點是類型是int,並且仍然是變量。
更好的方法是為這樣的枚舉類型定義一個class
類型,然后,每個常量都是class
的一個唯一實例。Python提供了Enum
類來實現這個功能:
from enum import Enum Month = Enum('Month', ( 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ))
這樣我們就獲得了Month
類型的枚舉類,可以直接使用Month.Jan
來引用一個常量,或者枚舉它的所有成員:
>>> for name, member in Month.__members__.items(): ... print(name, '=>', member, ',', member.value) ... Jan => Month.Jan , 1 Feb => Month.Feb , 2 Mar => Month.Mar , 3 Apr => Month.Apr , 4 May => Month.May , 5 Jun => Month.Jun , 6 Jul => Month.Jul , 7 Aug => Month.Aug , 8 Sep => Month.Sep , 9 Oct => Month.Oct , 10 Nov => Month.Nov , 11 Dec => Month.Dec , 12 >>>
value屬性則是自動賦給成員的int常量,默認從1開始計數。
如果需要更精確地控制枚舉類型,可以從Enum派生出自定義類:
from enum import Enum, unique # @unique裝飾器可以幫助我們檢查保證沒有重復值。 @unique classWeekday(Enum): Sun = 0 # Sun的value被設定為0 Mon = 1 Tue = 2 Wed = 3 Thu = 4 Fri = 5 Sat = 6
Enum可以把一組相關常量定義在一個class中,且class不可變,而且成員可以直接比較。
from enum import Enum, unique # @unique裝飾器可以幫助我們檢查保證沒有重復值。 @unique classWeekday(Enum): Sun = 0 # Sun的value被設定為0 Mon = 1 Tue = 2 Wed = 3 Thu = 4 Fri = 5 Sat = 6
使用元類
動態語言和靜態語言最大的不同,就是函數和類的定義,不是編譯時定義的,而是運行時動態創建的。
比方說我們要定義一個Hello
的class,就寫一個hello.py
模塊:
classHello(object): defhello(self,name='world'): print('Hello, %s.' % name)
當Python解釋器載入hello模塊時,就會依次執行該模塊的所有語句,執行結果就是動態創建出一個Hello的class對象,測試如下:
>>> from hello import Hello >>> h = Hello() >>> h.hello() Hello, world. >>> print(type(Hello)) <class'type'>>>> print(type(h))<class 'hello.Hello'>
type()
函數可以查看一個類型或變量的類型,Hello
是一個class
,它的類型就是type
,而h
是一個實例,它的類型就是class Hello
。
class
的定義是運行時動態創建的,而創建class
的方法就是使用type()
函數。
type()
函數既可以返回一個對象的類型,又可以創建出新的類型,比如,我們可以通過type()
函數創建出Hello
類,而無需通過class Hello(object)...
的定義:
>>> deffn(self,name='world'): # 先定義函數 ... print('Hello, %s.' % name) ... >>> Hello = type('Hello', (object,), dict(hello=fn)) # 創建Hello class >>> h = Hello() >>> h.hello() Hello, world. >>> print(type(Hello)) <class'type'>>>> print(type(h))<class '__main__.Hello'>
要創建一個class
對象,type()
函數依次傳入3個參數:
type(‘Hello’, (object,), dict(hello=fn))
- class名稱;
- 繼承父類的集合,注意Python支持多重繼承,別忘了tuple的單元素寫法;
- class的方法名稱與函數綁定,這里我們把函數fn綁定到方法名hello上。
通過type()函數創建的類和直接寫class是完全一樣的,因為Python解釋器遇到class定義時,僅僅是掃描一下class定義的語法,然后調用type()函數創建出class。
正常情況下,我們都用class Xxx…來定義類,但是,type()函數也允許我們動態創建出類來,也就是說,動態語言本身支持運行期動態創建類,這和靜態語言有非常大的不同,要在靜態語言運行期創建類,必須構造源代碼字符串再調用編譯器,或者借助一些工具生成字節碼實現,本質上都是動態編譯,會非常復雜。
除了使用type()
動態創建類以外,要控制類的創建行為,還可以使用metaclass
。
metaclass,直譯為 元類 ,簡單的解釋就是:
當我們定義了類以后,就可以根據這個類創建出實例,所以:先定義類,然后創建實例。
但是如果我們想創建出類呢?那就必須根據metaclass創建出類,所以:先定義metaclass,然后創建類。
連接起來就是:先定義metaclass,就可以創建類,最后創建實例。
所以,metaclass允許你創建類或者修改類。換句話說,你可以把類看成是metaclass創建出來的“實例”。
來個例子感受一下,按照默認習慣,metaclass的類名總是以Metaclass結尾,以便清楚地表示這是一個metaclass:
# metaclass是類的模板,所以必須從`type`類型派生: classListMetaclass(type): def__new__(cls,name,bases,attrs): attrs['add'] = lambda self, value: self.append(value) return type.__new__(cls, name, bases, attrs)
有了ListMetaclass
,我們在定義類的時候還要指示使用ListMetaclass
來定制類,傳入關鍵字參數metaclass
:
classMyList(list,metaclass=ListMetaclass): pass
當我們傳入關鍵字參數metaclass時,魔術就生效了,它指示Python解釋器在創建MyList時,要通過ListMetaclass.__new__()
來創建,在此,我們可以修改類的定義,比如,加上新的方法,然后,返回修改后的定義。
__new__()
方法接收到的參數依次是:
- 當前准備創建的類的對象;
- 類的名字;
- 類繼承的父類集合;
- 類的方法集合。