python基礎-反射、內建方法
一 反射
python是動態語言,而反射(reflection)機制被視為動態語言的關鍵。
反射機制指的是在程式的執行狀態中
對於任意一個類,都可以知道這個類的所有屬性和方法;
對於任意一個物件,都能夠呼叫他的任意方法和屬性。
這種動態獲取程式資訊以及動態呼叫物件的功能稱為反射機制。
在python中實現反射非常簡單,在程式執行過程中,如果我們獲取一個不知道存有何種屬性的物件,若想操作其內部屬性,可以先通過內建函式dir來獲取任意一個類或者物件的屬性列表,列表中全為字串格式
>>> class People:
... def __init__(self,name,age,gender):
... self.name =name
... self.age=age
... self.gender=gender
...
>>> obj=People('egon',18,'male')
>>> dir(obj) # 列表中檢視到的屬性全為字串
[......,'age', 'gender', 'name']
接下來就是想辦法通過字串來操作物件的屬性了,這就涉及到內建函式hasattr、getattr、setattr、delattr的使用了(Python中一切皆物件,類和物件都可以被這四個函式操作,用法一樣)
class Teacher:
def __init__(self,full_name):
self.full_name =full_name
t=Teacher('Egon Lin')
# hasattr(object,'name')
hasattr(t,'full_name') # 按字串'full_name'判斷有無屬性t.full_name
# getattr(object, 'name', default=None)
getattr(t,'full_name',None) # 等同於t.full_name,不存在該屬性則返回預設值None
# setattr(x, 'y', v)
setattr(t,' age',18) # 等同於t.age=18
# delattr(x, 'y')
delattr(t,'age') # 等同於del t.age
# 1、先通過多dir:查看出某一個物件下可以.出哪些屬性來 # print(dir(obj)) # 2、可以通過字串反射到真正的屬性上,得到屬性值 # print(obj.__dict__[dir(obj)[-2]]) # 實現反射機制的步驟 # 四個內建函式的使用:通過字串來操作屬性值 # 1、hasattr() # print(hasattr(obj,'name')) # print(hasattr(obj,'x')) # 2、getattr() # print(getattr(obj,'name')) # 3、setattr() # setattr(obj,'name','EGON') # obj.name='EGON' # print(obj.name) # 4、delattr() # delattr(obj,'name') # del obj.name # print(obj.__dict__) # res1=getattr(obj,'say') # obj.say # res2=getattr(People,'say') # People.say # print(res1) # print(res2) # obj=10 # if hasattr(obj,'x'): # print(getattr(10,'x')) # else: # pass # print(getattr(obj,'x',None)) # if hasattr(obj,'x'): # setattr(obj,'x',111111111) # 10.x=11111 # else: # pass
基於反射可以十分靈活地操作物件的屬性,比如將使用者互動的結果反射到具體的功能執行
>>> class FtpServer:
... def serve_forever(self):
... while True:
... inp=input('input your cmd>>: ').strip()
... cmd,file=inp.split()
... if hasattr(self,cmd): # 根據使用者輸入的cmd,判斷物件self有無對應的方法屬性
... func=getattr(self,cmd) # 根據字串cmd,獲取物件self對應的方法屬性
... func(file)
... def get(self,file):
... print('Downloading %s...' %file)
... def put(self,file):
... print('Uploading %s...' %file)
...
>>> server=FtpServer()
>>> server.serve_forever()
input your cmd>>: get a.txt
Downloading a.txt...
input your cmd>>: put a.txt
Uploading a.txt...
二 內建方法
Python的Class機制內建了很多特殊的方法來幫助使用者高度定製自己的類,這些內建方法都是以雙下劃線開頭和結尾的,會在滿足某種條件時自動觸發,我們以常用的__str__和__del__為例來簡單介紹它們的使用。
__str__方法會在物件被列印時自動觸發,print功能列印的就是它的返回值,我們通常基於方法來定製物件的列印資訊,該方法必須返回字串型別
>>> class People:
... def __init__(self,name,age):
... self.name=name
... self.age=age
... def __str__(self):
... return '<Name:%s Age:%s>' %(self.name,self.age) #返回型別必須是字串
...
>>> p=People('lili',18)
>>> print(p) #觸發p.__str__(),拿到返回值後進行列印
<Name:lili Age:18>
__del__會在物件被刪除時自動觸發。由於Python自帶的垃圾回收機制會自動清理Python程式的資源,所以當一個物件只佔用應用程式級資源時,完全沒必要為物件定製__del__方法,但在產生一個物件的同時涉及到申請系統資源(比如系統開啟的檔案、網路連線等)的情況下,關於系統資源的回收,Python的垃圾回收機制便派不上用場了,需要我們為物件定製該方法,用來在物件被刪除時自動觸發回收系統資源的操作
class MySQL:
def __init__(self,ip,port):
self.conn=connect(ip,port) # 虛擬碼,發起網路連線,需要佔用系統資源
def __del__(self):
self.conn.close() # 關閉網路連線,回收系統資源
obj=MySQL('127.0.0.1',3306) # 在物件obj被刪除時,自動觸發obj.__del__()
元類介紹
什麼是元類呢?一切源自於一句話:python中一切皆為物件。讓我們先定義一個類,然後逐步分析
class StanfordTeacher(object): school='Stanford' def __init__(self,name,age): self.name=name self.age=age def say(self): print('%s says welcome to the Stanford to learn Python' %self.name)
所有的物件都是例項化或者說呼叫類而得到的(呼叫類的過程稱為類的例項化),比如物件t1是呼叫類StanfordTeacher得到的
t1=StanfordTeacher('lili',18) print(type(t1)) #檢視物件t1的類是<class '__main__.StanfordTeacher'>
如果一切皆為物件,那麼類StanfordTeacher本質也是一個物件,既然所有的物件都是呼叫類得到的,那麼StanfordTeacher必然也是呼叫了一個類得到的,這個類稱為元類
於是我們可以推匯出===>產生StanfordTeacher的過程一定發生了:StanfordTeacher=元類(...)
print(type(StanfordTeacher)) # 結果為<class 'type'>,證明是呼叫了type這個元類而產生的StanfordTeacher,即預設的元類為type
二 class關鍵字建立類的流程分析
上文我們基於python中一切皆為物件的概念分析出:我們用class關鍵字定義的類本身也是一個物件,負責產生該物件的類稱之為元類(元類可以簡稱為類的類),內建的元類為type
class關鍵字在幫我們建立類時,必然幫我們呼叫了元類StanfordTeacher=type(...),那呼叫type時傳入的引數是什麼呢?必然是類的關鍵組成部分,一個類有三大組成部分,分別是
1、類名class_name='StanfordTeacher'
2、基類們class_bases=(object,)
3、類的名稱空間class_dic,類的名稱空間是執行類體程式碼而得到的
呼叫type時會依次傳入以上三個引數
綜上,class關鍵字幫我們建立一個類應該細分為以下四個過程
補充:exec的用法
#exec:三個引數 #引數一:包含一系列python程式碼的字串 #引數二:全域性作用域(字典形式),如果不指定,預設為globals() #引數三:區域性作用域(字典形式),如果不指定,預設為locals() #可以把exec命令的執行當成是一個函式的執行,會將執行期間產生的名字存放於區域性名稱空間中 g={ 'x':1, 'y':2 } l={} exec(''' global x,z x=100 z=200 m=300 ''',g,l) print(g) #{'x': 100, 'y': 2,'z':200,......} print(l) #{'m': 300}
四 自定義元類控制類StanfordTeacher的建立
一個類沒有宣告自己的元類,預設他的元類就是type,除了使用內建元類type,我們也可以通過繼承type來自定義元類,然後使用metaclass關鍵字引數為一個類指定元類
class Mymeta(type): #只有繼承了type類才能稱之為一個元類,否則就是一個普通的自定義類 pass # StanfordTeacher=Mymeta('StanfordTeacher',(object),{...}) class StanfordTeacher(object,metaclass=Mymeta): school='Stanford' def __init__(self,name,age): self.name=name self.age=age def say(self): print('%s says welcome to the Stanford to learn Python' %self.name)
自定義元類可以控制類的產生過程,類的產生過程其實就是元類的呼叫過程,即StanfordTeacher=Mymeta('StanfordTeacher',(object),{...}),呼叫Mymeta會先產生一個空物件StanfordTeacher,然後連同呼叫Mymeta括號內的引數一同傳給Mymeta下的__init__方法,完成初始化,於是我們可以
class Mymeta(type): #只有繼承了type類才能稱之為一個元類,否則就是一個普通的自定義類 def __init__(self,class_name,class_bases,class_dic): # print(self) #<class '__main__.StanfordTeacher'> # print(class_bases) #(<class 'object'>,) # print(class_dic) #{'__module__': '__main__', '__qualname__': 'StanfordTeacher', 'school': 'Stanford',
# '__init__': <function StanfordTeacher.__init__ at 0x102b95ae8>, 'say': <function StanfordTeacher.say at 0x10621c6a8>} super(Mymeta, self).__init__(class_name, class_bases, class_dic) # 重用父類的功能 if class_name.islower(): raise TypeError('類名%s請修改為駝峰體' %class_name) if '__doc__' not in class_dic or len(class_dic['__doc__'].strip(' \n')) == 0: raise TypeError('類中必須有文件註釋,並且文件註釋不能為空') # StanfordTeacher=Mymeta('StanfordTeacher',(object),{...}) class StanfordTeacher(object,metaclass=Mymeta): """ 類StanfordTeacher的文件註釋 """ school='Stanford' def __init__(self,name,age): self.name=name self.age=age def say(self): print('%s says welcome to the Stanford to learn Python' %self.name)
五 自定義元類控制類StanfordTeacher的呼叫
儲備知識:__call__
class Foo: def __call__(self, *args, **kwargs): print(self) print(args) print(kwargs) obj=Foo() #1、要想讓obj這個物件變成一個可呼叫的物件,需要在該物件的類中定義一個方法__call__方法,該方法會在呼叫物件時自動觸發 #2、呼叫obj的返回值就是__call__方法的返回值 res=obj(1,2,3,x=1,y=2)
由上例得知,呼叫一個物件,就是觸發物件所在類中的__call__方法的執行,如果把StanfordTeacher也當做一個物件,那麼在StanfordTeacher這個物件的類中也必然存在一個__call__方法
class Mymeta(type): #只有繼承了type類才能稱之為一個元類,否則就是一個普通的自定義類 def __call__(self, *args, **kwargs): print(self) #<class '__main__.StanfordTeacher'> print(args) #('lili', 18) print(kwargs) #{} return 123 class StanfordTeacher(object,metaclass=Mymeta): school='Stanford' def __init__(self,name,age): self.name=name self.age=age def say(self): print('%s says welcome to the Stanford to learn Python' %self.name) # 呼叫StanfordTeacher就是在呼叫StanfordTeacher類中的__call__方法 # 然後將StanfordTeacher傳給self,溢位的位置引數傳給*,溢位的關鍵字引數傳給** # 呼叫StanfordTeacher的返回值就是呼叫__call__的返回值 t1=StanfordTeacher('lili',18) print(t1) #123
預設地,呼叫t1=StanfordTeacher('lili',18)會做三件事
1、產生一個空物件obj
2、呼叫__init__方法初始化物件obj
3、返回初始化好的obj
對應著,StanfordTeacher類中的__call__方法也應該做這三件事
class Mymeta(type): #只有繼承了type類才能稱之為一個元類,否則就是一個普通的自定義類 def __call__(self, *args, **kwargs): #self=<class '__main__.StanfordTeacher'> #1、呼叫__new__產生一個空物件obj obj=self.__new__(self) # 此處的self是類OldoyTeacher,必須傳參,代表建立一個StanfordTeacher的物件obj #2、呼叫__init__初始化空物件obj self.__init__(obj,*args,**kwargs) #3、返回初始化好的物件obj return obj class StanfordTeacher(object,metaclass=Mymeta): school='Stanford' def __init__(self,name,age): self.name=name self.age=age def say(self): print('%s says welcome to the Stanford to learn Python' %self.name) t1=StanfordTeacher('lili',18) print(t1.__dict__) #{'name': 'lili', 'age': 18}
上例的__call__相當於一個模板,我們可以在該基礎上改寫__call__的邏輯從而控制呼叫StanfordTeacher的過程,比如將StanfordTeacher的物件的所有屬性都變成私有的
class Mymeta(type): #只有繼承了type類才能稱之為一個元類,否則就是一個普通的自定義類 def __call__(self, *args, **kwargs): #self=<class '__main__.StanfordTeacher'> #1、呼叫__new__產生一個空物件obj obj=self.__new__(self) # 此處的self是類StanfordTeacher,必須傳參,代表建立一個StanfordTeacher的物件obj #2、呼叫__init__初始化空物件obj self.__init__(obj,*args,**kwargs) # 在初始化之後,obj.__dict__裡就有值了 obj.__dict__={'_%s__%s' %(self.__name__,k):v for k,v in obj.__dict__.items()} #3、返回初始化好的物件obj return obj class StanfordTeacher(object,metaclass=Mymeta): school='Stanford' def __init__(self,name,age): self.name=name self.age=age def say(self): print('%s says welcome to the Stanford to learn Python' %self.name) t1=StanfordTeacher('lili',18) print(t1.__dict__) #{'_StanfordTeacher__name': 'lili', '_StanfordTeacher__age': 18}
上例中涉及到查詢屬性的問題,比如self.__new__
# class Mymeta(type): # 只有繼承了type類的類才是元類 # # 空物件,"People",(),{...} # def __init__(self,x,y,z): # print('run22222222222....') # print(self) # # print(x) # # print(y) # # print(z) # # print(y) # # if not x.istitle(): # # raise NameError('類名的首字母必須大寫啊!!!') # # # 當前所在的類,呼叫類時所傳入的引數 # def __new__(cls, *args, **kwargs): # # 造Mymeta的物件 # print('run1111111111.....') # # print(cls,args,kwargs) # # return super().__new__(cls,*args, **kwargs) # return type.__new__(cls,*args, **kwargs) # # # # People=Mymeta("People",(object,),{...}) # # 呼叫Mymeta發生三件事,呼叫Mymeta就是type.__call__ # # 1、先造一個空物件=>People,呼叫Mymeta類內的__new__方法 # # 2、呼叫Mymeta這個類內的__init__方法,完成初始化物件的操作 # # 3、返回初始化好的物件 # # class People(metaclass=Mymeta): # def __init__(self,name,age): # self.name=name # self.age=age # # def say(self): # print('%s:%s' %(self.name,self.name)) # # # 強調: # # 只要是呼叫類,那麼會一次呼叫 # # 1、類內的__new__ # # 2、類內的__init__ # __call__ # class Foo: # def __init__(self,x,y): # self.x=x # self.y=y # # # obj,1,2,3,a=4,b=5,c=6 # def __call__(self,*args,**kwargs): # print('===>',args,kwargs) # return 123 # # obj=Foo(111,222) # # print(obj) # obj.__str__ # res=obj(1,2,3,a=4,b=5,c=6) # res=obj.__call__() # print(res) # 應用:如果想讓一個物件可以加括號呼叫,需要在該物件的類中新增一個方法__call__ # 總結: # 物件()->類內的__call__ # 類()->自定義元類內的__call__ # 自定義元類()->內建元類__call__ # 自定義元類控制類的呼叫=》類的物件的產生 class Mymeta(type): # 只有繼承了type類的類才是元類 def __call__(self, *args, **kwargs): # 1、Mymeta.__call__函式內會先呼叫People內的__new__ people_obj=self.__new__(self) # 2、Mymeta.__call__函式內會呼叫People內的__init__ self.__init__(people_obj,*args, **kwargs) # print('people物件的屬性:',people_obj.__dict__) people_obj.__dict__['xxxxx']=11111 # 3、Mymeta.__call__函式內會返回一個初始化好的物件 return people_obj # 類的產生 # People=Mymeta()=》type.__call__=>幹了3件事 # 1、type.__call__函式內會先呼叫Mymeta內的__new__ # 2、type.__call__函式內會呼叫Mymeta內的__init__ # 3、type.__call__函式內會返回一個初始化好的物件 class People(metaclass=Mymeta): def __init__(self,name,age): self.name=name self.age=age def say(self): print('%s:%s' %(self.name,self.name)) def __new__(cls, *args, **kwargs): # 產生真正的物件 return object.__new__(cls) # 類的呼叫 # obj=People('egon',18) =》Mymeta.__call__=》幹了3件事 # 1、Mymeta.__call__函式內會先呼叫People內的__new__ # 2、Mymeta.__call__函式內會呼叫People內的__init__ # 3、Mymeta.__call__函式內會返回一個初始化好的物件 obj1=People('egon',18) obj2=People('egon',18) # print(obj) print(obj1.__dict__) print(obj2.__dict__)
五 再看屬性查詢
結合python繼承的實現原理+元類重新看屬性的查詢應該是什麼樣子呢???
在學習完元類後,其實我們用class自定義的類也全都是物件(包括object類本身也是元類type的 一個例項,可以用type(object)檢視),我們學習過繼承的實現原理,如果把類當成物件去看,將下述繼承應該說成是:物件StanfordTeacher繼承物件Foo,物件Foo繼承物件Bar,物件Bar繼承物件object
class Mymeta(type): #只有繼承了type類才能稱之為一個元類,否則就是一個普通的自定義類 n=444 def __call__(self, *args, **kwargs): #self=<class '__main__.StanfordTeacher'> obj=self.__new__(self) self.__init__(obj,*args,**kwargs) return obj class Bar(object): n=333 class Foo(Bar): n=222 class StanfordTeacher(Foo,metaclass=Mymeta): n=111 school='Stanford' def __init__(self,name,age): self.name=name self.age=age def say(self): print('%s says welcome to the Stanford to learn Python' %self.name) print(StanfordTeacher.n) #自下而上依次註釋各個類中的n=xxx,然後重新執行程式,發現n的查詢順序為StanfordTeacher->Foo->Bar->object->Mymeta->type
於是屬性查詢應該分成兩層,一層是物件層(基於c3演算法的MRO)的查詢,另外一個層則是類層(即元類層)的查詢
#查詢順序:
#1、先物件層:StanfordTeacher->Foo->Bar->object
#2、然後元類層:Mymeta->type
依據上述總結,我們來分析下元類Mymeta中__call__裡的self.__new__的查詢
class Mymeta(type): n=444 def __call__(self, *args, **kwargs): #self=<class '__main__.StanfordTeacher'> obj=self.__new__(self) print(self.__new__ is object.__new__) #True class Bar(object): n=333 # def __new__(cls, *args, **kwargs): # print('Bar.__new__') class Foo(Bar): n=222 # def __new__(cls, *args, **kwargs): # print('Foo.__new__') class StanfordTeacher(Foo,metaclass=Mymeta): n=111 school='Stanford' def __init__(self,name,age): self.name=name self.age=age def say(self): print('%s says welcome to the Stanford to learn Python' %self.name) # def __new__(cls, *args, **kwargs): # print('StanfordTeacher.__new__') StanfordTeacher('lili',18) #觸發StanfordTeacher的類中的__call__方法的執行,進而執行self.__new__開始查詢
總結,Mymeta下的__call__裡的self.__new__在StanfordTeacher、Foo、Bar裡都沒有找到__new__的情況下,會去找object裡的__new__,而object下預設就有一個__new__,所以即便是之前的類均未實現__new__,也一定會在object中找到一個,根本不會、也根本沒必要再去找元類Mymeta->type中查詢__new__
我們在元類的__call__中也可以用object.__new__(self)去造物件
但我們還是推薦在__call__中使用self.__new__(self)去創造空物件,因為這種方式會檢索三個類StanfordTeacher->Foo->Bar,而object.__new__則是直接跨過了他們三個
最後說明一點
class Mymeta(type): #只有繼承了type類才能稱之為一個元類,否則就是一個普通的自定義類 n=444 def __new__(cls, *args, **kwargs): obj=type.__new__(cls,*args,**kwargs) # 必須按照這種傳值方式 print(obj.__dict__) # return obj # 只有在返回值是type的物件時,才會觸發下面的__init__ return 123 def __init__(self,class_name,class_bases,class_dic): print('run。。。') class StanfordTeacher(object,metaclass=Mymeta): #StanfordTeacher=Mymeta('StanfordTeacher',(object),{...}) n=111 school='Stanford' def __init__(self,name,age): self.name=name self.age=age def say(self): print('%s says welcome to the Stanford to learn Python' %self.name) print(type(Mymeta)) #<class 'type'> # 產生類StanfordTeacher的過程就是在呼叫Mymeta,而Mymeta也是type類的一個物件,那麼Mymeta之所以可以呼叫,一定是在元類type中有一個__call__方法 # 該方法中同樣需要做至少三件事: # class type: # def __call__(self, *args, **kwargs): #self=<class '__main__.Mymeta'> # obj=self.__new__(self,*args,**kwargs) # 產生Mymeta的一個物件 # self.__init__(obj,*args,**kwargs) # return obj