目錄
面向對象進階程式設計
__slots__
@property
定制類
使用枚舉類
使用元類
面向對象進階程式設計
__slots__
- 在上一篇文章講到過,我們可以通過簡單的操作就能夠給執行個體或類綁定屬性。這裡我們來讨論如何給執行個體和類綁定方法,先來看個例子:
需要注意的是:若是通過下面這種方式給執行個體綁定方法則在調用方法時,解釋器就不會自動把本執行個體作為一個參數傳入self中了,也就是不會将執行個體和方法進行綁定:from types import MethodType class Dog(): def __init__(self, name): self.Name = name def show(self): print('my name\'s %s' % self.Name) d = Dog('Dolly')
正确的綁定操作應該是這樣:In [2]: d.show = show In [3]: d.show() Traceback (most recent call last): File "<ipython-input-3-73a424ac9a82>", line 1, in <module> d.show() TypeError: show() missing 1 required positional argument: 'self'
從例子中可以看到,我們通過MethodType函數來給執行個體綁定方法。這裡我們僅僅是給執行個體d綁定了show方法,也就是說其它的執行個體并沒有show方法:In [5]: d.show = MethodType(show, d) #通過MethodType函數給執行個體d綁定方法 In [6]: d.show() #調用剛剛綁定的方法 my name's Dolly
為了解決這個問題,我們可以嘗試給類綁定方法,因為類的方法是所有執行個體所共有的,給類綁定方法非常簡單:In [8]: d2 = Dog('Mike') In [9]: d2.show() Traceback (most recent call last): File "<ipython-input-9-714779789114>", line 1, in <module> d2.show() AttributeError: 'Dog' object has no attribute 'show'
現在我們來試試:In [10]: def bark(self): ...: print('wang wang wang !') ...: In [11]: Dog.bark = bark
In [12]: d.bark() wang wang wang ! In [13]: d2.bark() wang wang wang !
- 有時我們想給執行個體綁定的屬性添加一個限定,使得屬性的綁定不能是任意的,而隻能綁定我們規定的幾種屬性。這就要使用一個特殊的變量——__slots__:
現在我們來試試給執行個體綁定屬性:class Dog(): __slots__ = ('name', 'gender') #限定Dog的執行個體隻能夠綁定name和gender這兩個屬性
我們看到在給執行個體d綁定name和gender這兩個屬性時沒問題,在綁定屬性color時就抛出了錯誤,因為color這個屬性是不被允許的!如果再定義一個Dog的子類Husky,并且Husky類中沒有對__slots__進行聲明,那麼Husky的執行個體對屬性的綁定是不受其父類Dog影響的,可以任意綁定;如果Husky中對__slots__也進行了聲明,那麼Husky執行個體所允許綁定的屬性是兩個聲明的__slots__屬性之和。In [2]: d = Dog() In [3]: d.name = 'Dolly' #給d綁定name屬性 In [4]: d.name Out[4]: 'Dolly' In [5]: d.gender = 'femal' #給d綁定gender屬性 In [6]: d.gender Out[6]: 'femal' In [7]: d.color = 'yello' #給d綁定color屬性 Traceback (most recent call last): File "<ipython-input-7-dd28abd9292c>", line 1, in <module> d.color = 'yello' AttributeError: 'Dog' object has no attribute 'color'
@property
- 通過對前面知識的學習,我們在給執行個體綁定屬性時一般進行類似以下操作:
但是這樣會産生的一個問題是:如果沒有任何措施加以限制的話,屬性值是可以改成任意值的,就比如上述例子中将age設為999999,這明顯不符合實際!也許我們可以定義一個set_age()方法用于修改屬性值,在方法中對屬性的取值加以限制:In [13]: class Dog(): ...: def __init__(self, name): ...: self._name = name #綁定屬性_name ...: In [14]: d = Dog('Dolly') In [16]: d.age = 999999 #綁定屬性age
現在我們來試試:class Dog(): def get_age(self): return self.age def set_age(self, val): if not isinstance(val, int): #檢查val的類型 raise ValueError('年齡隻能為整數!') elif val < 0 or val > 30: #檢查val的取值 raise ValueError('年齡隻能在0~30之間!') self.age = val
其實心細的同學會發現這沒從根本上解決問題,我們還是可以随意更改age的值,隻要不通過調用set_age()方法就行了:In [18]: d = Dog() In [19]: d.set_age('六歲') Traceback (most recent call last): File "<ipython-input-19-8626661de8b1>", line 1, in <module> d.set_age('六歲') File "C:/Users/Whisky/.spyder-py3/temp.py", line 10, in set_age raise ValueError('年齡隻能為整數!') ValueError: 年齡隻能為整數! In [20]: d.set_age(35) Traceback (most recent call last): File "<ipython-input-20-b611f54c4999>", line 1, in <module> d.set_age(35) File "C:/Users/Whisky/.spyder-py3/temp.py", line 12, in set_age raise ValueError('年齡隻能在0~30之間!') ValueError: 年齡隻能在0~30之間! In [21]: d.set_age(6) In [22]: d.get_age() Out[22]: 6
那麼到底怎麼解決這個問題呢?這就要使用Pyhton提供的@property裝飾器了!先來看下面的例子:In [24]: d.age = 100 In [25]: d.age Out[25]: 100
Python内置的@property裝飾器能夠把方法轉成屬性的調用,而另一個裝飾器@age.setter能夠把方法轉成屬性的指派,來看下怎麼使用:class Dog(): @property def age(self): return self._age ##這裡不要寫成self.age @age.setter def age(self, val): if not isinstance(val, int): raise ValueError('年齡必須為整數值!') elif val < 0 or val > 30: raise ValueError('年齡隻能在0~30間') self._age = val ##這裡不要寫成self.age = val
我們看到,在使用d.age = 999對age的值進行修改時,實際上是轉化為執行d.set_age(999),而999是age不能接受的取值故抛出錯誤。_在兩個方法中我們使用的屬性是_age而非age,其實将_age改成其他的名字也行,但就是不能寫成和age一樣!!!自己試試看會出現什麼情況!這實際上是給屬性age取了個别名_age,試試看直接通路_age會是什麼結果:In [21]: d = Dog() In [22]: d.age = 6 #實際上是轉化為d.set_age(6) In [23]: d.age #實際上是轉化為d.get_age() Out[23]: 6 In [27]: d.age = 999 #實際上是轉化為d.set_age(999) Traceback (most recent call last): File "<ipython-input-27-8e0eec63a531>", line 1, in <module> d.age = 999 File "C:/Users/Whisky/.spyder-py3/temp.py", line 14, in age raise ValueError('年齡隻能在0~30間') ValueError: 年齡隻能在0~30間
可以看到,我們其實還是可以直接修改age,你非要給age胡亂的設值也沒辦法,是以隻能要求你遵守程式設計規範,按規矩辦事😅。從上面這些例子我們可以到,@property修飾的方法是用于“讀”,而@屬性.setter修飾的方法是用于“寫”。是以如果要建立一個隻讀屬性那麼就隻需要添加@property裝飾器:In [24]: d._age Out[24]: 6 In [25]: d._age = 999 In [26]: d.age Out[26]: 999
class Dog(): @property def age(self): return self._age @age.setter def age(self, val): if not isinstance(val, int): raise ValueError('年齡必須為整數值!') elif val < 0 or val > 30: raise ValueError('年齡隻能在0~30間') self._age = val @property def birth(self): #birth為隻讀屬性 return 2020 - self.age
我們可以用一句話來總結這兩個裝飾器的功能:@property能夠使我們在通路屬性時按照我們自己規定的方式進行,@屬性.setter能夠使我們在設定屬性的值時按照我們自己規定的方式進行。In [58]: print(Dog.birth) <property object at 0x000002976BDCE868> In [59]: d.birth Out[59]: 2014 In [60]: d.birth = 1996 #嘗試修改隻讀屬性的值 Traceback (most recent call last): File "<ipython-input-60-d4aa4bab9960>", line 1, in <module> d.birth = 1996 AttributeError: can't set attribute
定制類
- __str__()。當我們在類中實作了__str__()方法後,在使用print()函數列印某個對象時,會自動調用__str__()函數:
class Dog(): def __init__(self, name): self._name = name def __str__(self): return 'hello, my name\'s %s' % self._name
那麼直接在控制台輸入d會列印出什麼結果呢?試試看:In [70]: d = Dog('Dolly') In [71]: print(d) hello, my name's Dolly
我們希望輸出的和print(d)結果一樣,實際上這兩者理應是一樣的。這時就需要實作__repr__()方法了,也許你會這樣實作:In [72]: d Out[72]: <__main__.Dog at 0x2976bdb2f28>
但其實上隻需要這樣就行了:class Dog(): def __init__(self, name): self._name = name def __str__(self): return 'hello, my name\'s %s' % self._name def __repr__(self): return 'hello, my name\'s %s' % self._name
實作__repr__()是針對開發者,也就是說,class Dog(): def __init__(self, name): self._name = name def __str__(self): return 'hello, my name\'s %s' % self._name __repr__ = __str__
是為調試服務的;而__str__()是針對使用者的,傳回使用者能夠靠的字元串。__repr__()
- __iter__()。在第一篇文章中講到過:凡是能用于for循環的就是Iterable類型,凡是能夠調用__next__()方法的就是Iterator類型,Iterator也屬于是Iterable類型。如果想要自己寫的類也能用于for...in,那就要在類中實作__iter__()方法,這樣,在使用for循環時就會不斷調用__next__()方法不斷生成下一個元素,直到抛出
StopIteration為止。
現在來試試:class Fib(): def __init__(self): self.a, self.b = 0, 1 def __iter__(self): return self def __next__(self): if self.b >= 500: raise StopIteration() self.a, self.b = self.b, self.a + self.b return self.b
那麼f是否是Iterable和Iterator類型的呢:In [117]: f = Fib() In [118]: for x in f: ...: print(x) ...: 1 2 3 5 8 13 21 34 55 89 144 233 377 610
In [119]: from collections import Iterable In [123]: from collections import Iterator In [124]: isinstance(f, Iterable) Out[124]: True In [125]: isinstance(f, Iterator) Out[125]: True
- __getitem__()。如果要讓我們的類能像list,str那樣能用下标對元素進行通路,那麼就要實作__getitem__()方法:
class Fib(): def __getitem__(self, idx): a, b = 1, 1 for i in range(idx): a, b = b, a + b return a
如果要能夠像list,str那樣能使用切片呢?那麼就要對__getitem__()再進行修改:In [139]: f = Fib() In [140]: f[10] Out[140]: 89 In [141]: f[100] Out[141]: 573147844013817084101
試試看:class Fib(): def __getitem__(self, oj): if isinstance(oj, int): #判斷傳入的對象是否是int類型 a, b = 1, 1 for i in range(oj): a, b = b, a + b return a elif isinstance(oj, slice): #判斷傳入的類型是否是slice類型 a, b = 1, 1 L = [] #定義一個list用于儲存結果 start = oj.start stop = oj.stop if start == None: start = 0 if stop == None: #預設設定stop最大為1000 stop = 1000 for i in range(start): a, b = b, a + b for i in range(start, stop): L.append(a) a, b = b, a + b return L
這裡隻是實作了簡單的切片操作,沒有對步長進行處理,也沒有考慮一些異常情況。In [147]: f[0] Out[147]: 1 In [148]: f[2] Out[148]: 2 In [149]: f[:20] Out[149]: [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765]
- __getattr__()。我們先定義一個類,并建立個執行個體:
當通路該執行個體已綁定的屬性時自然沒有問題:class Dog(): def __init__(self, name): self._name = name d = Dog('Dolly')
而通路不存在的屬性時就會抛出錯誤:In [186]: d._name Out[186]: 'Dolly'
為此,python中提供了__getattr__()應對此問題。我們可以在類中實作該方法,這樣,在通路到不存在的屬性時就會自動調用該方法生成屬性,比如:In [187]: d._gender Traceback (most recent call last): File "<ipython-input-187-984b0f00d8a6>", line 1, in <module> d._gender AttributeError: 'Dog' object has no attribute '_gender'
class Dog(): def __init__(self, name): self._name = name def __getattr__(self, attr): #調用該方法,自動生成屬性,注意!這裡的attr是字元串(經傳入的屬性名轉成了字元串形式) if attr == '_gender': return 'male' #會将_gender指派為'male'
該方法預設響應所有的屬性,如果屬性在__getattr__()中沒經過處理,預設傳回的是None,試試看:In [226]: Dog('Dolly')._gender Out[226]: 'male'
若要規定隻響應規定的幾個屬性,那麼就要這樣做:In [239]: hasattr(d, 'age') #測試是否有age屬性 Out[239]: True In [240]: hasattr(d, 'color') #測試是否有color屬性 Out[240]: True In [241]: d.age #預設傳回None,在控制台None是不顯示的 In [242]: d.color #預設傳回None,在控制台None是不顯示的 In [243]: d._gender #經過處理,傳回給定的值 Out[243]: 'male'
class Dog(): def __init__(self, name): self._name = name def __getattr__(self, attr): if attr == '_gender': return 'male' raise AttributeError("'Dog'的執行個體沒有綁定該屬性!")
In [246]: d = Dog('Mike') In [247]: d._gender Out[247]: 'male' In [248]: hasattr(d, 'color') #測試是否有color屬性 Out[248]: False In [249]: d.color #通路未綁定的color屬性 Traceback (most recent call last): File "<ipython-input-249-6063e1808d7c>", line 1, in <module> d.color File "C:/Users/Whisky/.spyder-py3/temp.py", line 10, in __getattr__ raise AttributeError("'Dog'的執行個體沒有綁定該屬性!") AttributeError: 'Dog'的執行個體沒有綁定該屬性! In [250]: d.age #通路未綁定的age屬性 Traceback (most recent call last): File "<ipython-input-250-ec62f7b57bf8>", line 1, in <module> d.age File "C:/Users/Whisky/.spyder-py3/temp.py", line 10, in __getattr__ raise AttributeError("'Dog'的執行個體沒有綁定該屬性!") AttributeError: 'Dog'的執行個體沒有綁定該屬性!
- __call__()。python中還提供了一個很有趣的方法__call__(),我們在類中實作這個方法就能夠像調用函數一般來調用對象了:
class Dog(): def __call__(self, breed): print('I\'m %s!' % breed)
這樣看起來好像對象和函數就沒什麼差別了,是以我們就可以将對象看成是函數,将函數看成是對象,實際上這兩者本來就沒啥差別!既然如此,我們怎麼判斷一個對象是否能夠像函數那樣被調用呢?可以通過判斷該對象是否是Callable類型,能被調用就是Callable類型,比如函數和實作了__call__()方法的類執行個體:In [268]: d = Dog() In [269]: d('Husky') I'm Husky!
In [270]: callable(d) Out[270]: True In [271]: callable(abs) Out[271]: True In [272]: callable(123) Out[272]: False In [273]: callable('hello') Out[273]: False
- 在本節的最後來看個鍊式調用的例子:
class Chain(): def __init__(self, info): self._info = info def __str__(self): return self._info def __call__(self): return Chain(self._info) def __getattr__(self, attr): return Chain(self._info) def test(self): return Chain(self._info)
相信大家已經看出來了,就這麼一句代碼将類中定義的方法全部都用上了!!現在我們來對這句代碼進行剖析。首先“Chain('鍊式調用測試!')”是建立個Chain執行個體(為了方面叙述,将這裡的執行個體記為INST1),“.test()”是調用該執行個體的test的方法,而該方法傳回的是Chain執行個體(将該執行個體記為INS2),這裡相信大家都能夠看得懂。接下來“.user1”是通路INS2的屬性user1,但是INS2并沒有預先綁定該屬性,是以這裡就調用了INS2的__getattr__()方法,該方法得到的也是一個Chain執行個體(将該執行個體記為INS3),接着,同樣也先是通路INS3的user2屬性,但INS3沒有預先綁定該屬性,則調用__getattr__()傳回一個執行個體(記為INS4),大家發現沒,這裡對該執行個體是以函數的方式調用的,是以調用INS4的__call__()方法,同樣傳回一個執行個體(記為INS5),最後回到最外層的print()函數對INS5進行列印調用INS5的__str__()方法,該方法傳回INS5的info屬性。至此代碼運作完畢!!簡單的一行代碼藏了很深的知識!!!了解這個鍊式調用栗子,那麼這一節的知識就算是弄通了!!!!In [283]: print(Chain('鍊式調用測試!').test().user1.user2()) 鍊式調用測試!
使用枚舉類
- 枚舉類型可以看成是一些标簽的集合,比如:月份包含一到十二個月,星期包含周一到周日,顔色包含紅橙黃綠青藍紫等等。python中提供了Enum類來實作枚舉類型,而我們可以通過繼承Enum來定制我們自己的枚舉類型,比如:
這裡要注意,①我們無法直接執行個體化枚舉類,枚舉類中的成員稱為單例,都是枚舉類型的;②每個标簽都被賦予一個固定的值,标簽的值也是不能夠更改的:from enum import Enum, unique class Month(Enum): Jan = 1 Feb = 2 Mar = 3 Apr = 4 May = 5 Jun = 6 Jul = 7 Aug = 8 Sep = 9 Oct = 10 Nov = 11 Dec = 12
标簽中的值可以重複,值重複的多個标簽會被認為是具有多個名字的同一個标簽,若要限定不同标簽值不能重複則要使用@unique裝飾器:In [336]: m = Month() #嘗試執行個體化枚舉類 Traceback (most recent call last): File "<ipython-input-336-1c51a47c3f35>", line 1, in <module> m = Month() TypeError: __call__() missing 1 required positional argument: 'value' In [337]: isinstance(Month.Jan, Month) Out[337]: True In [338]: Month.Jan = 13 Traceback (most recent call last): File "<ipython-input-337-27c71d0b74c9>", line 1, in <module> Month.Jan = 13 File "E:\Anaconda3\lib\enum.py", line 386, in __setattr__ raise AttributeError('Cannot reassign members.') AttributeError: Cannot reassign members.
由于标簽Jun、Jul、Aug三個标簽的值重複,抛出以下錯誤:from enum import Enum, unique @unique class Month(Enum): Jan = 1 Feb = 2 Mar = 3 Apr = 4 May = 5 Jun = 6 #值重複 Jul = 6 #值重複 Aug = 6 #值重複 Sep = 9 Oct = 10 Nov = 11 Dec = 12
枚舉類型通路方式有多種:ValueError: duplicate values found in <enum 'Month'>: Jul -> Jun, Aug -> Jun
這裡的特殊屬性__members__是一個将名稱映射到成員的有序字典,也可以通過它來完成周遊,大家可以自己試試周遊__members__.keys()、__members__.values()和__members__.items()看看會得到什麼結果。In [354]: Month.Feb #由标簽通路值 Out[354]: <Month.Feb: 2> In [355]: Month['Nov'] #由标簽通路值 Out[355]: <Month.Nov: 11> In [356]: Month(12) #由值通路标簽 Out[356]: <Month.Dec: 12> In [357]: for m in Month: #這種方式對于值相同的标簽隻列印一次 ...: print(m) ...: Month.Jan Month.Feb Month.Mar Month.Apr Month.May Month.Jun Month.Jul Month.Aug Month.Sep Month.Oct Month.Nov Month.Dec In [358]: for m in Month.__members__: #這種方式會列印出所有标簽,包括值相同的标簽 ...: print(m) ...: Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec In [359]: for name, member in Month.__members__.items(): ...: print(name, '=>', member) ...: Jan => Month.Jan Feb => Month.Feb Mar => Month.Mar Apr => Month.Apr May => Month.May Jun => Month.Jun Jul => Month.Jul Aug => Month.Aug Sep => Month.Sep Oct => Month.Oct Nov => Month.Nov Dec => Month.Dec
使用元類
- 前面我們用到過type()這個函數,可以用來判斷一個對象的類型:
它還有另一個功能——能夠建立類。之前我們建立類都是用下面這種形式:In [43]: type(1) Out[43]: int In [44]: type('hello') Out[44]: str In [45]: type((1,)) Out[45]: tuple In [46]: type({}) Out[46]: dict
其實在運作過程中,解釋器隻是掃描了下文法,最終還是要通過調用type()函數來建立,下面來看下如何用type來建立類:class Dog(): def __init__(self, name): self._name = name
type()需要傳入三個參數:①類名;②所繼承的父類; ③綁定的類屬型和方法。In [63]: def fun(self): #先定義函數 ...: print('hello world!') ...: In [64]: x = 1 In [66]: A = type('A', (object,), dict(show = fun, _x = x)) #調用type函數建立類A In [67]: A._x #通路A的類屬型 Out[67]: 1 In [68]: A().show() #調用方法 hello world!
- 除了用type函數建立類,我們還可用元類(metaclass)定制我們自己的類,metaclass就是類的模闆。我們知道,對象是類的執行個體,其實我們也可以将類了解為metaclass的“執行個體”。下面看看元類怎麼使用:
定義元類和定義一般的類是一樣的方式,但是記得要繼承type(其實所有的類都是type類型的),接着我們要在元類中實作一個__new__方法,__new__方法中的參數含義分别是:①目前要建立的類,相當于我們在定義一般類時的self; ②類名; ③類需要繼承的父類集合;④dict類型,類方法和集屬性集合。接下來建立類B,同時傳入關鍵字參數metaclass,表明使用我們定義的MyMetaclass來定制類:class MyMetaclass(type): #定義元類 def __new__(cls, name, bases, attr): attr['show'] = lambda self: print('hello world!') #給類添加show方法 attr['x'] = 7 #給類添加屬性x bases = (A,) #讓類繼承A return type.__new__(cls, name, bases, attr) class A(): #類A def __print__(self): print('I\'m \'A\'')
接下來進行測試:class B(metaclass = MyMetaclass): pass
In [78]: b = B() In [79]: B.x Out[79]: 7 In [80]: b.show() hello world! In [81]: b.__print__() I'm 'A'