面向對象的設計思想是從天然界中來的,由於在天然界中,類(Class)和實例(Instance)的概念是很天然的。Class是一種抽象概念,好比咱們定義的Class——Student,是指學生這個概念,而實例(Instance)則是一個個具體的Student,好比,Bart Simpson和Lisa Simpson是兩個具體的Student。html
面向對象的抽象程度又比函數要高,由於一個Class既包含數據,又包含操做數據的方法。python
數據封裝、繼承和多態是面向對象的三大特色,咱們後面會詳細講解。程序員
類和實例
類(Class
)和實例(Instance
)是面向對象最重要的概念。sql
類是指抽象出的模板。實例則是根據類建立出來的具體的「對象」,每一個對象都擁有從類中繼承的相同的方法,但各自的數據可能不一樣。編程
在python中定義一個類:安全
classStudent(object): pass
關鍵字class
後面跟着類名,類名一般是大寫字母開頭的單詞,緊接着是(object)
,表示該類是從哪一個類繼承下來的。一般,若是沒有合適的繼承類,就使用object
類,這是全部類最終都會繼承下來的類。網絡
定義好了 類,就能夠根據Student
類建立實例:app
>>> 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__()
方法接收到的參數依次是:
- 當前準備建立的類的對象;
- 類的名字;
- 類繼承的父類集合;
- 類的方法集合。