方法value作用於物件range時失敗_Python入門教程 | 第 7 章 面向物件高階程式設計
技術標籤:方法value作用於物件range時失敗
第七章 面向物件高階程式設計
資料封裝、繼承和多型只是面向物件程式設計中最基礎的 3 個概念。在 Python 中,面向物件還有很多高階特性,允許我們寫出非常強大的功能。
我們會討論多重繼承、定製類、元類等概念。
7.1 使用 __slots__
正常情況下,當我們定義了一個 class,建立了一個 class 的例項後,我們可以給該例項繫結任何屬性
和方法
,這就是動態語言的靈活性。先定義 class:
classStudent(object):
pass
然後,嘗試給例項繫結一個屬性:
>>>s=Student()
>>>s.name='Michael'#動態給例項繫結一個屬性
>>>print(s.name)
Michael
還可以嘗試給例項繫結一個方法:
>>>defset_age(self,age):#定義一個函式作為例項方法
...self.age=age
...
>>>fromtypesimportMethodType
>>>s.set_age=MethodType(set_age,s)#給例項繫結一個方法
>>>s.set_age(25)#呼叫例項方法
>>>s.age#測試結果
25
但是,給一個例項繫結的方法,對另一個例項是不起作用的:
>>>s2=Student()#建立新的例項
>>>s2.set_age(25)#嘗試呼叫方法
Traceback(mostrecentcalllast):
File"",line1,in
AttributeError:'Student'objecthasnoattribute'set_age'
為了給所有例項都繫結方法,可以給 class 繫結方法:
>>>defset_score(self,score):
...self.score=score
...
>>>Student.set_score=set_score
給 class 繫結方法後,所有例項均可呼叫:
>>>s.set_score(100)
>>>s.score
100
>>>s2.set_score(99)
>>>s2.score
99
通常情況下,上面的set_score
方法可以直接定義在 class 中,但動態繫結允許我們在程式執行的過程中動態給 class 加上功能,這在靜態語言中很難實現。
但是,如果我們想要限制例項的屬性怎麼辦?比如,只允許對 Student 例項新增name
和age
屬性。
為了達到限制的目的,Python 允許在定義 class 的時候,定義一個特殊的__slots__
變數,來限制該 class 例項能新增的屬性:
classStudent(object):
__slots__=('name','age')#用tuple定義允許繫結的屬性名稱
然後,我們試試:
>>>s=Student()#建立新的例項
>>>s.name='Michael'#繫結屬性'name'
>>>s.age=25#繫結屬性'age'
>>>s.score=99#繫結屬性'score'
Traceback(mostrecentcalllast):
File"",line1,in
AttributeError:'Student'objecthasnoattribute'score'
由於'score'
沒有被放到__slots__
中,所以不能繫結score
屬性,試圖繫結score
將得到AttributeError
的錯誤。
使用__slots__
要注意,__slots__
定義的屬性僅對當前類例項起作用,對繼承的子類是不起作用的:
>>>classGraduateStudent(Student):
...pass
...
>>>g=GraduateStudent()
>>>g.score=9999
除非在子類中也定義__slots__
,這樣,子類例項允許定義的屬性就是自身的__slots__
加上父類的__slots__
。
7.2 使用 @property
在繫結屬性時,如果我們直接把屬性暴露出去,雖然寫起來很簡單,但是,沒辦法檢查引數,導致可以把成績隨便改:
s=Student()
s.score=9999
這顯然不合邏輯。為了限制 score 的範圍,可以通過一個set_score()
方法來設定成績,再通過一個get_score()
來獲取成績,這樣,在set_score()
方法裡,就可以檢查引數:
classStudent(object):
defget_score(self):
returnself._score
defset_score(self,value):
ifnotisinstance(value,int):
raiseValueError('scoremustbeaninteger!')
ifvalue0orvalue>100:
raiseValueError('scoremustbetween0~100!')
self._score=value
現在,對任意的 Student 例項進行操作,就不能隨心所欲地設定 score 了:
>>>s=Student()
>>>s.set_score(60)#ok!
>>>s.get_score()
60
>>>s.set_score(9999)
Traceback(mostrecentcalllast):
...
ValueError:scoremustbetween0~100!
但是,上面的呼叫方法又略顯複雜,沒有直接用屬性這麼直接簡單。
有沒有既能檢查引數,又可以用類似屬性這樣簡單的方式來訪問類的變數呢?對於追求完美的 Python 程式設計師來說,這是必須要做到的!
還記得裝飾器(decorator)可以給函式動態加上功能嗎?對於類的方法,裝飾器一樣起作用。Python 內建的@property
裝飾器就是負責把一個方法變成屬性呼叫的:
classStudent(object):
@property
defscore(self):
returnself._score
@score.setter
defscore(self,value):
ifnotisinstance(value,int):
raiseValueError('scoremustbeaninteger!')
ifvalue0orvalue>100:
raiseValueError('scoremustbetween0~100!')
self._score=value
@property
的實現比較複雜,我們先考察如何使用。把一個 getter 方法變成屬性,只需要加上@property
就可以了,此時,@property
本身又建立了另一個裝飾器@score.setter
,負責把一個 setter 方法變成屬性賦值,於是,我們就擁有一個可控的屬性操作:
>>>s=Student()
>>>s.score=60#OK,實際轉化為s.set_score(60)
>>>s.score#OK,實際轉化為s.get_score()
60
>>>s.score=9999
Traceback(mostrecentcalllast):
...
ValueError:scoremustbetween0~100!
注意到這個神奇的@property
,我們在對例項屬性操作的時候,就知道該屬性很可能不是直接暴露的,而是通過 getter 和 setter 方法來實現的。
還可以定義只讀屬性,只定義 getter 方法,不定義 setter 方法就是一個只讀屬性:
classStudent(object):
@property
defbirth(self):
returnself._birth
@birth.setter
defbirth(self,value):
self._birth=value
@property
defage(self):
return2015-self._birth
上面的birth
是可讀寫屬性,而age
就是一個只讀屬性,因為age
可以根據birth
和當前時間計算出來。
❝小結:
❞
@property
廣泛應用在類的定義中,可以讓呼叫者寫出簡短的程式碼,同時保證對引數進行必要的檢查,這樣,程式執行時就減少了出錯的可能性。
❝練習:
❞
請利用@property
給一個Screen
物件加上width
和height
屬性,以及一個只讀屬性resolution
:
classScreen(object):
@property
defwidth(self):
returnself._width
@width.setter
defwidth(self,value):
ifnotisinstance(value,(int,float)):
raiseValueError('widthmustbeannumber!')
ifvalue0:
raiseValueError('widthmust>0')
self._width=value
@property
defheight(self):
returnself._height
@height.setter
defheight(self,value):
ifnotisinstance(value,(int,float)):
raiseValueError('heightmustbeannumber!')
ifvalue0:
raiseValueError('heightmust>0')
self._height=value
@property
defresolution(self):
returnself._height*self._width
測試:
#測試:
s=Screen()
s.width=1024
s.height=768
print('resolution=',s.resolution)
ifs.resolution==786432:
print('測試通過!')
else:
print('測試失敗!')
7.3 多重繼承
繼承是面向物件程式設計的一個重要的方式,因為通過繼承,子類就可以擴充套件父類的功能。
回憶一下Animal
類層次的設計,假設我們要實現以下 4 種動物:
- Dog - 狗狗;
- Bat - 蝙蝠;
- Parrot - 鸚鵡;
- Ostrich - 鴕鳥。
如果按照哺乳動物和鳥類歸類,我們可以設計出這樣的類的層次:
但是如果按照“能跑”和“能飛”來歸類,我們就應該設計出這樣的類的層次:
如果要把上面的兩種分類都包含進來,我們就得設計更多的層次:
- 哺乳類:能跑的哺乳類,能飛的哺乳類;
- 鳥類:能跑的鳥類,能飛的鳥類。
這麼一來,類的層次就複雜了:
如果要再增加“寵物類”和“非寵物類”,這麼搞下去,類的數量會呈指數增長,很明顯這樣設計是不行的。
正確的做法是採用多重繼承。首先,主要的類層次仍按照哺乳類和鳥類設計:
classAnimal(object):
pass
#大類:
classMammal(Animal):
pass
classBird(Animal):
pass
#各種動物:
classDog(Mammal):
pass
classBat(Mammal):
pass
classParrot(Bird):
pass
classOstrich(Bird):
pass
現在,我們要給動物再加上Runnable
和Flyable
的功能,只需要先定義好Runnable
和Flyable
的類:
classRunnable(object):
defrun(self):
print('Running...')
classFlyable(object):
deffly(self):
print('Flying...')
對於需要Runnable
功能的動物,就多繼承一個Runnable
,例如Dog
:
classDog(Mammal,Runnable):
pass
對於需要Flyable
功能的動物,就多繼承一個Flyable
,例如Bat
:
classBat(Mammal,Flyable):
pass
通過多重繼承,一個子類就可以同時獲得多個父類的所有功能。
7.3.1 MixIn
在設計類的繼承關係時,通常,主線都是單一繼承下來的,例如,Ostrich
繼承自Bird
。但是,如果需要“混入”額外的功能,通過多重繼承就可以實現,比如,讓Ostrich
除了繼承自Bird
外,再同時繼承Runnable
。這種設計通常稱之為 MixIn。
為了更好地看出繼承關係,我們把Runnable
和Flyable
改為RunnableMixIn
和FlyableMixIn
。類似的,你還可以定義出肉食動物CarnivorousMixIn
和植食動物HerbivoresMixIn
,讓某個動物同時擁有好幾個 MixIn:
classDog(Mammal,RunnableMixIn,CarnivorousMixIn):
pass
MixIn 的目的就是給一個類增加多個功能,這樣,在設計類的時候,我們優先考慮通過多重繼承來組合多個 MixIn 的功能,而不是設計多層次的複雜的繼承關係。
Python 自帶的很多庫也使用了 MixIn。舉個例子, Python 自帶了TCPServer
和UDPServer
這兩類網路服務,而要同時服務多個使用者就必須使用多程序或多執行緒模型,這兩種模型由ForkingMixIn
和ThreadingMixIn
提供。通過組合,我們就可以創造出合適的服務來。
比如,編寫一個多程序模式的 TCP 服務,定義如下:
classMyTCPServer(TCPServer,ForkingMixIn):
pass
編寫一個多執行緒模式的 UDP 服務,定義如下:
classMyUDPServer(UDPServer,ThreadingMixIn):
pass
如果你打算搞一個更先進的協程模型,可以編寫一個CoroutineMixIn
:
classMyTCPServer(TCPServer,CoroutineMixIn):
pass
這樣一來,我們不需要複雜而龐大的繼承鏈,只要選擇組合不同的類的功能,就可以快速構造出所需的子類。
❝小結:
❞
由於 Python 允許使用多重繼承,因此,MixIn 就是一種常見的設計。
只允許單一繼承的語言(如 Java)不能使用 MixIn 的設計。
7.4 定製類
看到類似__slots__
這種形如__xxx__
的變數或者函式名就要注意,這些在 Python 中是有特殊用途的。
__slots__
我們已經知道怎麼用了,__len__()
方法我們也知道是為了能讓 class 作用於len()
函式。
除此之外,Python 的 class 中還有許多這樣有特殊用途的函式,可以幫助我們定製類。
7.4.1 __str__
我們先定義一個Student
類,列印一個例項:
>>>classStudent(object):
...def__init__(self,name):
...self.name=name
...
>>>print(Student('Michael'))
<__main__.student>0x109afb190>
打印出一堆<__main__.student object at>
,不好看。
怎麼才能列印得好看呢?只需要定義好__str__()
方法,返回一個好看的字串就可以了:
>>>classStudent(object):
...def__init__(self,name):
...self.name=name
...def__str__(self):
...return'Studentobject(name:%s)'%self.name
...
>>>print(Student('Michael'))
Studentobject(name:Michael)
這樣打印出來的例項,不但好看,而且容易看出例項內部重要的資料。
但是細心的朋友會發現直接敲變數不用print
,打印出來的例項還是不好看:
>>>s=Student('Michael')
>>>s
<__main__.student>0x109afb310>
這是因為直接顯示變數呼叫的不是__str__()
,而是__repr__()
,兩者的區別是__str__()
返回使用者看到的字串,而__repr__()
返回程式開發者看到的字串,也就是說,__repr__()
是為除錯服務的。
解決辦法是再定義一個__repr__()
。但是通常__str__()
和__repr__()
程式碼都是一樣的,所以,有個偷懶的寫法:
classStudent(object):
def__init__(self,name):
self.name=name
def__str__(self):
return'Studentobject(name=%s)'%self.name
__repr__=__str__
7.4.2 __iter__
如果一個類想被用於for ... in
迴圈,類似 list 或 tuple 那樣,就必須實現一個__iter__()
方法,該方法返回一個迭代物件,然後,Python 的 for 迴圈就會不斷呼叫該迭代物件的__next__()
方法拿到迴圈的下一個值,直到遇到StopIteration
錯誤時退出迴圈。
我們以斐波那契數列為例,寫一個 Fib 類,可以作用於 for 迴圈:
classFib(object):
def__init__(self):
self.a,self.b=0,1#初始化兩個計數器a,b
def__iter__(self):
returnself#例項本身就是迭代物件,故返回自己
def__next__(self):
self.a,self.b=self.b,self.a+self.b#計算下一個值
ifself.a>100000:#退出迴圈的條件
raiseStopIteration()
returnself.a#返回下一個值
現在,試試把 Fib 例項作用於 for 迴圈:
forninFib():
print(n)
7.4.3 __getitem__
Fib例項雖然能作用於 for 迴圈,看起來和 list 有點像,但是,把它當成 list 來使用還是不行,比如,取第 5 個元素:
>>>Fib()[5]
Traceback(mostrecentcalllast):
File"",line1,in
TypeError:'Fib'objectdoesnotsupportindexing
要表現得像 list 那樣按照下標取出元素,需要實現__getitem__()
方法:
classFib(object):
def__getitem__(self,n):
a,b=1,1
forxinrange(n):
a,b=b,a+b
returna
現在,就可以按下標訪問數列的任意一項了:
>>>f=Fib()
>>>f[0]
1
>>>f[1]
1
>>>f[2]
2
>>>f[3]
3
>>>f[10]
89
>>>f[100]
573147844013817084101
但是 list 有個神奇的切片方法:
>>>list(range(100))[5:10]
[5,6,7,8,9]
對於 Fib 卻報錯。原因是__getitem__()
傳入的引數可能是一個 int,也可能是一個切片物件slice
,所以要做判斷:
classFib(object):
def__getitem__(self,n):
ifisinstance(n,int):#n是索引
a,b=1,1
forxinrange(n):
a,b=b,a+b
returna
ifisinstance(n,slice):#n是切片
start=n.start
stop=n.stop
ifstartisNone:
start=0
a,b=1,1
L=[]
forxinrange(stop):
ifx>=start:
L.append(a)
a,b=b,a+b
returnL
現在試試 Fib 的切片:
>>>f=Fib()
>>>f[0:5]
[1,1,2,3,5]
>>>f[:10]
[1,1,2,3,5,8,13,21,34,55]
但是沒有對 step 引數作處理:
>>>f[:10:2]
[1,1,2,3,5,8,13,21,34,55,89]
也沒有對負數作處理,所以,要正確實現一個__getitem__()
還是有很多工作要做的。
此外,如果把物件看成dict
,__getitem__()
的引數也可能是一個可以作 key 的 object,例如str
。
與之對應的是__setitem__()
方法,把物件視作 list 或 dict 來對集合賦值。最後,還有一個__delitem__()
方法,用於刪除某個元素。
總之,通過上面的方法,我們自己定義的類表現得和 Python 自帶的 list、tuple、dict 沒什麼區別,這完全歸功於動態語言的“鴨子型別”,不需要強制繼承某個介面。
7.4.4 __getattr__
正常情況下,當我們呼叫類的方法或屬性時,如果不存在,就會報錯。比如定義Student
類:
classStudent(object):
def__init__(self):
self.name='Michael'
呼叫name
屬性,沒問題,但是,呼叫不存在的score
屬性,就有問題了:
>>>s=Student()
>>>print(s.name)
Michael
>>>print(s.score)
Traceback(mostrecentcalllast):
...
AttributeError:'Student'objecthasnoattribute'score'
錯誤資訊很清楚地告訴我們,沒有找到score
這個 attribute。
要避免這個錯誤,除了可以加上一個score
屬性外,Python 還有另一個機制,那就是寫一個__getattr__()
方法,動態返回一個屬性。修改如下:
classStudent(object):
def__init__(self):
self.name='Michael'
def__getattr__(self,attr):
ifattr=='score':
return99
當呼叫不存在的屬性時,比如score
,Python 直譯器會試圖呼叫__getattr__(self, 'score')
來嘗試獲得屬性,這樣,我們就有機會返回score
的值:
>>>s=Student()
>>>s.name
'Michael'
>>>s.score
99
返回函式也是完全可以的:
classStudent(object):
def__getattr__(self,attr):
ifattr=='age':
returnlambda:25
只是呼叫方式要變為:
>>>s.age()
25
注意,只有在沒有找到屬性的情況下,才呼叫__getattr__
,已有的屬性,比如name
,不會在__getattr__
中查詢。
此外,注意到任意呼叫如s.abc
都會返回None
,這是因為我們定義的__getattr__
預設返回就是None
。要讓 class 只響應特定的幾個屬性,我們就要按照約定,丟擲AttributeError
的錯誤:
classStudent(object):
def__getattr__(self,attr):
ifattr=='age':
returnlambda:25
raiseAttributeError('\'Student\'objecthasnoattribute\'%s\''%attr)
這實際上可以把一個類的所有屬性和方法呼叫全部動態化處理了,不需要任何特殊手段。
這種完全動態呼叫的特性有什麼實際作用呢?作用就是,可以針對完全動態的情況作呼叫。
舉個例子:
現在很多網站都搞 REST API,比如新浪微博、豆瓣啥的,呼叫 API 的 URL 類似:
- http://api.server/user/friends
- http://api.server/user/timeline/list
如果要寫 SDK,給每個 URL 對應的 API 都寫一個方法,那得累死,而且,API 一旦改動,SDK 也要改。
利用完全動態的__getattr__
,我們可以寫出一個鏈式呼叫:
classChain(object):
def__init__(self,path=''):
self._path=path
def__getattr__(self,path):
returnChain('%s/%s'%(self._path,path))
def__str__(self):
returnself._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
就可以非常方便地呼叫 API 了。有興趣的童鞋可以試試寫出來。
7.4.5 __call__
一個物件例項可以有自己的屬性和方法,當我們呼叫例項方法時,我們用instance.method()
來呼叫。能不能直接在例項本身上呼叫呢?在 Python 中,答案是肯定的。
任何類,只需要定義一個__call__()
方法,就可以直接對例項進行呼叫。請看示例:
classStudent(object):
def__init__(self,name):
self.name=name
def__call__(self):
print('Mynameis%s.'%self.name)
呼叫方式如下:
>>>s=Student('Michael')
>>>s()#self引數不要傳入
MynameisMichael.
__call__()
還可以定義引數。對例項進行直接呼叫就好比對一個函式進行呼叫一樣,所以你完全可以把物件看成函式,把函式看成物件,因為這兩者之間本來就沒啥根本的區別。
如果你把物件看成函式,那麼函式本身其實也可以在執行期動態創建出來,因為類的例項都是執行期創建出來的,這麼一來,我們就模糊了物件和函式的界限。
那麼,怎麼判斷一個變數是物件還是函式呢?其實,更多的時候,我們需要判斷一個物件是否能被呼叫,能被呼叫的物件就是一個Callable
物件,比如函式和我們上面定義的帶有__call__()
的類例項:
>>>callable(Student())
True
>>>callable(max)
True
>>>callable([1,2,3])
False
>>>callable(None)
False
>>>callable('str')
False
通過callable()
函式,我們就可以判斷一個物件是否是“可呼叫”物件。
❝小結:
❞
- Python 的 class 允許定義許多定製方法,可以讓我們非常方便地生成特定的類。
- 本節介紹的是最常用的幾個定製方法,還有很多可定製的方法,請參考Python的官方文件[1]。
7.5 使用列舉類
當我們需要定義常量時,一個辦法是用大寫變數通過整數來定義,例如月份:
JAN=1
FEB=2
MAR=3
...
NOV=11
DEC=12
好處是簡單,缺點是型別是int
,並且仍然是變數。
更好的方法是為這樣的列舉型別定義一個 class 型別,然後,每個常量都是 class 的一個唯一例項。Python 提供了Enum
類來實現這個功能:
7.5.1 Enum
fromenumimportEnum
Month=Enum('Month',('Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'))
這樣我們就獲得了Month
型別的列舉類,可以直接使用Month.Jan
來引用一個常量,或者列舉它的所有成員:
forname,memberinMonth.__members__.items():
print(name,'=>',member,',',member.value)
value
屬性則是自動賦給成員的int
常量,預設從1
開始計數。
如果需要更精確地控制列舉型別,可以從Enum
派生出自定義類:
fromenumimportEnum,unique
@unique
classWeekday(Enum):
Sun=0#Sun的value被設定為0
Mon=1
Tue=2
Wed=3
Thu=4
Fri=5
Sat=6
@unique
裝飾器可以幫助我們檢查保證沒有重複值。
訪問這些列舉型別可以有若干種方法:
>>>day1=Weekday.Mon
>>>print(day1)
Weekday.Mon
>>>print(Weekday.Tue)
Weekday.Tue
>>>print(Weekday['Tue'])
Weekday.Tue
>>>print(Weekday.Tue.value)
2
>>>print(day1==Weekday.Mon)
True
>>>print(day1==Weekday.Tue)
False
>>>print(Weekday(1))
Weekday.Mon
>>>print(day1==Weekday(1))
True
>>>Weekday(7)
Traceback(mostrecentcalllast):
...
ValueError:7isnotavalidWeekday
>>>forname,memberinWeekday.__members__.items():
...print(name,'=>',member)
...
Sun=>Weekday.Sun
Mon=>Weekday.Mon
Tue=>Weekday.Tue
Wed=>Weekday.Wed
Thu=>Weekday.Thu
Fri=>Weekday.Fri
Sat=>Weekday.Sat
可見,既可以用成員名稱引用列舉常量,又可以直接根據value的值獲得列舉常量。
7.6 使用元類
7.6.1 type()
動態語言和靜態語言最大的不同,就是函式和類的定義,不是編譯時定義的,而是執行時動態建立的。
比方說我們要定義一個Hello
的 class,就寫一個hello.py
模組:
classHello(object):
defhello(self,name='world'):
print('Hello,%s.'%name)
當 Python 直譯器載入hello
模組時,就會依次執行該模組的所有語句,執行結果就是動態創建出一個Hello
的 class 物件,測試如下:
>>>fromhelloimportHello
>>>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))#建立Helloclass
>>>h=Hello()
>>>h.hello()
Hello,world.
>>>print(type(Hello))
<class'type'>
>>>print(type(h))
<class'__main__.Hello'>
要建立一個class物件,type()
函式依次傳入3個引數:
- class的名稱;
- 繼承的父類集合,注意Python支援多重繼承,如果只有一個父類,別忘了tuple的單元素寫法;
- class的方法名稱與函式繫結,這裡我們把函式
fn
繫結到方法名hello
上。
通過type()
函式建立的類和直接寫class是完全一樣的,因為Python直譯器遇到class定義時,僅僅是掃描一下class定義的語法,然後呼叫type()
函式創建出class。
正常情況下,我們都用class Xxx...
來定義類,但是,type()
函式也允許我們動態創建出類來,也就是說,動態語言本身支援執行期動態建立類,這和靜態語言有非常大的不同,要在靜態語言執行期建立類,必須構造原始碼字串再呼叫編譯器,或者藉助一些工具生成位元組碼實現,本質上都是動態編譯,會非常複雜。
7.6.2 metaclass
除了使用type()
動態建立類以外,要控制類的建立行為,還可以使用metaclass。
metaclass,直譯為元類,簡單的解釋就是:
當我們定義了類以後,就可以根據這個類創建出例項,所以:先定義類,然後建立例項。
但是如果我們想創建出類呢?那就必須根據metaclass創建出類,所以:先定義metaclass,然後建立類。
連線起來就是:先定義metaclass,就可以建立類,最後建立例項。
所以,metaclass允許你建立類或者修改類。換句話說,你可以把類看成是metaclass創建出來的“例項”。
metaclass是Python面向物件裡最難理解,也是最難使用的魔術程式碼。正常情況下,你不會碰到需要使用metaclass的情況,所以,以下內容看不懂也沒關係,因為基本上你不會用到。
我們先看一個簡單的例子,這個metaclass可以給我們自定義的MyList增加一個add
方法:
定義ListMetaclass
,按照預設習慣,metaclass的類名總是以Metaclass結尾,以便清楚地表示這是一個metaclass:
#metaclass是類的模板,所以必須從`type`型別派生:
classListMetaclass(type):
def__new__(cls,name,bases,attrs):
attrs['add']=lambdaself,value:self.append(value)
returntype.__new__(cls,name,bases,attrs)
有了ListMetaclass,我們在定義類的時候還要指示使用ListMetaclass來定製類,傳入關鍵字引數metaclass
:
classMyList(list,metaclass=ListMetaclass):
pass
當我們傳入關鍵字引數metaclass
時,魔術就生效了,它指示Python直譯器在建立MyList
時,要通過ListMetaclass.__new__()
來建立,在此,我們可以修改類的定義,比如,加上新的方法,然後,返回修改後的定義。
__new__()
方法接收到的引數依次是:
- 當前準備建立的類的物件;
- 類的名字;
- 類繼承的父類集合;
- 類的方法集合。
測試一下MyList
是否可以呼叫add()
方法:
>>>L=MyList()
>>>L.add(1)
>>L
[1]
而普通的list
沒有add()
方法:
>>>L2=list()
>>>L2.add(1)
Traceback(mostrecentcalllast):
File"",line1,in
AttributeError:'list'objecthasnoattribute'add'
動態修改有什麼意義?直接在MyList
定義中寫上add()
方法不是更簡單嗎?正常情況下,確實應該直接寫,通過metaclass修改純屬變態。
但是,總會遇到需要通過metaclass修改類定義的。ORM就是一個典型的例子。
ORM全稱“Object Relational Mapping”,即物件-關係對映,就是把關係資料庫的一行對映為一個物件,也就是一個類對應一個表,這樣,寫程式碼更簡單,不用直接操作SQL語句。
要編寫一個ORM框架,所有的類都只能動態定義,因為只有使用者才能根據表的結構定義出對應的類來。
讓我們來嘗試編寫一個ORM框架。
編寫底層模組的第一步,就是先把呼叫介面寫出來。比如,使用者如果使用這個ORM框架,想定義一個User
類來操作對應的資料庫表User
,我們期待他寫出這樣的程式碼:
classUser(Model):
#定義類的屬性到列的對映:
id=IntegerField('id')
name=StringField('username')
email=StringField('email')
password=StringField('password')
#建立一個例項:
u=User(id=12345,name='Michael',email='[email protected]',password='my-pwd')
#儲存到資料庫:
u.save()
其中,父類Model
和屬性型別StringField
、IntegerField
是由ORM框架提供的,剩下的魔術方法比如save()
全部由metaclass自動完成。雖然metaclass的編寫會比較複雜,但ORM的使用者用起來卻異常簡單。
現在,我們就按上面的介面來實現該ORM。
首先來定義Field
類,它負責儲存資料庫表的欄位名和欄位型別:
classField(object):
def__init__(self,name,column_type):
self.name=name
self.column_type=column_type
def__str__(self):
return''%(self.__class__.__name__,self.name)
在Field
的基礎上,進一步定義各種型別的Field
,比如StringField
,IntegerField
等等:
classStringField(Field):
def__init__(self,name):
super(StringField,self).__init__(name,'varchar(100)')
classIntegerField(Field):
def__init__(self,name):
super(IntegerField,self).__init__(name,'bigint')
下一步,就是編寫最複雜的ModelMetaclass
了:
classModelMetaclass(type):
def__new__(cls,name,bases,attrs):
ifname=='Model':
returntype.__new__(cls,name,bases,attrs)
print('Foundmodel:%s'%name)
mappings=dict()
fork,vinattrs.items():
ifisinstance(v,Field):
print('Foundmapping:%s==>%s'%(k,v))
mappings[k]=v
forkinmappings.keys():
attrs.pop(k)
attrs['__mappings__']=mappings#儲存屬性和列的對映關係
attrs['__table__']=name#假設表名和類名一致
returntype.__new__(cls,name,bases,attrs)
以及基類Model
:
classModel(dict,metaclass=ModelMetaclass):
def__init__(self,**kw):
super(Model,self).__init__(**kw)
def__getattr__(self,key):
try:
returnself[key]
exceptKeyError:
raiseAttributeError(r"'Model'objecthasnoattribute'%s'"%key)
def__setattr__(self,key,value):
self[key]=value
defsave(self):
fields=[]
params=[]
args=[]
fork,vinself.__mappings__.items():
fields.append(v.name)
params.append('?')
args.append(getattr(self,k,None))
sql='insertinto%s(%s)values(%s)'%(self.__table__,','.join(fields),','.join(params))
print('SQL:%s'%sql)
print('ARGS:%s'%str(args))
當用戶定義一個class User(Model)
時,Python直譯器首先在當前類User
的定義中查詢metaclass
,如果沒有找到,就繼續在父類Model
中查詢metaclass
,找到了,就使用Model
中定義的metaclass
的ModelMetaclass
來建立User
類,也就是說,metaclass可以隱式地繼承到子類,但子類自己卻感覺不到。
在ModelMetaclass
中,一共做了幾件事情:
- 排除掉對
Model
類的修改; - 在當前類(比如
User
)中查詢定義的類的所有屬性,如果找到一個Field屬性,就把它儲存到一個__mappings__
的dict中,同時從類屬性中刪除該Field屬性,否則,容易造成執行時錯誤(例項的屬性會遮蓋類的同名屬性); - 把表名儲存到
__table__
中,這裡簡化為表名預設為類名。
在Model
類中,就可以定義各種操作資料庫的方法,比如save()
,delete()
,find()
,update
等等。
我們實現了save()
方法,把一個例項儲存到資料庫中。因為有表名,屬性到欄位的對映和屬性值的集合,就可以構造出INSERT
語句。
編寫程式碼試試:
u=User(id=12345,name='Michael',email='[email protected]',password='my-pwd')
u.save()
輸出如下:
Foundmodel:User
Foundmapping:email==>
Foundmapping:password==>
Foundmapping:id==>
Foundmapping:name==>
SQL:insertintoUser(password,email,username,id)values(?,?,?,?)
ARGS:['my-pwd','[email protected]','Michael',12345]
可以看到,save()
方法已經打印出了可執行的SQL語句,以及引數列表,只需要真正連線到資料庫,執行該SQL語句,就可以完成真正的功能。
不到100行程式碼,我們就通過metaclass實現了一個精簡的ORM框架,是不是非常簡單?
❝小結:
❞
metaclass是Python中非常具有魔術性的物件,它可以改變類建立時的行為。這種強大的功能使用起來務必小心。
7.7 參考資料
- [廖雪峰 - Python 3.x - 面向物件高階程式設計](
Reference
[1]Python的官方文件: http://docs.python.org/3/reference/datamodel.html#special-method-names