1. 程式人生 > 實用技巧 >python基礎-反射、內建方法

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